Skip to content

Commit 9feb1f6

Browse files
committed
fix: add scoped runtime tool source
1 parent 9caa8a9 commit 9feb1f6

10 files changed

Lines changed: 276 additions & 10 deletions

inc/Api/Chat/ChatOrchestrator.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -734,16 +734,17 @@ public static function executeConversationTurn(
734734
}
735735

736736
$resolver = new ToolPolicyResolver();
737+
$client_context = $options['client_context'] ?? array();
737738
$all_tools = $resolver->resolve(
738739
array(
739-
'modes' => $modes,
740-
'agent_id' => $agent_id,
741-
'agent_slug' => $agent_slug,
742-
'user_id' => $user_id,
743-
'interactive' => true,
740+
'modes' => $modes,
741+
'agent_id' => $agent_id,
742+
'agent_slug' => $agent_slug,
743+
'user_id' => $user_id,
744+
'interactive' => true,
745+
'client_context' => is_array( $client_context ) ? $client_context : array(),
744746
)
745747
);
746-
$client_context = $options['client_context'] ?? array();
747748

748749
// `calling_user_id` is the human user on whose behalf this AI invocation
749750
// is running. In a chat session that's the chat caller. Tools that resolve
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
/**
3+
* Run-scoped runtime tool source.
4+
*
5+
* Adapts client/transport-declared runtime tool definitions into the normal
6+
* tool source pipeline. These tools are visible to model requests only after
7+
* the existing ToolPolicyResolver/Agents API policy pass allows them; execution
8+
* remains outside PHP because declarations are marked with a client executor.
9+
*
10+
* @package DataMachine\Engine\AI\Tools\Sources
11+
*/
12+
13+
namespace DataMachine\Engine\AI\Tools\Sources;
14+
15+
use AgentsAPI\AI\Tools\WP_Agent_Tool_Declaration;
16+
17+
defined( 'ABSPATH' ) || exit;
18+
19+
final class RuntimeToolSource {
20+
21+
/**
22+
* Gather normalized runtime tool declarations from resolver context.
23+
*
24+
* @param array $modes Agent mode slugs.
25+
* @param array $args Full resolution arguments.
26+
* @return array Tools keyed by tool name.
27+
*/
28+
public function __invoke( array $modes, array $args = array() ): array {
29+
$tools = array();
30+
31+
foreach ( $this->declarationsFromContext( $args ) as $declaration ) {
32+
if ( ! is_array( $declaration ) ) {
33+
continue;
34+
}
35+
36+
try {
37+
$normalized = WP_Agent_Tool_Declaration::normalize( $declaration );
38+
} catch ( \InvalidArgumentException ) {
39+
continue;
40+
}
41+
42+
$name = (string) $normalized['name'];
43+
$tools[ $name ] = array_merge(
44+
$normalized,
45+
array(
46+
'modes' => $modes,
47+
'access_level' => 'public',
48+
'runtime_tool' => true,
49+
'external_executor' => true,
50+
'requires_opt_in' => true,
51+
)
52+
);
53+
}
54+
55+
return $tools;
56+
}
57+
58+
/**
59+
* Extract declarations from explicit resolver args and nested client context.
60+
*
61+
* @param array $args Full resolution arguments.
62+
* @return array<int,array|string|mixed> Runtime declarations.
63+
*/
64+
private function declarationsFromContext( array $args ): array {
65+
$sets = array(
66+
$args['runtime_tool_declarations'] ?? null,
67+
$args['runtime_tools'] ?? null,
68+
);
69+
70+
$client_context = is_array( $args['client_context'] ?? null ) ? $args['client_context'] : array();
71+
$sets[] = $client_context['runtime_tool_declarations'] ?? null;
72+
$sets[] = $client_context['runtime_tools'] ?? null;
73+
$sets[] = $client_context['tool_declarations'] ?? null;
74+
75+
$declarations = array();
76+
foreach ( $sets as $set ) {
77+
if ( ! is_array( $set ) ) {
78+
continue;
79+
}
80+
81+
foreach ( $set as $name => $declaration ) {
82+
if ( ! is_array( $declaration ) ) {
83+
continue;
84+
}
85+
86+
if ( is_string( $name ) && '' !== $name && empty( $declaration['name'] ) ) {
87+
$declaration['name'] = $name;
88+
}
89+
90+
$declarations[] = $declaration;
91+
}
92+
}
93+
94+
return $declarations;
95+
}
96+
}

inc/Engine/AI/Tools/ToolExecutor.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ public static function executeTool(
6767
$tool_def = $prepared['tool_def'];
6868
$complete_parameters = $prepared['parameters'];
6969

70+
if ( 'client' === (string) ( $tool_def['executor'] ?? '' ) || ! empty( $tool_def['external_executor'] ) ) {
71+
return array(
72+
'success' => false,
73+
'error' => sprintf( 'Tool "%s" is declared for client-side execution and cannot be executed by the Data Machine PHP tool executor.', $tool_name ),
74+
'tool_name' => $tool_name,
75+
'executor' => 'client',
76+
);
77+
}
78+
7079
// Resolve the action policy for this invocation. Tools without
7180
// action_policy metadata resolve to 'direct' and behave exactly
7281
// as before this feature landed.

inc/Engine/AI/Tools/ToolPolicyResolver.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ public function resolve( array $args ): array {
102102
$args['modes'] = $modes;
103103
$tools = $this->gatherByModes( $modes, $args );
104104

105+
$agent_policy = $agent_id > 0 ? $this->getAgentToolPolicy( $agent_id ) : null;
106+
$tools = $this->filterRuntimeToolsByPolicyOptIn( $tools, $args, $agent_policy );
107+
105108
// 2. Delegate generic mode/allow/deny/category policy resolution to Agents API.
106109
$policy_context = array_merge(
107110
$args,
@@ -112,7 +115,6 @@ public function resolve( array $args ): array {
112115
)
113116
);
114117

115-
$agent_policy = $agent_id > 0 ? $this->getAgentToolPolicy( $agent_id ) : null;
116118
if ( null !== $agent_policy ) {
117119
$policy_context['agent_config'] = array( 'tool_policy' => $agent_policy );
118120
} else {
@@ -145,6 +147,47 @@ private function gatherByModes( array $modes, array $args ): array {
145147
return $this->tool_source_registry->gather( $modes, $args );
146148
}
147149

150+
/**
151+
* Keep client-declared runtime tools only when explicitly opted in.
152+
*
153+
* Runtime declarations are caller supplied and client-executed. They must be
154+
* named by an existing allow path before the generic policy pass can expose
155+
* them to a provider request.
156+
*
157+
* @param array $tools Resolved tools keyed by name.
158+
* @param array $args Resolver args.
159+
* @param array|null $agent_policy Optional persisted agent policy.
160+
* @return array Filtered tools.
161+
*/
162+
private function filterRuntimeToolsByPolicyOptIn( array $tools, array $args, ?array $agent_policy ): array {
163+
$runtime_tool_names = array();
164+
foreach ( $tools as $name => $tool ) {
165+
if ( is_array( $tool ) && ! empty( $tool['runtime_tool'] ) ) {
166+
$runtime_tool_names[] = (string) $name;
167+
}
168+
}
169+
170+
if ( empty( $runtime_tool_names ) ) {
171+
return $tools;
172+
}
173+
174+
$allowed = $this->policy_filter->string_list( $args['allow_only'] ?? array() );
175+
foreach ( array( $agent_policy, $args['tool_policy'] ?? null ) as $policy ) {
176+
if ( is_array( $policy ) && \WP_Agent_Tool_Policy::MODE_ALLOW === ( $policy['mode'] ?? \WP_Agent_Tool_Policy::MODE_DENY ) ) {
177+
$allowed = array_merge( $allowed, $this->policy_filter->string_list( $policy['tools'] ?? array() ) );
178+
}
179+
}
180+
181+
$allowed = array_flip( array_values( array_unique( $allowed ) ) );
182+
foreach ( $runtime_tool_names as $name ) {
183+
if ( ! isset( $allowed[ $name ] ) ) {
184+
unset( $tools[ $name ] );
185+
}
186+
}
187+
188+
return $tools;
189+
}
190+
148191
/**
149192
* Get tool policy from an agent's config.
150193
*

inc/Engine/AI/Tools/ToolSourceRegistry.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111

1212
use DataMachine\Engine\AI\Tools\Sources\AdjacentHandlerToolSource;
1313
use DataMachine\Engine\AI\Tools\Sources\DataMachineToolRegistrySource;
14+
use DataMachine\Engine\AI\Tools\Sources\RuntimeToolSource;
1415

1516
defined( 'ABSPATH' ) || exit;
1617

1718
class ToolSourceRegistry {
1819

1920
public const SOURCE_STATIC_REGISTRY = 'static_registry';
2021
public const SOURCE_ADJACENT_HANDLERS = 'adjacent_handlers';
22+
public const SOURCE_RUNTIME_TOOLS = 'runtime_tools';
2123

2224
private ToolManager $tool_manager;
2325

@@ -63,6 +65,7 @@ private function getRegisteredSources( array $modes, array $args ): array {
6365
$sources = apply_filters(
6466
'agents_api_tool_sources',
6567
array(
68+
self::SOURCE_RUNTIME_TOOLS => new RuntimeToolSource(),
6669
self::SOURCE_STATIC_REGISTRY => new DataMachineToolRegistrySource( $this->tool_manager ),
6770
self::SOURCE_ADJACENT_HANDLERS => new AdjacentHandlerToolSource(),
6871
),
@@ -83,8 +86,8 @@ private function getRegisteredSources( array $modes, array $args ): array {
8386
*/
8487
private function getSourcesForModes( array $modes, array $args ): array {
8588
$sources = in_array( ToolPolicyResolver::MODE_PIPELINE, $modes, true )
86-
? array( self::SOURCE_ADJACENT_HANDLERS, self::SOURCE_STATIC_REGISTRY )
87-
: array( self::SOURCE_STATIC_REGISTRY );
89+
? array( self::SOURCE_RUNTIME_TOOLS, self::SOURCE_ADJACENT_HANDLERS, self::SOURCE_STATIC_REGISTRY )
90+
: array( self::SOURCE_RUNTIME_TOOLS, self::SOURCE_STATIC_REGISTRY );
8891

8992
// @phpstan-ignore-next-line WordPress apply_filters accepts additional hook arguments.
9093
$sources = apply_filters( 'agents_api_tool_sources_for_mode', $sources, $modes, $args );

tests/adjacent-handler-tool-policy-smoke.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ function apply_filters( string $hook, $value ) {
3737

3838
require_once __DIR__ . '/../inc/Core/Steps/FlowStepConfig.php';
3939
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-access-policy.php';
40+
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-declaration.php';
4041
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-policy-filter.php';
4142
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-policy.php';
4243
require_once __DIR__ . '/../inc/Engine/AI/Tools/Policy/DataMachineAgentToolPolicyProvider.php';
4344
require_once __DIR__ . '/../inc/Engine/AI/Tools/Policy/DataMachineMandatoryToolPolicy.php';
4445
require_once __DIR__ . '/../inc/Engine/AI/Tools/Policy/DataMachineToolAccessPolicy.php';
46+
require_once __DIR__ . '/../inc/Engine/AI/Tools/Sources/RuntimeToolSource.php';
47+
require_once __DIR__ . '/../inc/Engine/AI/Tools/Sources/DataMachineToolRegistrySource.php';
4548
require_once __DIR__ . '/../inc/Engine/AI/Tools/Sources/AdjacentHandlerToolSource.php';
4649
require_once __DIR__ . '/../inc/Engine/AI/Tools/ToolSourceRegistry.php';
4750
require_once __DIR__ . '/../inc/Engine/AI/Tools/ToolPolicyResolver.php';

tests/ai-completion-assertion-packet-smoke.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,15 @@ function get_option( string $key, $default_value = false ) {
5858
require_once __DIR__ . '/../inc/Core/Steps/QueueableTrait.php';
5959
require_once __DIR__ . '/../inc/Core/Steps/FlowStepConfig.php';
6060
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-access-policy.php';
61+
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-declaration.php';
6162
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-policy-filter.php';
6263
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-policy.php';
6364
require_once __DIR__ . '/../inc/Engine/AI/ConversationManager.php';
6465
require_once __DIR__ . '/../inc/Engine/AI/Tools/ToolManager.php';
6566
require_once __DIR__ . '/../inc/Engine/AI/Tools/Policy/DataMachineAgentToolPolicyProvider.php';
6667
require_once __DIR__ . '/../inc/Engine/AI/Tools/Policy/DataMachineMandatoryToolPolicy.php';
6768
require_once __DIR__ . '/../inc/Engine/AI/Tools/Policy/DataMachineToolAccessPolicy.php';
69+
require_once __DIR__ . '/../inc/Engine/AI/Tools/Sources/RuntimeToolSource.php';
6870
require_once __DIR__ . '/../inc/Engine/AI/Tools/Sources/DataMachineToolRegistrySource.php';
6971
require_once __DIR__ . '/../inc/Engine/AI/Tools/Sources/AdjacentHandlerToolSource.php';
7072
require_once __DIR__ . '/../inc/Engine/AI/Tools/ToolSourceRegistry.php';

tests/pipeline-tool-policy-snapshot-smoke.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,14 @@ function wp_generate_uuid4(): string {
6262
require_once __DIR__ . '/../inc/Core/Steps/WorkflowConfigFactory.php';
6363
require_once __DIR__ . '/../inc/Core/Steps/AI/ToolPolicy/PipelineToolPolicyArgs.php';
6464
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-access-policy.php';
65+
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-declaration.php';
6566
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-policy-filter.php';
6667
require_once __DIR__ . '/../vendor/automattic/agents-api/src/Tools/class-wp-agent-tool-policy.php';
6768
require_once __DIR__ . '/../inc/Engine/AI/Tools/ToolManager.php';
6869
require_once __DIR__ . '/../inc/Engine/AI/Tools/Policy/DataMachineAgentToolPolicyProvider.php';
6970
require_once __DIR__ . '/../inc/Engine/AI/Tools/Policy/DataMachineMandatoryToolPolicy.php';
7071
require_once __DIR__ . '/../inc/Engine/AI/Tools/Policy/DataMachineToolAccessPolicy.php';
72+
require_once __DIR__ . '/../inc/Engine/AI/Tools/Sources/RuntimeToolSource.php';
7173
require_once __DIR__ . '/../inc/Engine/AI/Tools/Sources/DataMachineToolRegistrySource.php';
7274
require_once __DIR__ . '/../inc/Engine/AI/Tools/Sources/AdjacentHandlerToolSource.php';
7375
require_once __DIR__ . '/../inc/Engine/AI/Tools/ToolSourceRegistry.php';

0 commit comments

Comments
 (0)