From 9ccc6d26a7d662ebb04c6f35a1d430757219a59b Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 17 May 2026 21:29:24 -0400 Subject: [PATCH 1/2] feat: add sandbox runtime agent probe --- inc/Abilities/WordPressRuntimeAbilities.php | 65 ++++++ .../SandboxRuntimeAgentProbeRunner.php | 196 ++++++++++++++++++ ...oke-sandbox-runtime-agent-probe-runner.php | 163 +++++++++++++++ 3 files changed, 424 insertions(+) create mode 100644 inc/Runtime/SandboxRuntimeAgentProbeRunner.php create mode 100644 tests/smoke-sandbox-runtime-agent-probe-runner.php diff --git a/inc/Abilities/WordPressRuntimeAbilities.php b/inc/Abilities/WordPressRuntimeAbilities.php index 1295a1c..a4d3e14 100644 --- a/inc/Abilities/WordPressRuntimeAbilities.php +++ b/inc/Abilities/WordPressRuntimeAbilities.php @@ -8,6 +8,7 @@ namespace DataMachineCode\Abilities; use DataMachine\Abilities\PermissionHelper; +use DataMachineCode\Runtime\SandboxRuntimeAgentProbeRunner; use DataMachineCode\Runtime\WordPressRuntimeInspector; defined( 'ABSPATH' ) || exit; @@ -145,6 +146,65 @@ private function registerAbilities(): void { 'meta' => array( 'show_in_rest' => true ), ) ); + + wp_register_ability( + 'datamachine-code/sandbox-runtime-agent-probe', + array( + 'label' => 'Probe Sandbox Runtime Agent Stack', + 'description' => 'Run a WordPress Playground sandbox through the Sandbox Runtime CLI and verify the Agents API, Data Machine, Data Machine Code, and OpenAI provider stack activates cleanly.', + 'category' => 'datamachine-code-runtime', + 'input_schema' => array( + 'type' => 'object', + 'required' => array( 'agents_api_path', 'data_machine_path', 'openai_provider_path' ), + 'properties' => array( + 'agents_api_path' => array( + 'type' => 'string', + 'description' => 'Local checkout path for the Agents API plugin.', + ), + 'data_machine_path' => array( + 'type' => 'string', + 'description' => 'Local checkout path for the Data Machine plugin.', + ), + 'data_machine_code_path' => array( + 'type' => 'string', + 'description' => 'Local checkout path for Data Machine Code. Defaults to the active plugin path.', + ), + 'openai_provider_path' => array( + 'type' => 'string', + 'description' => 'Local checkout path for the AI Provider for OpenAI plugin.', + ), + 'sandbox_runtime_bin' => array( + 'type' => 'string', + 'description' => 'Sandbox Runtime CLI binary or path. JS dist files are run through node. Defaults to sandbox-runtime.', + ), + 'wp' => array( + 'type' => 'string', + 'description' => 'WordPress version passed to Playground. Defaults to trunk.', + ), + 'artifacts_path' => array( + 'type' => 'string', + 'description' => 'Directory where Sandbox Runtime should write artifact bundles.', + ), + ), + ), + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'success' => array( 'type' => 'boolean' ), + 'schema' => array( 'type' => 'string' ), + 'command' => array( 'type' => 'string' ), + 'wp' => array( 'type' => 'string' ), + 'paths' => array( 'type' => 'object' ), + 'artifacts' => array( 'type' => 'string' ), + 'exit_code' => array( 'type' => 'integer' ), + 'probe' => array( 'type' => 'object' ), + ), + ), + 'execute_callback' => array( self::class, 'sandboxRuntimeAgentProbe' ), + 'permission_callback' => fn() => PermissionHelper::can_manage(), + 'meta' => array( 'show_in_rest' => true ), + ) + ); }; if ( function_exists( 'doing_action' ) && doing_action( 'wp_abilities_api_init' ) ) { @@ -169,4 +229,9 @@ public static function ls( array $input ): array|\WP_Error { public static function read( array $input ): array|\WP_Error { return ( new WordPressRuntimeInspector() )->read( $input ); } + + /** @param array $input Input parameters. @return array|\WP_Error */ + public static function sandboxRuntimeAgentProbe( array $input ): array|\WP_Error { + return ( new SandboxRuntimeAgentProbeRunner() )->run( $input ); + } } diff --git a/inc/Runtime/SandboxRuntimeAgentProbeRunner.php b/inc/Runtime/SandboxRuntimeAgentProbeRunner.php new file mode 100644 index 0000000..0eb72e2 --- /dev/null +++ b/inc/Runtime/SandboxRuntimeAgentProbeRunner.php @@ -0,0 +1,196 @@ + */ + private array $callbacks; + + /** + * @param array $callbacks Test seams for pure-PHP smoke coverage. + */ + public function __construct( array $callbacks = array() ) { + $this->callbacks = $callbacks; + } + + /** + * Run the Sandbox Runtime WordPress agent stack probe. + * + * @param array $input Probe input. + * @return array|\WP_Error + */ + public function run( array $input ): array|\WP_Error { + if ( ! $this->shell_available() ) { + return new \WP_Error( 'datamachine_code_shell_unavailable', 'Shell execution is not available for Sandbox Runtime.', array( 'status' => 500 ) ); + } + + $paths = array( + 'agents_api' => $this->clean_path( (string) ( $input['agents_api_path'] ?? '' ) ), + 'data_machine' => $this->clean_path( (string) ( $input['data_machine_path'] ?? '' ) ), + 'data_machine_code' => $this->clean_path( (string) ( $input['data_machine_code_path'] ?? ( defined( 'DATAMACHINE_CODE_PATH' ) ? DATAMACHINE_CODE_PATH : '' ) ) ), + 'openai_provider' => $this->clean_path( (string) ( $input['openai_provider_path'] ?? '' ) ), + ); + + foreach ( $paths as $key => $path ) { + if ( '' === $path || ! is_dir( $path ) ) { + return new \WP_Error( 'datamachine_code_probe_path_missing', sprintf( 'Sandbox Runtime probe path %s is missing or not a directory.', $key ), array( 'status' => 400 ) ); + } + } + + $artifacts = $this->clean_path( (string) ( $input['artifacts_path'] ?? '' ) ); + if ( '' === $artifacts ) { + $artifacts = rtrim( sys_get_temp_dir(), DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR . 'datamachine-code-sandbox-runtime-' . $this->generate_run_id(); + } + + $wp_version = trim( (string) ( $input['wp'] ?? 'trunk' ) ); + if ( '' === $wp_version ) { + $wp_version = 'trunk'; + } + + $bin = trim( (string) ( $input['sandbox_runtime_bin'] ?? 'sandbox-runtime' ) ); + if ( '' === $bin || ! preg_match( '#^[A-Za-z0-9_./:@+-]+$#', $bin ) ) { + return new \WP_Error( 'datamachine_code_probe_bin_invalid', 'sandbox_runtime_bin must be a command name or path without shell metacharacters.', array( 'status' => 400 ) ); + } + $command_prefix = $this->command_prefix( $bin ); + + $command = sprintf( + '%s agent-runtime-probe --agents-api %s --data-machine %s --data-machine-code %s --openai-provider %s --wp %s --artifacts %s --json', + $command_prefix, + escapeshellarg( $paths['agents_api'] ), + escapeshellarg( $paths['data_machine'] ), + escapeshellarg( $paths['data_machine_code'] ), + escapeshellarg( $paths['openai_provider'] ), + escapeshellarg( $wp_version ), + escapeshellarg( $artifacts ) + ); + + $result = $this->run_command( $command ); + $exit_code = (int) ( $result['exit_code'] ?? 1 ); + $output = (string) ( $result['output'] ?? '' ); + $decoded = $this->decode_json_output( $output ); + + if ( is_wp_error( $decoded ) ) { + return new \WP_Error( + 'datamachine_code_probe_json_invalid', + 'Sandbox Runtime probe did not return valid JSON: ' . $decoded->get_error_message(), + array( + 'status' => 500, + 'exit_code' => $exit_code, + 'output' => $this->bound_output( $output ), + ) + ); + } + + if ( 0 !== $exit_code ) { + return new \WP_Error( + 'datamachine_code_probe_failed', + 'Sandbox Runtime probe failed.', + array( + 'status' => 500, + 'exit_code' => $exit_code, + 'output' => $this->bound_output( $output ), + 'probe' => $decoded, + ) + ); + } + + return array( + 'success' => true, + 'schema' => self::SCHEMA, + 'command' => $command, + 'wp' => $wp_version, + 'paths' => $paths, + 'artifacts' => $artifacts, + 'exit_code' => $exit_code, + 'probe' => $decoded, + ); + } + + private function shell_available(): bool { + if ( isset( $this->callbacks['shell_available'] ) ) { + return (bool) ( $this->callbacks['shell_available'] )(); + } + + return Environment::has_shell(); + } + + private function clean_path( string $path ): string { + return rtrim( trim( $path ), DIRECTORY_SEPARATOR ); + } + + private function command_prefix( string $bin ): string { + if ( str_ends_with( $bin, '.js' ) && is_file( $bin ) ) { + return 'node ' . escapeshellarg( $bin ); + } + + return escapeshellarg( $bin ); + } + + private function generate_run_id(): string { + if ( function_exists( 'wp_generate_uuid4' ) ) { + return \wp_generate_uuid4(); + } + + return bin2hex( random_bytes( 16 ) ); + } + + /** @return array|\WP_Error */ + private function decode_json_output( string $output ): array|\WP_Error { + $trimmed = trim( $output ); + if ( '' === $trimmed ) { + return new \WP_Error( 'empty_output', 'Empty output.' ); + } + + $decoded = json_decode( $trimmed, true ); + if ( is_array( $decoded ) ) { + return $decoded; + } + + $offset = strrpos( $trimmed, "\n{" ); + if ( false !== $offset ) { + $decoded = json_decode( substr( $trimmed, $offset + 1 ), true ); + if ( is_array( $decoded ) ) { + return $decoded; + } + } + + return new \WP_Error( 'json_decode_failed', json_last_error_msg() ); + } + + /** @return array{exit_code:int,output:string} */ + private function run_command( string $command ): array { + if ( isset( $this->callbacks['command_runner'] ) ) { + return ( $this->callbacks['command_runner'] )( $command ); + } + + $output = array(); + $exit = 0; + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Required host-side Sandbox Runtime execution primitive. + exec( $command . ' 2>&1', $output, $exit ); + + return array( + 'exit_code' => $exit, + 'output' => implode( "\n", $output ), + ); + } + + private function bound_output( string $output ): string { + if ( strlen( $output ) <= 4000 ) { + return $output; + } + + return substr( $output, 0, 4000 ); + } +} diff --git a/tests/smoke-sandbox-runtime-agent-probe-runner.php b/tests/smoke-sandbox-runtime-agent-probe-runner.php new file mode 100644 index 0000000..e209341 --- /dev/null +++ b/tests/smoke-sandbox-runtime-agent-probe-runner.php @@ -0,0 +1,163 @@ +code = $code; + $this->message = $message; + $this->data = $data; + } + + public function get_error_code(): string { return $this->code; } + public function get_error_message(): string { return $this->message; } + public function get_error_data(): array { return $this->data; } + } + } + + if ( ! function_exists( 'is_wp_error' ) ) { + function is_wp_error( $thing ): bool { return $thing instanceof WP_Error; } + } + + require __DIR__ . '/../inc/Runtime/SandboxRuntimeAgentProbeRunner.php'; + + use DataMachineCode\Runtime\SandboxRuntimeAgentProbeRunner; + + $root = sys_get_temp_dir() . '/dmc-sandbox-runtime-probe-' . getmypid(); + foreach ( array( 'agents-api', 'data-machine', 'data-machine-code', 'ai-provider-for-openai', 'artifacts' ) as $dir ) { + mkdir( $root . '/' . $dir, 0777, true ); + } + file_put_contents( $root . '/sandbox-runtime.js', "#!/usr/bin/env node\n" ); + + $failures = 0; + $total = 0; + $assert = function ( bool $condition, string $message ) use ( &$failures, &$total ): void { + ++$total; + if ( $condition ) { + echo " ok {$message}\n"; + return; + } + + ++$failures; + echo " fail {$message}\n"; + }; + + echo "Sandbox Runtime agent probe runner - smoke\n\n"; + + $captured_command = ''; + $runner = new SandboxRuntimeAgentProbeRunner( + array( + 'shell_available' => fn() => true, + 'command_runner' => function ( string $command ) use ( &$captured_command ): array { + $captured_command = $command; + return array( + 'exit_code' => 0, + 'output' => "Preparing runtime\n" . json_encode( + array( + 'success' => true, + 'runtime' => array( 'backend' => 'wordpress-playground' ), + 'execution' => array( 'command' => 'agent-runtime-probe' ), + 'artifacts' => array( 'manifest' => 'run.json' ), + ) + ), + ); + }, + ) + ); + + $result = $runner->run( + array( + 'agents_api_path' => $root . '/agents-api', + 'data_machine_path' => $root . '/data-machine', + 'data_machine_code_path' => $root . '/data-machine-code', + 'openai_provider_path' => $root . '/ai-provider-for-openai', + 'artifacts_path' => $root . '/artifacts', + 'sandbox_runtime_bin' => 'sandbox-runtime', + 'wp' => 'trunk', + ) + ); + + $assert( ! is_wp_error( $result ), 'happy path returns a result array' ); + $assert( true === ( $result['success'] ?? false ), 'success flag is included' ); + $assert( 'data-machine-code/sandbox-runtime-agent-probe/v1' === ( $result['schema'] ?? '' ), 'schema is pinned' ); + $assert( 'trunk' === ( $result['wp'] ?? '' ), 'WordPress version is carried through' ); + $assert( $root . '/artifacts' === ( $result['artifacts'] ?? '' ), 'artifact path is carried through' ); + $assert( true === ( $result['probe']['success'] ?? false ), 'CLI JSON output is decoded' ); + $assert( 'wordpress-playground' === ( $result['probe']['runtime']['backend'] ?? '' ), 'probe runtime metadata is preserved' ); + $assert( str_contains( $captured_command, 'agent-runtime-probe' ), 'command invokes agent-runtime-probe' ); + $assert( str_contains( $captured_command, '--agents-api' ), 'command includes Agents API mount path' ); + $assert( str_contains( $captured_command, '--json' ), 'command requests JSON output' ); + + $runner->run( + array( + 'agents_api_path' => $root . '/agents-api', + 'data_machine_path' => $root . '/data-machine', + 'data_machine_code_path' => $root . '/data-machine-code', + 'openai_provider_path' => $root . '/ai-provider-for-openai', + 'sandbox_runtime_bin' => $root . '/sandbox-runtime.js', + ) + ); + $assert( str_contains( $captured_command, 'node ' ), 'JS CLI path is run through node' ); + + $missing = $runner->run( + array( + 'agents_api_path' => $root . '/missing', + 'data_machine_path' => $root . '/data-machine', + 'data_machine_code_path' => $root . '/data-machine-code', + 'openai_provider_path' => $root . '/ai-provider-for-openai', + ) + ); + $assert( is_wp_error( $missing ), 'missing plugin path fails closed' ); + $assert( is_wp_error( $missing ) && 'datamachine_code_probe_path_missing' === $missing->get_error_code(), 'missing path error code is explicit' ); + + $invalid_bin = $runner->run( + array( + 'agents_api_path' => $root . '/agents-api', + 'data_machine_path' => $root . '/data-machine', + 'data_machine_code_path' => $root . '/data-machine-code', + 'openai_provider_path' => $root . '/ai-provider-for-openai', + 'sandbox_runtime_bin' => 'sandbox-runtime; rm -rf /', + ) + ); + $assert( is_wp_error( $invalid_bin ), 'invalid binary fails closed' ); + $assert( is_wp_error( $invalid_bin ) && 'datamachine_code_probe_bin_invalid' === $invalid_bin->get_error_code(), 'invalid binary error code is explicit' ); + + $bad_json = ( new SandboxRuntimeAgentProbeRunner( + array( + 'shell_available' => fn() => true, + 'command_runner' => fn() => array( 'exit_code' => 0, 'output' => 'not-json' ), + ) + ) )->run( + array( + 'agents_api_path' => $root . '/agents-api', + 'data_machine_path' => $root . '/data-machine', + 'data_machine_code_path' => $root . '/data-machine-code', + 'openai_provider_path' => $root . '/ai-provider-for-openai', + ) + ); + $assert( is_wp_error( $bad_json ), 'invalid JSON fails closed' ); + $assert( is_wp_error( $bad_json ) && 'datamachine_code_probe_json_invalid' === $bad_json->get_error_code(), 'invalid JSON error code is explicit' ); + + if ( $failures > 0 ) { + echo "\nFAIL: {$failures}/{$total} assertion(s) failed\n"; + exit( 1 ); + } + + echo "\nOK ({$total} assertions)\n"; + exit( 0 ); +} From 56ac8d5ab8ae7fadaba1e834b173089c4b776ff1 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 17 May 2026 21:46:39 -0400 Subject: [PATCH 2/2] feat: run agent sandbox tasks --- inc/Abilities/WordPressRuntimeAbilities.php | 31 +++++-- ...p => SandboxRuntimeAgentSandboxRunner.php} | 47 +++++++--- ...-sandbox-runtime-agent-sandbox-runner.php} | 86 ++++++++++++------- 3 files changed, 108 insertions(+), 56 deletions(-) rename inc/Runtime/{SandboxRuntimeAgentProbeRunner.php => SandboxRuntimeAgentSandboxRunner.php} (73%) rename tests/{smoke-sandbox-runtime-agent-probe-runner.php => smoke-sandbox-runtime-agent-sandbox-runner.php} (51%) diff --git a/inc/Abilities/WordPressRuntimeAbilities.php b/inc/Abilities/WordPressRuntimeAbilities.php index a4d3e14..185db4a 100644 --- a/inc/Abilities/WordPressRuntimeAbilities.php +++ b/inc/Abilities/WordPressRuntimeAbilities.php @@ -8,7 +8,7 @@ namespace DataMachineCode\Abilities; use DataMachine\Abilities\PermissionHelper; -use DataMachineCode\Runtime\SandboxRuntimeAgentProbeRunner; +use DataMachineCode\Runtime\SandboxRuntimeAgentSandboxRunner; use DataMachineCode\Runtime\WordPressRuntimeInspector; defined( 'ABSPATH' ) || exit; @@ -148,15 +148,19 @@ private function registerAbilities(): void { ); wp_register_ability( - 'datamachine-code/sandbox-runtime-agent-probe', + 'datamachine-code/run-agent-sandbox', array( - 'label' => 'Probe Sandbox Runtime Agent Stack', - 'description' => 'Run a WordPress Playground sandbox through the Sandbox Runtime CLI and verify the Agents API, Data Machine, Data Machine Code, and OpenAI provider stack activates cleanly.', + 'label' => 'Run Agent Sandbox', + 'description' => 'Run a task inside an isolated WordPress Playground sandbox with Agents API, Data Machine, Data Machine Code, and the OpenAI provider mounted.', 'category' => 'datamachine-code-runtime', 'input_schema' => array( 'type' => 'object', - 'required' => array( 'agents_api_path', 'data_machine_path', 'openai_provider_path' ), + 'required' => array( 'task', 'agents_api_path', 'data_machine_path', 'openai_provider_path' ), 'properties' => array( + 'task' => array( + 'type' => 'string', + 'description' => 'Task description to run inside the isolated sandbox.', + ), 'agents_api_path' => array( 'type' => 'string', 'description' => 'Local checkout path for the Agents API plugin.', @@ -185,6 +189,14 @@ private function registerAbilities(): void { 'type' => 'string', 'description' => 'Directory where Sandbox Runtime should write artifact bundles.', ), + 'code' => array( + 'type' => 'string', + 'description' => 'Optional PHP code body to execute inside the sandbox after the agent stack boots.', + ), + 'code_file' => array( + 'type' => 'string', + 'description' => 'Optional PHP file to execute inside the sandbox after the agent stack boots.', + ), ), ), 'output_schema' => array( @@ -193,14 +205,15 @@ private function registerAbilities(): void { 'success' => array( 'type' => 'boolean' ), 'schema' => array( 'type' => 'string' ), 'command' => array( 'type' => 'string' ), + 'task' => array( 'type' => 'string' ), 'wp' => array( 'type' => 'string' ), 'paths' => array( 'type' => 'object' ), 'artifacts' => array( 'type' => 'string' ), 'exit_code' => array( 'type' => 'integer' ), - 'probe' => array( 'type' => 'object' ), + 'run' => array( 'type' => 'object' ), ), ), - 'execute_callback' => array( self::class, 'sandboxRuntimeAgentProbe' ), + 'execute_callback' => array( self::class, 'runAgentSandbox' ), 'permission_callback' => fn() => PermissionHelper::can_manage(), 'meta' => array( 'show_in_rest' => true ), ) @@ -231,7 +244,7 @@ public static function read( array $input ): array|\WP_Error { } /** @param array $input Input parameters. @return array|\WP_Error */ - public static function sandboxRuntimeAgentProbe( array $input ): array|\WP_Error { - return ( new SandboxRuntimeAgentProbeRunner() )->run( $input ); + public static function runAgentSandbox( array $input ): array|\WP_Error { + return ( new SandboxRuntimeAgentSandboxRunner() )->run( $input ); } } diff --git a/inc/Runtime/SandboxRuntimeAgentProbeRunner.php b/inc/Runtime/SandboxRuntimeAgentSandboxRunner.php similarity index 73% rename from inc/Runtime/SandboxRuntimeAgentProbeRunner.php rename to inc/Runtime/SandboxRuntimeAgentSandboxRunner.php index 0eb72e2..e1873fe 100644 --- a/inc/Runtime/SandboxRuntimeAgentProbeRunner.php +++ b/inc/Runtime/SandboxRuntimeAgentSandboxRunner.php @@ -1,6 +1,6 @@ */ private array $callbacks; @@ -26,9 +26,9 @@ public function __construct( array $callbacks = array() ) { } /** - * Run the Sandbox Runtime WordPress agent stack probe. + * Run a task inside an isolated Sandbox Runtime WordPress agent stack. * - * @param array $input Probe input. + * @param array $input Sandbox run input. * @return array|\WP_Error */ public function run( array $input ): array|\WP_Error { @@ -45,10 +45,21 @@ public function run( array $input ): array|\WP_Error { foreach ( $paths as $key => $path ) { if ( '' === $path || ! is_dir( $path ) ) { - return new \WP_Error( 'datamachine_code_probe_path_missing', sprintf( 'Sandbox Runtime probe path %s is missing or not a directory.', $key ), array( 'status' => 400 ) ); + return new \WP_Error( 'datamachine_code_sandbox_path_missing', sprintf( 'Sandbox Runtime path %s is missing or not a directory.', $key ), array( 'status' => 400 ) ); } } + $task = trim( (string) ( $input['task'] ?? '' ) ); + if ( '' === $task ) { + return new \WP_Error( 'datamachine_code_sandbox_task_missing', 'task is required for Sandbox Runtime agent sandbox runs.', array( 'status' => 400 ) ); + } + + $code = trim( (string) ( $input['code'] ?? '' ) ); + $code_file = $this->clean_path( (string) ( $input['code_file'] ?? '' ) ); + if ( '' !== $code && '' !== $code_file ) { + return new \WP_Error( 'datamachine_code_sandbox_code_conflict', 'Use either code or code_file, not both.', array( 'status' => 400 ) ); + } + $artifacts = $this->clean_path( (string) ( $input['artifacts_path'] ?? '' ) ); if ( '' === $artifacts ) { $artifacts = rtrim( sys_get_temp_dir(), DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR . 'datamachine-code-sandbox-runtime-' . $this->generate_run_id(); @@ -61,20 +72,27 @@ public function run( array $input ): array|\WP_Error { $bin = trim( (string) ( $input['sandbox_runtime_bin'] ?? 'sandbox-runtime' ) ); if ( '' === $bin || ! preg_match( '#^[A-Za-z0-9_./:@+-]+$#', $bin ) ) { - return new \WP_Error( 'datamachine_code_probe_bin_invalid', 'sandbox_runtime_bin must be a command name or path without shell metacharacters.', array( 'status' => 400 ) ); + return new \WP_Error( 'datamachine_code_sandbox_bin_invalid', 'sandbox_runtime_bin must be a command name or path without shell metacharacters.', array( 'status' => 400 ) ); } $command_prefix = $this->command_prefix( $bin ); $command = sprintf( - '%s agent-runtime-probe --agents-api %s --data-machine %s --data-machine-code %s --openai-provider %s --wp %s --artifacts %s --json', + '%s agent-sandbox-run --agents-api %s --data-machine %s --data-machine-code %s --openai-provider %s --task %s --wp %s --artifacts %s --json', $command_prefix, escapeshellarg( $paths['agents_api'] ), escapeshellarg( $paths['data_machine'] ), escapeshellarg( $paths['data_machine_code'] ), escapeshellarg( $paths['openai_provider'] ), + escapeshellarg( $task ), escapeshellarg( $wp_version ), escapeshellarg( $artifacts ) ); + if ( '' !== $code ) { + $command .= ' --code ' . escapeshellarg( $code ); + } + if ( '' !== $code_file ) { + $command .= ' --code-file ' . escapeshellarg( $code_file ); + } $result = $this->run_command( $command ); $exit_code = (int) ( $result['exit_code'] ?? 1 ); @@ -83,8 +101,8 @@ public function run( array $input ): array|\WP_Error { if ( is_wp_error( $decoded ) ) { return new \WP_Error( - 'datamachine_code_probe_json_invalid', - 'Sandbox Runtime probe did not return valid JSON: ' . $decoded->get_error_message(), + 'datamachine_code_sandbox_json_invalid', + 'Sandbox Runtime did not return valid JSON: ' . $decoded->get_error_message(), array( 'status' => 500, 'exit_code' => $exit_code, @@ -95,13 +113,13 @@ public function run( array $input ): array|\WP_Error { if ( 0 !== $exit_code ) { return new \WP_Error( - 'datamachine_code_probe_failed', - 'Sandbox Runtime probe failed.', + 'datamachine_code_sandbox_failed', + 'Sandbox Runtime agent sandbox run failed.', array( 'status' => 500, 'exit_code' => $exit_code, 'output' => $this->bound_output( $output ), - 'probe' => $decoded, + 'run' => $decoded, ) ); } @@ -110,11 +128,12 @@ public function run( array $input ): array|\WP_Error { 'success' => true, 'schema' => self::SCHEMA, 'command' => $command, + 'task' => $task, 'wp' => $wp_version, 'paths' => $paths, 'artifacts' => $artifacts, 'exit_code' => $exit_code, - 'probe' => $decoded, + 'run' => $decoded, ); } diff --git a/tests/smoke-sandbox-runtime-agent-probe-runner.php b/tests/smoke-sandbox-runtime-agent-sandbox-runner.php similarity index 51% rename from tests/smoke-sandbox-runtime-agent-probe-runner.php rename to tests/smoke-sandbox-runtime-agent-sandbox-runner.php index e209341..59f83ae 100644 --- a/tests/smoke-sandbox-runtime-agent-probe-runner.php +++ b/tests/smoke-sandbox-runtime-agent-sandbox-runner.php @@ -1,15 +1,15 @@ data; } function is_wp_error( $thing ): bool { return $thing instanceof WP_Error; } } - require __DIR__ . '/../inc/Runtime/SandboxRuntimeAgentProbeRunner.php'; + require __DIR__ . '/../inc/Runtime/SandboxRuntimeAgentSandboxRunner.php'; - use DataMachineCode\Runtime\SandboxRuntimeAgentProbeRunner; + use DataMachineCode\Runtime\SandboxRuntimeAgentSandboxRunner; - $root = sys_get_temp_dir() . '/dmc-sandbox-runtime-probe-' . getmypid(); + $root = sys_get_temp_dir() . '/dmc-sandbox-runtime-agent-sandbox-' . getmypid(); foreach ( array( 'agents-api', 'data-machine', 'data-machine-code', 'ai-provider-for-openai', 'artifacts' ) as $dir ) { mkdir( $root . '/' . $dir, 0777, true ); } @@ -57,10 +57,10 @@ function is_wp_error( $thing ): bool { return $thing instanceof WP_Error; } echo " fail {$message}\n"; }; - echo "Sandbox Runtime agent probe runner - smoke\n\n"; + echo "Sandbox Runtime agent sandbox runner - smoke\n\n"; $captured_command = ''; - $runner = new SandboxRuntimeAgentProbeRunner( + $runner = new SandboxRuntimeAgentSandboxRunner( array( 'shell_available' => fn() => true, 'command_runner' => function ( string $command ) use ( &$captured_command ): array { @@ -71,7 +71,7 @@ function is_wp_error( $thing ): bool { return $thing instanceof WP_Error; } array( 'success' => true, 'runtime' => array( 'backend' => 'wordpress-playground' ), - 'execution' => array( 'command' => 'agent-runtime-probe' ), + 'execution' => array( 'command' => 'agent-sandbox-run' ), 'artifacts' => array( 'manifest' => 'run.json' ), ) ), @@ -82,29 +82,35 @@ function is_wp_error( $thing ): bool { return $thing instanceof WP_Error; } $result = $runner->run( array( - 'agents_api_path' => $root . '/agents-api', - 'data_machine_path' => $root . '/data-machine', + 'task' => 'Run an isolated coding sandbox task.', + 'agents_api_path' => $root . '/agents-api', + 'data_machine_path' => $root . '/data-machine', 'data_machine_code_path' => $root . '/data-machine-code', - 'openai_provider_path' => $root . '/ai-provider-for-openai', - 'artifacts_path' => $root . '/artifacts', - 'sandbox_runtime_bin' => 'sandbox-runtime', - 'wp' => 'trunk', + 'openai_provider_path' => $root . '/ai-provider-for-openai', + 'artifacts_path' => $root . '/artifacts', + 'sandbox_runtime_bin' => 'sandbox-runtime', + 'wp' => 'trunk', + 'code' => 'echo "sandbox ok";', ) ); $assert( ! is_wp_error( $result ), 'happy path returns a result array' ); $assert( true === ( $result['success'] ?? false ), 'success flag is included' ); - $assert( 'data-machine-code/sandbox-runtime-agent-probe/v1' === ( $result['schema'] ?? '' ), 'schema is pinned' ); + $assert( 'data-machine-code/run-agent-sandbox/v1' === ( $result['schema'] ?? '' ), 'schema is pinned' ); + $assert( 'Run an isolated coding sandbox task.' === ( $result['task'] ?? '' ), 'task is carried through' ); $assert( 'trunk' === ( $result['wp'] ?? '' ), 'WordPress version is carried through' ); $assert( $root . '/artifacts' === ( $result['artifacts'] ?? '' ), 'artifact path is carried through' ); - $assert( true === ( $result['probe']['success'] ?? false ), 'CLI JSON output is decoded' ); - $assert( 'wordpress-playground' === ( $result['probe']['runtime']['backend'] ?? '' ), 'probe runtime metadata is preserved' ); - $assert( str_contains( $captured_command, 'agent-runtime-probe' ), 'command invokes agent-runtime-probe' ); + $assert( true === ( $result['run']['success'] ?? false ), 'CLI JSON output is decoded' ); + $assert( 'wordpress-playground' === ( $result['run']['runtime']['backend'] ?? '' ), 'sandbox runtime metadata is preserved' ); + $assert( str_contains( $captured_command, 'agent-sandbox-run' ), 'command invokes agent-sandbox-run' ); + $assert( str_contains( $captured_command, '--task' ), 'command includes task text' ); + $assert( str_contains( $captured_command, '--code' ), 'command includes optional task code' ); $assert( str_contains( $captured_command, '--agents-api' ), 'command includes Agents API mount path' ); $assert( str_contains( $captured_command, '--json' ), 'command requests JSON output' ); $runner->run( array( + 'task' => 'Run with JS CLI.', 'agents_api_path' => $root . '/agents-api', 'data_machine_path' => $root . '/data-machine', 'data_machine_code_path' => $root . '/data-machine-code', @@ -116,42 +122,56 @@ function is_wp_error( $thing ): bool { return $thing instanceof WP_Error; } $missing = $runner->run( array( - 'agents_api_path' => $root . '/missing', - 'data_machine_path' => $root . '/data-machine', + 'task' => 'Run missing path test.', + 'agents_api_path' => $root . '/missing', + 'data_machine_path' => $root . '/data-machine', 'data_machine_code_path' => $root . '/data-machine-code', - 'openai_provider_path' => $root . '/ai-provider-for-openai', + 'openai_provider_path' => $root . '/ai-provider-for-openai', ) ); $assert( is_wp_error( $missing ), 'missing plugin path fails closed' ); - $assert( is_wp_error( $missing ) && 'datamachine_code_probe_path_missing' === $missing->get_error_code(), 'missing path error code is explicit' ); + $assert( is_wp_error( $missing ) && 'datamachine_code_sandbox_path_missing' === $missing->get_error_code(), 'missing path error code is explicit' ); + + $missing_task = $runner->run( + array( + 'agents_api_path' => $root . '/agents-api', + 'data_machine_path' => $root . '/data-machine', + 'data_machine_code_path' => $root . '/data-machine-code', + 'openai_provider_path' => $root . '/ai-provider-for-openai', + ) + ); + $assert( is_wp_error( $missing_task ), 'missing task fails closed' ); + $assert( is_wp_error( $missing_task ) && 'datamachine_code_sandbox_task_missing' === $missing_task->get_error_code(), 'missing task error code is explicit' ); $invalid_bin = $runner->run( array( - 'agents_api_path' => $root . '/agents-api', - 'data_machine_path' => $root . '/data-machine', + 'task' => 'Run invalid binary test.', + 'agents_api_path' => $root . '/agents-api', + 'data_machine_path' => $root . '/data-machine', 'data_machine_code_path' => $root . '/data-machine-code', - 'openai_provider_path' => $root . '/ai-provider-for-openai', - 'sandbox_runtime_bin' => 'sandbox-runtime; rm -rf /', + 'openai_provider_path' => $root . '/ai-provider-for-openai', + 'sandbox_runtime_bin' => 'sandbox-runtime; rm -rf /', ) ); $assert( is_wp_error( $invalid_bin ), 'invalid binary fails closed' ); - $assert( is_wp_error( $invalid_bin ) && 'datamachine_code_probe_bin_invalid' === $invalid_bin->get_error_code(), 'invalid binary error code is explicit' ); + $assert( is_wp_error( $invalid_bin ) && 'datamachine_code_sandbox_bin_invalid' === $invalid_bin->get_error_code(), 'invalid binary error code is explicit' ); - $bad_json = ( new SandboxRuntimeAgentProbeRunner( + $bad_json = ( new SandboxRuntimeAgentSandboxRunner( array( 'shell_available' => fn() => true, 'command_runner' => fn() => array( 'exit_code' => 0, 'output' => 'not-json' ), ) ) )->run( array( - 'agents_api_path' => $root . '/agents-api', - 'data_machine_path' => $root . '/data-machine', + 'task' => 'Run invalid JSON test.', + 'agents_api_path' => $root . '/agents-api', + 'data_machine_path' => $root . '/data-machine', 'data_machine_code_path' => $root . '/data-machine-code', - 'openai_provider_path' => $root . '/ai-provider-for-openai', + 'openai_provider_path' => $root . '/ai-provider-for-openai', ) ); $assert( is_wp_error( $bad_json ), 'invalid JSON fails closed' ); - $assert( is_wp_error( $bad_json ) && 'datamachine_code_probe_json_invalid' === $bad_json->get_error_code(), 'invalid JSON error code is explicit' ); + $assert( is_wp_error( $bad_json ) && 'datamachine_code_sandbox_json_invalid' === $bad_json->get_error_code(), 'invalid JSON error code is explicit' ); if ( $failures > 0 ) { echo "\nFAIL: {$failures}/{$total} assertion(s) failed\n";