Skip to content

Commit 7a94b4f

Browse files
committed
Use runtime tool overlays for sandbox workspaces
1 parent b9b384f commit 7a94b4f

15 files changed

Lines changed: 187 additions & 207 deletions

.github/scripts/run-agent-task/execute-native-agent-task.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,10 @@ if (execution.code === 0 && runtimeRecord.success === true && verificationPassed
426426
await runnerWorkspaceCore.verifyRunnerWorkspaceIntegrity(workspaceApply.integrity)
427427
const testPublisher = string(process.env.WP_CODEBOX_TEST_PUBLISHER_MODULE)
428428
const publisher = testPublisher ? (await import(pathToFileURL(resolve(testPublisher)).href)).publishRunnerWorkspace : publishRunnerWorkspace
429-
publication = await publisher({ request, changedFiles: workspaceApply.changedFiles, publicationFiles: workspaceApply.publicationFiles, token: process.env.ACCESS_TOKEN || process.env.GITHUB_TOKEN })
429+
const testHook = testPublisher && process.env.WP_CODEBOX_TEST_PUBLISHER_HOOK
430+
? JSON.parse(process.env.WP_CODEBOX_TEST_PUBLISHER_HOOK)
431+
: undefined
432+
publication = await publisher({ request, changedFiles: workspaceApply.changedFiles, publicationFiles: workspaceApply.publicationFiles, token: process.env.ACCESS_TOKEN || process.env.GITHUB_TOKEN, ...(testHook ? { testHook } : {}) })
430433
runtimeRecord.metadata = { ...record(runtimeRecord.metadata), runner_workspace_publication: publication }
431434
runtimeRecord.outputs = { ...record(runtimeRecord.outputs), runner_workspace_publication: publication }
432435
}

docs/agent-task-reusable-workflow.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,12 @@ package file is mounted into or visible from the agent workspace.
105105
## Runtime Coverage
106106

107107
The repository's native-loop and PHP runtime-package tests execute generated
108-
PHP with narrow WordPress and native agent-registry
109-
shims. They prove digest-then-schema validation, canonical import, exact slug
110-
resolution, `agents/chat` selection, invocation order, and temporary-file
111-
cleanup. They are not a WordPress Playground end-to-end test: this repository
112-
does not provide a fixture that boots both the agent-registry plugin and a real
113-
provider-backed chat turn in Playground. The existing Playground CLI tests use
114-
injected CLI modules and do not exercise that plugin/provider path.
108+
PHP with narrow WordPress and native agent-registry shims. The non-skippable
109+
Playground integration additionally boots the agent registry and OpenAI
110+
provider path with a local HTTP interceptor. Its deterministic three-turn
111+
fixture reads and edits a seeded sandbox workspace, verifies provider tool
112+
outputs, and captures the canonical changed-files manifest and patch without
113+
network access or provider billing.
115114

116115
To run the optional cross-repository package coverage, use a Docs Agent checkout
117116
pinned to commit `3da1b8076359db9bf9f4ee7dadcc3932c080ed71`, which contains

fixtures/agent-task-runtime-sources-run-29299109269.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"run_id": "29299109269",
33
"runtime_sources": [
4-
{ "version": 1, "role": "component", "repository": "automattic/agents-api", "revision": "59d1e6b473f22498e40e279130bbb4f9bcde3b73", "path": ".", "metadata": { "slug": "agents-api", "loadAs": "mu-plugin", "activate": false } },
4+
{ "version": 1, "role": "component", "repository": "automattic/agents-api", "revision": "fbd5641d412af76a1b8288426a577e750838b4be", "path": ".", "metadata": { "slug": "agents-api", "loadAs": "mu-plugin", "activate": false } },
55
{ "version": 1, "role": "provider_plugin", "source": { "type": "https_zip", "url": "https://downloads.wordpress.org/plugin/ai-provider-for-openai.1.0.3.zip", "sha256": "48f3c0c714b3164cda79d320829830d5a0ea1116e0b19653da8af898a22d3bb6", "archive_root": "ai-provider-for-openai" }, "metadata": { "slug": "ai-provider-for-openai", "pluginFile": "plugin.php", "activate": true, "providers": ["openai"] } },
66
{ "version": 1, "role": "bundled_library", "repository": "wordpress/php-ai-client", "revision": "631704201d15ffeff7091ad3bc7156db74054956", "path": ".", "metadata": { "library": "php-ai-client", "strategy": "wordpress-scoped-bundle" } }
77
]

packages/runtime-core/src/runner-workspace-apply.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,10 @@ function exactlyOne(refs: RunnerWorkspaceArtifactRef[], kind: string): RunnerWor
114114

115115
async function artifactPath(root: string, value: string): Promise<string> {
116116
if (!value) throw new Error("Artifact reference path is required.")
117-
const candidate = await realpath(isAbsolute(value) ? resolve(value) : resolve(root, value))
117+
const requested = isAbsolute(value) ? resolve(value) : resolve(root, value)
118+
const requestedStat = await lstat(requested)
119+
if (!requestedStat.isFile() || requestedStat.isSymbolicLink()) throw new Error("Artifact must be a bounded regular file.")
120+
const candidate = await realpath(requested)
118121
if (!pathIsWithinRoot(candidate, root)) throw new Error("Artifact reference escapes the trusted artifact root.")
119122
return candidate
120123
}

packages/wordpress-plugin/src/class-wp-codebox-agent-runtime-invoker.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ function wp_codebox_browser_runtime_execute_ability( string $ability_name, array
194194
return $ability->execute( $input );
195195
}
196196
197-
function wp_codebox_browser_runtime_prepare_input( array $payload, array $invocation, string $session_id, array $runtime_tool_declarations, array $ability_tools, array $allowed_tool_ids, array $sandbox_tool_ids ): array {
197+
function wp_codebox_browser_runtime_prepare_input( array $payload, array $invocation, string $session_id, array $ability_tools, array $allowed_tool_ids, array $sandbox_tool_ids ): array {
198198
$agent = sanitize_key( (string) ( $payload['agent'] ?? '' ) );
199199
if ( '' === $agent ) {
200200
$agent = 'wp-codebox-sandbox';
@@ -230,8 +230,6 @@ function wp_codebox_browser_runtime_prepare_input( array $payload, array $invoca
230230
'client_name' => 'wp-codebox-browser-runner',
231231
'caller_session_id' => $session_id,
232232
'task_input' => $payload['task_input'] ?? array(),
233-
'runtime_tools' => $runtime_tool_declarations,
234-
'runtime_tool_callback' => 'wp_codebox_browser_runtime_tool_callback',
235233
'ability_tools' => $ability_tools,
236234
),
237235
);
@@ -249,7 +247,7 @@ function wp_codebox_browser_runtime_prepare_input( array $payload, array $invoca
249247
}
250248
251249
$input = array_replace_recursive( $base_input, is_array( $invocation['input'] ?? null ) ? $invocation['input'] : array() );
252-
return function_exists( 'apply_filters' ) ? apply_filters( 'wp_codebox_browser_runtime_invocation_input', $input, $payload, $invocation, $session_id, $runtime_tool_declarations, $ability_tools, $allowed_tool_ids, $sandbox_tool_ids ) : $input;
250+
return function_exists( 'apply_filters' ) ? apply_filters( 'wp_codebox_browser_runtime_invocation_input', $input, $payload, $invocation, $session_id, $ability_tools, $allowed_tool_ids, $sandbox_tool_ids ) : $input;
253251
}
254252
255253
function wp_codebox_browser_runtime_import_agent_bundles( array $bundle_specs ): array {

packages/wordpress-plugin/src/class-wp-codebox-browser-runner-template.php

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -969,44 +969,6 @@ function wp_codebox_browser_runtime_tool_name( string $tool_id ): string {
969969
return \'\';
970970
}
971971
972-
function wp_codebox_browser_runtime_tool_declarations( array $tool_names ): array {
973-
global $wp_codebox_browser_artifact_environment;
974-
975-
$declarations = array();
976-
foreach ( WP_Codebox_Sandbox_Workspace_Executor::tool_declarations() as $name => $declaration ) {
977-
$base = substr( $name, strlen( "client/" ) );
978-
if ( in_array( $base, $tool_names, true ) ) {
979-
$declarations[ $base ] = $declaration;
980-
}
981-
}
982-
983-
$environment = is_array( $wp_codebox_browser_artifact_environment ?? null ) ? $wp_codebox_browser_artifact_environment : array();
984-
$root = (string) ( $environment[\'root\'] ?? \'wp-codebox-output/\' );
985-
$base_path = rtrim( (string) ( $environment[\'base_path\'] ?? \'/wordpress/wp-content/uploads/wp-codebox/artifacts\' ), \'/\' );
986-
$declarations[\'filesystem_write\'] = array(
987-
\'name\' => \'filesystem_write\',
988-
\'source\' => \'client\',
989-
\'description\' => sprintf( \'Write one generated artifact file inside %s/%s. Call this once per file required by the caller artifact contract.\', $base_path, $root ),
990-
\'executor\' => \'client\',
991-
\'scope\' => \'run\',
992-
\'parameters\' => array(
993-
\'type\' => \'object\',
994-
\'required\' => array( \'path\', \'content\' ),
995-
\'properties\' => array(
996-
\'path\' => array( \'type\' => \'string\', \'description\' => sprintf( \'Relative artifact path under %s, for example %sindex.html.\', $root, $root ) ),
997-
\'content\' => array( \'type\' => \'string\', \'description\' => \'Full file contents. Use UTF-8 text unless encoding is base64.\' ),
998-
\'encoding\' => array( \'type\' => \'string\', \'enum\' => array( \'utf-8\', \'base64\' ) ),
999-
),
1000-
),
1001-
\'runtime\' => array(
1002-
\'environment\' => \'runtime_local\',
1003-
\'capability_scope\' => \'runtime_local\',
1004-
),
1005-
);
1006-
1007-
return $declarations;
1008-
}
1009-
1010972
function wp_codebox_browser_runtime_ability_tool_declarations( array $payload ): array {
1011973
$task_input = is_array( $payload[\'task_input\'] ?? null ) ? $payload[\'task_input\'] : array();
1012974
$declared = is_array( $task_input[\'ability_tools\'] ?? null ) ? $task_input[\'ability_tools\'] : array();
@@ -1129,17 +1091,6 @@ function wp_codebox_browser_runtime_replay_ability_lifecycle(): array {
11291091
}
11301092
11311093
function wp_codebox_browser_runtime_tool_callback( array $request, array $payload ) {
1132-
1133-
$workspace_tool = (string) ( $request[\'tool_name\'] ?? \'\' );
1134-
if ( preg_match( "/^(?:client\\/)?workspace_(?:show|ls|read|grep|write|edit|apply_patch|git_status|git_diff)$/", $workspace_tool ) ) {
1135-
$task_input = is_array( $payload[\'task_input\'] ?? null ) ? $payload[\'task_input\'] : array();
1136-
$context = array(
1137-
\'workspace_root\' => \'/workspace\',
1138-
\'writable_paths\' => is_array( $task_input[\'writable_paths\'] ?? null ) ? $task_input[\'writable_paths\'] : array(),
1139-
);
1140-
return ( new WP_Codebox_Sandbox_Workspace_Executor() )->executeWP_Agent_Tool_Call( $request, is_array( $request[\'tool_def\'] ?? null ) ? $request[\'tool_def\'] : array(), $context );
1141-
}
1142-
11431094
if ( ! in_array( (string) ( $request[\'tool_name\'] ?? \'\' ), array( \'filesystem_write\', \'client/filesystem-write\' ), true ) ) {
11441095
return null;
11451096
}

packages/wordpress-plugin/src/class-wp-codebox-runtime-package-executor.php

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,39 @@ private function execute_workflow( array $task ): array|WP_Error {
124124
$input['message'] = $this->string_value( $input['prompt'] ?? '' );
125125
}
126126
$input['runtime_package_task'] = $task;
127+
$input['run_id'] = $this->runtime_run_id( $input );
128+
$workspace_context = $this->sandbox_workspace_context( $input );
127129
$ability_object = function_exists( 'wp_get_ability' ) ? wp_get_ability( $ability ) : null;
128130
if ( ! is_object( $ability_object ) || ! method_exists( $ability_object, 'execute' ) ) {
129131
return new WP_Error( 'wp_codebox_runtime_package_workflow_unavailable', 'Runtime package workflow ability is unavailable.', array( 'status' => 500, 'ability' => $ability ) );
130132
}
131133

132-
$result = $ability_object->execute( $input );
134+
$runtime_tool_declarations = static function ( array $declarations, $agent, array $runtime_context ) use ( $input, $workspace_context ): array {
135+
if ( ! $agent instanceof \WP_Agent || ! hash_equals( (string) $input['agent'], (string) $runtime_context['agent_slug'] ) || ! hash_equals( (string) $input['run_id'], (string) $runtime_context['run_id'] ) ) {
136+
return $declarations;
137+
}
138+
return array_merge( $declarations, WP_Codebox_Sandbox_Workspace_Executor::tool_declarations_for_enabled_tools( $agent->get_default_config(), $workspace_context ) );
139+
};
140+
$tool_executors = static function ( array $executors, array $runtime_context ) use ( $input, $workspace_context ): array {
141+
if ( ! hash_equals( (string) $input['agent'], (string) ( $runtime_context['agent_slug'] ?? '' ) ) || ! hash_equals( (string) $input['run_id'], (string) ( $runtime_context['run_id'] ?? '' ) ) ) {
142+
return $executors;
143+
}
144+
$executors[ WP_Codebox_Sandbox_Workspace_Executor::TARGET_ID ] = WP_Codebox_Sandbox_Workspace_Executor::executor_for_context( $workspace_context );
145+
return $executors;
146+
};
147+
$filters_available = function_exists( 'add_filter' ) && function_exists( 'remove_filter' );
148+
if ( $filters_available ) {
149+
add_filter( 'agents_api_runtime_tool_declarations', $runtime_tool_declarations, 20, 3 );
150+
add_filter( 'agents_api_tool_executors', $tool_executors, 20, 2 );
151+
}
152+
try {
153+
$result = $ability_object->execute( $input );
154+
} finally {
155+
if ( $filters_available ) {
156+
remove_filter( 'agents_api_runtime_tool_declarations', $runtime_tool_declarations, 20 );
157+
remove_filter( 'agents_api_tool_executors', $tool_executors, 20 );
158+
}
159+
}
133160
if ( is_wp_error( $result ) ) {
134161
return $result;
135162
}
@@ -148,6 +175,19 @@ private function execute_workflow( array $task ): array|WP_Error {
148175
return $result;
149176
}
150177

178+
/** @param array<string,mixed> $input @return array<string,mixed> */
179+
private function sandbox_workspace_context( array $input ): array {
180+
$policy = is_array( $input['runner_workspace_policy'] ?? null ) ? $input['runner_workspace_policy'] : array();
181+
$paths = is_array( $policy['writable_paths'] ?? null ) ? $policy['writable_paths'] : array();
182+
return array( 'workspace_root' => '/workspace', 'writable_paths' => $paths );
183+
}
184+
185+
/** @param array<string,mixed> $input */
186+
private function runtime_run_id( array $input ): string {
187+
$run_id = $this->string_value( $input['run_id'] ?? '' );
188+
return '' !== $run_id ? $run_id : 'wp-codebox-runtime-' . ( function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : uniqid( '', true ) );
189+
}
190+
151191
/** @param array<string,mixed> $task Runtime package task. @param array<int,array<string,mixed>> $imports Import results. @return string|WP_Error */
152192
private function imported_agent_slug( array $task, array $imports ): string|WP_Error {
153193
$package = is_array( $task['package'] ?? null ) ? $task['package'] : array();

0 commit comments

Comments
 (0)