From 98f01dd15a43c71748819d9f389b5f663033a37e Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 1 Jun 2026 20:23:08 -0400 Subject: [PATCH] fix: add inherited browser agent invocation --- .../assets/browser-runtime.js | 7 +- .../src/class-wp-codebox-abilities.php | 365 +++++++++++++++++- scripts/browser-runtime-operation-smoke.ts | 18 + tests/smoke-wordpress-plugin.php | 72 +++- 4 files changed, 448 insertions(+), 14 deletions(-) diff --git a/packages/wordpress-plugin/assets/browser-runtime.js b/packages/wordpress-plugin/assets/browser-runtime.js index 78011283..69d91b98 100644 --- a/packages/wordpress-plugin/assets/browser-runtime.js +++ b/packages/wordpress-plugin/assets/browser-runtime.js @@ -373,7 +373,12 @@ try { throw new Error( 'WP Codebox browser recipe missing.' ); } - await client.writeFile( taskPath, JSON.stringify( taskPayload ) ); + const payload = taskPayload && typeof taskPayload === 'object' ? taskPayload : recipe?.browser?.task_payload; + if ( ! payload || typeof payload !== 'object' ) { + throw new Error( 'WP Codebox browser recipe task payload missing.' ); + } + + await client.writeFile( taskPath, JSON.stringify( payload ) ); let lastResult = null; for ( const step of steps ) { diff --git a/packages/wordpress-plugin/src/class-wp-codebox-abilities.php b/packages/wordpress-plugin/src/class-wp-codebox-abilities.php index 30fb7ca1..cfa1db16 100644 --- a/packages/wordpress-plugin/src/class-wp-codebox-abilities.php +++ b/packages/wordpress-plugin/src/class-wp-codebox-abilities.php @@ -311,6 +311,22 @@ private function register(): void { 'expected_artifacts' => $task_input_schema['properties']['expected_artifacts'], 'policy' => $task_input_schema['properties']['policy'], 'context' => $task_input_schema['properties']['context'], + 'agent' => array( + 'type' => 'string', + 'description' => 'Sandbox agent slug to invoke through agents/chat inside the browser Playground.', + ), + 'provider' => array( + 'type' => 'string', + 'description' => 'AI provider id to seed into the browser Playground agent invocation.', + ), + 'model' => array( + 'type' => 'string', + 'description' => 'AI model id to seed into the browser Playground agent invocation.', + ), + 'mode' => array( + 'type' => 'string', + 'description' => 'Agent execution mode. Defaults to sandbox.', + ), 'provider_plugin_paths' => array( 'type' => 'array', 'description' => 'AI provider plugin directories the browser sandbox should have available before code execution.', @@ -884,6 +900,7 @@ private static function browser_playground_session_schema(): array { ), 'session' => array( 'type' => 'object' ), 'task_input' => self::task_input_schema(), + 'task_payload' => array( 'type' => 'object' ), 'playground' => array( 'type' => 'object' ), 'runtime' => array( 'type' => 'object' ), 'site_blueprint_artifact' => array( 'type' => 'object' ), @@ -930,6 +947,11 @@ public static function create_browser_playground_session( array $input ): array| return $playground; } + $inheritance_payload = self::browser_inheritance_resolution_payload( $input ); + if ( is_wp_error( $inheritance_payload ) ) { + return $inheritance_payload; + } + $input = self::browser_input_with_inheritance( $input, $inheritance_payload['inheritance'] ); $browser_runner = is_array( $input['browser_runner'] ?? null ) ? $input['browser_runner'] : array(); $legacy_plugins = self::browser_plugins( $input ); if ( is_wp_error( $legacy_plugins ) ) { @@ -957,7 +979,8 @@ public static function create_browser_playground_session( array $input ): array| return self::blocked_browser_playground_session( $session_id, $input, $task_input, $ready_to_code, $browser_plugins, $runtime, $artifacts, $playground, $blueprint, $site_blueprint_artifact ); } - $recipe = self::browser_agent_recipe( $task_input, $session_id, $browser_runner, $blueprint, $playground ); + $task_payload = self::browser_task_payload( $input, $task_input, $session_id, $artifacts, $inheritance_payload['inheritance'] ); + $recipe = self::browser_agent_recipe( $task_input, $session_id, $browser_runner, $blueprint, $playground, $task_payload ); if ( is_wp_error( $recipe ) ) { return $recipe; } @@ -972,7 +995,11 @@ public static function create_browser_playground_session( array $input ): array| 'session' => self::browser_session_envelope( $session_id, 'ready', $input ), 'task' => (string) $task_input['goal'], 'task_input' => $task_input, + 'task_payload' => $task_payload, 'agent' => (string) ( $input['agent'] ?? 'wp-codebox-sandbox' ), + 'provider' => self::browser_provider( $input, $inheritance_payload['inheritance'] ), + 'model' => self::browser_model( $input, $inheritance_payload['inheritance'] ), + 'inheritance' => $inheritance_payload['inheritance'], 'plugins' => $browser_plugins, 'runtime' => $runtime, 'site_blueprint_artifact' => $site_blueprint_artifact, @@ -1197,6 +1224,320 @@ private static function normalize_task_input( array $input ): array|WP_Error { return WP_Codebox_Agent_Task::normalize_input( $input, fn( array $tools ): WP_Error|null => ( new WP_Codebox_Agent_Sandbox_Runner() )->validate_allowed_tools( $tools ), true ); } + /** @param array $input Ability input. @return array{connectors:string[],settings:string[]} */ + private static function browser_inheritance_request( array $input ): array { + $inherit = is_array( $input['inherit'] ?? null ) ? $input['inherit'] : array(); + + return array( + 'connectors' => self::string_list( $inherit['connectors'] ?? array() ), + 'settings' => self::string_list( $inherit['settings'] ?? array() ), + ); + } + + /** @param array $input Ability input. @return array{inheritance:array{connectors:array>,settings:array>}}|WP_Error */ + private static function browser_inheritance_resolution_payload( array $input ): array|WP_Error { + $request = self::browser_inheritance_request( $input ); + $resolution = array( + 'connectors' => array_map( + static fn( string $name ): array => array( + 'name' => $name, + 'status' => 'unresolved', + ), + $request['connectors'] + ), + 'settings' => array_map( + static fn( string $name ): array => array( + 'name' => $name, + 'status' => 'unresolved', + ), + $request['settings'] + ), + ); + + if ( function_exists( 'apply_filters' ) && ( ! empty( $request['connectors'] ) || ! empty( $request['settings'] ) ) ) { + $filtered = apply_filters( 'wp_codebox_resolve_inheritance', $resolution, $request, $input ); + if ( is_array( $filtered ) ) { + $resolution = $filtered; + } + } + + $inheritance = array( + 'connectors' => self::sanitize_browser_inheritance_connectors( $resolution['connectors'] ?? array() ), + 'settings' => self::sanitize_browser_inheritance_settings( $resolution['settings'] ?? array() ), + ); + $credential_error = self::browser_connector_credentials_error( $inheritance ); + if ( null !== $credential_error ) { + return $credential_error; + } + + return array( 'inheritance' => $inheritance ); + } + + /** @param array $input Ability input. @param array{connectors:array>,settings:array>} $inheritance @return array */ + private static function browser_input_with_inheritance( array $input, array $inheritance ): array { + $input['provider_plugin_paths'] = array_values( array_unique( array_merge( self::browser_provider_plugin_paths( $input ), self::browser_inheritance_provider_plugin_paths( $inheritance ) ) ) ); + $input['secret_env'] = array_values( array_unique( array_merge( self::browser_secret_env_names( $input ), self::browser_inheritance_secret_env_names( $inheritance ) ) ) ); + + return $input; + } + + /** @param array $input Ability input. @param array $task_input Normalized task input. @param array> $artifacts Browser artifact specs. @param array{connectors:array>,settings:array>} $inheritance @return array */ + private static function browser_task_payload( array $input, array $task_input, string $session_id, array $artifacts, array $inheritance ): array { + return array_filter( + array( + 'schema' => 'wp-codebox/browser-agent-task-payload/v1', + 'agent' => self::browser_agent_slug( $input ), + 'mode' => self::browser_mode( $input ), + 'provider' => self::browser_provider( $input, $inheritance ), + 'model' => self::browser_model( $input, $inheritance ), + 'message' => (string) $task_input['goal'], + 'session_id' => $session_id, + 'task_input' => $task_input, + 'inheritance' => $inheritance, + 'secret_env' => self::browser_secret_env_names( $input ), + 'artifacts' => array( + 'schema' => 'wp-codebox/browser-artifacts/v1', + 'files' => $artifacts, + ), + ), + static fn( mixed $value ): bool => '' !== $value && array() !== $value + ); + } + + /** @param array $input Ability input. */ + private static function browser_agent_slug( array $input ): string { + $agent = trim( (string) ( $input['agent'] ?? '' ) ); + if ( '' === $agent && function_exists( 'apply_filters' ) ) { + $agent = (string) apply_filters( 'wp_codebox_default_agent', '' ); + } + + return '' !== trim( $agent ) ? trim( $agent ) : 'wp-codebox-sandbox'; + } + + /** @param array $input Ability input. */ + private static function browser_mode( array $input ): string { + $mode = trim( (string) ( $input['mode'] ?? '' ) ); + return '' !== $mode ? $mode : 'sandbox'; + } + + /** @param array $input Ability input. @param array{connectors:array>,settings:array>} $inheritance */ + private static function browser_provider( array $input, array $inheritance ): string { + $provider = trim( (string) ( $input['provider'] ?? '' ) ); + if ( '' !== $provider ) { + return $provider; + } + + foreach ( $inheritance['connectors'] as $connector ) { + $provider = trim( (string) ( $connector['provider'] ?? '' ) ); + if ( '' !== $provider ) { + return $provider; + } + } + + return function_exists( 'apply_filters' ) ? trim( (string) apply_filters( 'wp_codebox_default_provider', '' ) ) : ''; + } + + /** @param array $input Ability input. @param array{connectors:array>,settings:array>} $inheritance */ + private static function browser_model( array $input, array $inheritance ): string { + $model = trim( (string) ( $input['model'] ?? '' ) ); + if ( '' !== $model ) { + return $model; + } + + foreach ( $inheritance['connectors'] as $connector ) { + $model = trim( (string) ( $connector['model'] ?? '' ) ); + if ( '' !== $model ) { + return $model; + } + } + + return function_exists( 'apply_filters' ) ? trim( (string) apply_filters( 'wp_codebox_default_model', '' ) ) : ''; + } + + /** @param array $input Ability input. @return string[] */ + private static function browser_provider_plugin_paths( array $input ): array { + return array_values( array_unique( array_filter( array_map( static fn( $path ): string => self::browser_clean_path( (string) $path ), is_array( $input['provider_plugin_paths'] ?? null ) ? $input['provider_plugin_paths'] : array() ), static fn( string $path ): bool => '' !== $path && is_dir( $path ) ) ) ); + } + + /** @param array $input Ability input. @return string[] */ + private static function browser_secret_env_names( array $input ): array { + return array_values( array_unique( array_filter( self::string_list( $input['secret_env'] ?? array() ), static fn( string $name ): bool => 1 === preg_match( '/^[A-Z_][A-Z0-9_]*$/', $name ) ) ) ); + } + + /** @param array{connectors:array>,settings:array>} $inheritance @return string[] */ + private static function browser_inheritance_provider_plugin_paths( array $inheritance ): array { + $paths = array(); + foreach ( $inheritance['connectors'] as $connector ) { + $paths = array_merge( $paths, self::string_list( $connector['providerPluginPaths'] ?? array() ) ); + } + + return array_values( array_unique( $paths ) ); + } + + /** @param array{connectors:array>,settings:array>} $inheritance @return string[] */ + private static function browser_inheritance_secret_env_names( array $inheritance ): array { + $names = array(); + foreach ( $inheritance['connectors'] as $connector ) { + $names = array_merge( $names, self::string_list( $connector['secretEnv'] ?? array() ) ); + $credentials = is_array( $connector['credentials'] ?? null ) ? $connector['credentials'] : array(); + foreach ( is_array( $credentials['secrets'] ?? null ) ? $credentials['secrets'] : array() as $secret ) { + if ( is_array( $secret ) && 'available' === ( $secret['status'] ?? '' ) ) { + $names[] = (string) ( $secret['name'] ?? '' ); + } + } + } + + return array_values( array_unique( array_filter( $names ) ) ); + } + + /** @param array $connectors Inheritance connector rows. @return array> */ + private static function sanitize_browser_inheritance_connectors( array $connectors ): array { + $sanitized = array(); + foreach ( $connectors as $connector ) { + if ( ! is_array( $connector ) ) { + continue; + } + + $name = trim( (string) ( $connector['name'] ?? '' ) ); + if ( '' === $name ) { + continue; + } + + $entry = array( + 'name' => $name, + 'status' => trim( (string) ( $connector['status'] ?? 'resolved' ) ), + ); + foreach ( array( 'provider', 'model' ) as $field ) { + $value = trim( (string) ( $connector[ $field ] ?? '' ) ); + if ( '' !== $value ) { + $entry[ $field ] = $value; + } + } + + $provider_plugin_paths = array_values( array_filter( array_map( static fn( string $path ): string => self::browser_clean_path( $path ), self::string_list( $connector['provider_plugin_paths'] ?? $connector['providerPluginPaths'] ?? array() ) ), static fn( string $path ): bool => '' !== $path && is_dir( $path ) ) ); + if ( ! empty( $provider_plugin_paths ) ) { + $entry['providerPluginPaths'] = array_values( array_unique( $provider_plugin_paths ) ); + } + + $secret_env = array_values( array_filter( self::string_list( $connector['secret_env'] ?? $connector['secretEnv'] ?? array() ), static fn( string $name ): bool => 1 === preg_match( '/^[A-Z_][A-Z0-9_]*$/', $name ) ) ); + if ( ! empty( $secret_env ) ) { + $entry['secretEnv'] = array_values( array_unique( $secret_env ) ); + } + + $credentials = self::sanitize_browser_connector_credentials( $connector['credentials'] ?? null, $name ); + if ( ! empty( $credentials ) ) { + $entry['credentials'] = $credentials; + } + + $sanitized[] = $entry; + } + + return $sanitized; + } + + /** @return array */ + private static function sanitize_browser_connector_credentials( mixed $credentials, string $connector_name ): array { + if ( ! is_array( $credentials ) ) { + return array(); + } + + $status = self::browser_credential_status( (string) ( $credentials['status'] ?? 'missing' ) ); + $entry = array( + 'schema' => 'wp-codebox/connector-credentials/v1', + 'connector' => $connector_name, + 'scope' => 'connector', + 'status' => $status, + 'secrets' => array(), + ); + $reason = self::browser_redacted_reason( $credentials['reason'] ?? '' ); + if ( '' !== $reason ) { + $entry['reason'] = $reason; + } + + foreach ( is_array( $credentials['secrets'] ?? null ) ? $credentials['secrets'] : array() as $secret ) { + if ( ! is_array( $secret ) ) { + continue; + } + + $name = trim( (string) ( $secret['name'] ?? '' ) ); + if ( 1 !== preg_match( '/^[A-Z_][A-Z0-9_]*$/', $name ) ) { + continue; + } + + $secret_entry = array( + 'name' => $name, + 'status' => self::browser_credential_status( (string) ( $secret['status'] ?? $status ) ), + ); + foreach ( array( 'scope', 'source', 'reason' ) as $field ) { + $value = 'reason' === $field ? self::browser_redacted_reason( $secret[ $field ] ?? '' ) : trim( (string) ( $secret[ $field ] ?? '' ) ); + if ( '' !== $value ) { + $secret_entry[ $field ] = $value; + } + } + + $entry['secrets'][] = $secret_entry; + } + + return $entry; + } + + private static function browser_credential_status( string $status ): string { + return in_array( $status, array( 'available', 'missing', 'denied' ), true ) ? $status : 'missing'; + } + + private static function browser_redacted_reason( mixed $reason ): string { + $reason = trim( (string) $reason ); + return '' === $reason ? '' : substr( preg_replace( '/[^A-Za-z0-9 .:_-]/', '', $reason ) ?? '', 0, 160 ); + } + + /** @param array{connectors:array>,settings:array>} $inheritance */ + private static function browser_connector_credentials_error( array $inheritance ): WP_Error|null { + $failures = array(); + foreach ( $inheritance['connectors'] as $connector ) { + $credentials = is_array( $connector['credentials'] ?? null ) ? $connector['credentials'] : array(); + if ( empty( $credentials ) ) { + continue; + } + + $status = (string) ( $credentials['status'] ?? 'missing' ); + $secrets = array_filter( is_array( $credentials['secrets'] ?? null ) ? $credentials['secrets'] : array(), static fn( mixed $secret ): bool => is_array( $secret ) && in_array( (string) ( $secret['status'] ?? '' ), array( 'missing', 'denied' ), true ) ); + if ( in_array( $status, array( 'missing', 'denied' ), true ) || ! empty( $secrets ) ) { + $failures[] = array( + 'name' => (string) ( $connector['name'] ?? '' ), + 'status' => (string) ( $connector['status'] ?? '' ), + 'credentials' => $credentials, + ); + } + } + + return empty( $failures ) ? null : new WP_Error( 'wp_codebox_connector_credentials_unavailable', 'Requested connector credentials are missing or denied for this browser sandbox scope.', array( 'status' => 403, 'schema' => 'wp-codebox/connector-credential-failure/v1', 'connectors' => $failures ) ); + } + + /** @param array $settings Inheritance setting rows. @return array> */ + private static function sanitize_browser_inheritance_settings( array $settings ): array { + $sanitized = array(); + foreach ( $settings as $setting ) { + if ( ! is_array( $setting ) ) { + continue; + } + $name = trim( (string) ( $setting['name'] ?? '' ) ); + if ( '' === $name ) { + continue; + } + $entry = array( + 'name' => $name, + 'status' => trim( (string) ( $setting['status'] ?? 'resolved' ) ), + ); + $scope = trim( (string) ( $setting['scope'] ?? '' ) ); + if ( '' !== $scope ) { + $entry['scope'] = $scope; + } + $sanitized[] = $entry; + } + + return $sanitized; + } + /** @return string[] */ private static function string_list( mixed $value ): array { return WP_Codebox_Agent_Task::string_list( $value ); @@ -1584,7 +1925,7 @@ private static function browser_plugins( array $input ): array|WP_Error { /** @param array $input Ability input. @param array> $legacy_plugins Legacy browser_plugins specs. @return array|WP_Error */ private static function browser_runtime_dependencies( array $input, array $legacy_plugins ): array|WP_Error { $runtime = is_array( $input['runtime'] ?? null ) ? $input['runtime'] : array(); - $runtime_plugin_specs = self::browser_runtime_plugin_specs( is_array( $runtime['plugins'] ?? null ) ? $runtime['plugins'] : array() ); + $runtime_plugin_specs = self::browser_runtime_plugin_specs( array_merge( self::browser_provider_plugin_specs( $input ), is_array( $runtime['plugins'] ?? null ) ? $runtime['plugins'] : array() ) ); if ( is_wp_error( $runtime_plugin_specs ) ) { return $runtime_plugin_specs; } @@ -1633,6 +1974,21 @@ private static function browser_runtime_dependencies( array $input, array $legac ); } + /** @param array $input Ability input. @return array> */ + private static function browser_provider_plugin_specs( array $input ): array { + return array_map( + static fn( string $path ): array => array( + 'slug' => self::safe_key( basename( $path ) ), + 'path' => $path, + 'activate' => true, + 'provenance' => array( + 'source' => 'provider-plugin-path', + ), + ), + self::browser_provider_plugin_paths( $input ) + ); + } + /** @param array $plugins Runtime plugin specs. @return array>|WP_Error */ private static function browser_runtime_plugin_specs( array $plugins ): array|WP_Error { $resolved = array(); @@ -2705,8 +3061,8 @@ private static function browser_blueprint_has_login_step( array $steps ): bool { return false; } - /** @param array $task_input Normalized task input. @param array $runner Runner overrides. @return array|WP_Error */ - private static function browser_agent_recipe( array $task_input, string $session_id, array $runner, array $blueprint, array $playground ): array|WP_Error { + /** @param array $task_input Normalized task input. @param array $runner Runner overrides. @param array $task_payload Browser task payload. @return array|WP_Error */ + private static function browser_agent_recipe( array $task_input, string $session_id, array $runner, array $blueprint, array $playground, array $task_payload ): array|WP_Error { $task_path = (string) ( $runner['task_path'] ?? '/tmp/wp-codebox-agent-task.json' ); $result_path = (string) ( $runner['result_path'] ?? '/tmp/wp-codebox-agent-result.json' ); $invocation = self::browser_runner_invocation( $runner ); @@ -2757,6 +3113,7 @@ private static function browser_agent_recipe( array $task_input, string $session 'execution' => 'php-wasm', 'task_path' => $task_path, 'result_path' => $result_path, + 'task_payload' => $task_payload, 'invocation' => self::browser_runner_invocation_metadata( $invocation ), 'captures' => $captures, ), diff --git a/scripts/browser-runtime-operation-smoke.ts b/scripts/browser-runtime-operation-smoke.ts index 68783329..b3930647 100644 --- a/scripts/browser-runtime-operation-smoke.ts +++ b/scripts/browser-runtime-operation-smoke.ts @@ -130,6 +130,7 @@ const recipeResult = await runtime.runRecipe(recipeClient, { }, { goal: "Smoke test browser recipe marker." }) assert.equal(recipeResult.success, true) assert.equal(recipeClient.files[0]?.path, "/tmp/wp-codebox-agent-task.json") +assert.deepEqual(JSON.parse(recipeClient.files[0]?.contents ?? "{}"), { goal: "Smoke test browser recipe marker." }) assert.match(recipeClient.files[1]?.contents ?? "", /WP_CODEBOX_BROWSER_PLAYGROUND_RUNNER/) assert.match(recipeClient.files[1]?.contents ?? "", /<\?php\ndefine\( 'WP_CODEBOX_BROWSER_PLAYGROUND_RUNNER', true \);/) @@ -168,6 +169,23 @@ await assert.rejects( /not ready/ ) +const embeddedPayloadClient = createClient('{"success":true,"schema":"wp-codebox/browser-agent-run/v1"}') +await runtime.runRecipe(embeddedPayloadClient, { + browser: { + task_path: "/tmp/wp-codebox-agent-task.json", + task_payload: { schema: "wp-codebox/browser-agent-task-payload/v1", goal: "embedded" }, + }, + workflow: { + steps: [ + { + command: "wordpress.run-php", + args: ["code= true ) );"], + }, + ], + }, +}) +assert.deepEqual(JSON.parse(embeddedPayloadClient.files[0]?.contents ?? "{}"), { schema: "wp-codebox/browser-agent-task-payload/v1", goal: "embedded" }) + await assert.rejects( () => runtime.runWordPressOperation(createClient("{}"), { args: {} }), /WordPress browser operation must include a type/ diff --git a/tests/smoke-wordpress-plugin.php b/tests/smoke-wordpress-plugin.php index 8477196b..21d155b9 100644 --- a/tests/smoke-wordpress-plugin.php +++ b/tests/smoke-wordpress-plugin.php @@ -169,6 +169,9 @@ function get_users( array $args ): array { return array( new WP_User( 11, 'Priva foreach ( array( 'agents-api', 'data-machine', 'data-machine-code' ) as $component_slug ) { file_put_contents( $root . '/' . $component_slug . '/' . $component_slug . '.php', ' 'agents-api' === ( $plugin['slug'] ?? '' ) ) ) ); $assert( 'browser Playground session packages required host runtime plugins', ! is_wp_error( $browser_session ) && str_starts_with( (string) ( $browser_session['plugins'][1]['url'] ?? '' ), 'data:application/zip;base64,' ) && str_starts_with( (string) ( $browser_session['plugins'][2]['url'] ?? '' ), 'data:application/zip;base64,' ) && 64 === strlen( (string) ( $browser_session['plugins'][1]['provenance']['sha256'] ?? '' ) ) ); -$assert( 'browser Playground session accepts structured runtime dependencies', ! is_wp_error( $browser_session ) && 'wp-codebox/browser-runtime-dependencies/v1' === ( $browser_session['runtime']['schema'] ?? '' ) && 7 === ( $browser_session['runtime']['summary']['plugins'] ?? 0 ) && 2 === ( $browser_session['runtime']['component_plugins'] ?? 0 ) && 1 === ( $browser_session['runtime']['summary']['mu_plugins'] ?? 0 ) && 1 === ( $browser_session['runtime']['summary']['themes'] ?? 0 ) && 1 === ( $browser_session['runtime']['summary']['bootstrap'] ?? 0 ) ); -$assert( 'browser Playground session server-packages remote runtime plugins after required components', ! is_wp_error( $browser_session ) && 'installPlugin' === ( $browser_session['playground']['blueprint']['steps'][4]['step'] ?? '' ) && str_starts_with( (string) ( $browser_session['playground']['blueprint']['steps'][4]['pluginData']['url'] ?? '' ), 'data:application/zip;base64,' ) && false === ( $browser_session['playground']['blueprint']['steps'][4]['options']['activate'] ?? true ) && 'runtime-plugin-remote-package' === ( $browser_session['plugins'][3]['provenance']['source'] ?? '' ) ); -$assert( 'browser Playground session packages release ZIP runtime plugins without exposing source URLs', ! is_wp_error( $browser_session ) && str_starts_with( (string) ( $browser_session['plugins'][4]['url'] ?? '' ), 'data:application/zip;base64,' ) && ! str_contains( (string) ( $browser_session['playground']['blueprint']['steps'][5]['pluginData']['url'] ?? '' ), 'github.com' ) && 'runtime-plugin-remote-package' === ( $browser_session['plugins'][4]['provenance']['source'] ?? '' ) ); -$assert( 'browser Playground session packages server runtime plugin paths', ! is_wp_error( $browser_session ) && str_starts_with( (string) ( $browser_session['plugins'][5]['url'] ?? '' ), 'data:application/zip;base64,' ) && 64 === strlen( (string) ( $browser_session['plugins'][5]['provenance']['sha256'] ?? '' ) ) && 'runtime-plugin-path' === ( $browser_session['plugins'][5]['provenance']['source'] ?? '' ) ); -$assert( 'browser Playground session compiles git directory runtime plugins', ! is_wp_error( $browser_session ) && 'git:directory' === ( $browser_session['playground']['blueprint']['steps'][7]['pluginData']['resource'] ?? '' ) && 'plugins/example-git-plugin' === ( $browser_session['playground']['blueprint']['steps'][7]['pluginData']['path'] ?? '' ) && 'example-git-plugin' === ( $browser_session['playground']['blueprint']['steps'][7]['options']['targetFolderName'] ?? '' ) ); -$assert( 'browser Playground session compiles caller mu-plugin runtime dependency', ! is_wp_error( $browser_session ) && 'runPHP' === ( $browser_session['playground']['blueprint']['steps'][8]['step'] ?? '' ) && str_contains( (string) ( $browser_session['playground']['blueprint']['steps'][8]['code'] ?? '' ), '/wordpress/wp-content/mu-plugins/caller-runtime.php' ) && str_contains( (string) ( $browser_session['playground']['blueprint']['steps'][8]['code'] ?? '' ), 'caller_runtime_task' ) ); -$assert( 'browser Playground session compiles theme runtime dependency', ! is_wp_error( $browser_session ) && 'runPHP' === ( $browser_session['playground']['blueprint']['steps'][9]['step'] ?? '' ) && str_contains( (string) ( $browser_session['playground']['blueprint']['steps'][9]['code'] ?? '' ), '/wordpress/wp-content/themes/example-starter/style.css' ) && str_contains( (string) ( $browser_session['playground']['blueprint']['steps'][9]['code'] ?? '' ), "require_once '/wordpress/wp-load.php'" ) && str_contains( (string) ( $browser_session['playground']['blueprint']['steps'][9]['code'] ?? '' ), "require_once ABSPATH . WPINC . '/theme.php'" ) && str_contains( (string) ( $browser_session['playground']['blueprint']['steps'][9]['code'] ?? '' ), "switch_theme( 'example-starter' )" ) ); -$assert( 'browser Playground session compiles named bootstrap runtime operation', ! is_wp_error( $browser_session ) && 'runPHP' === ( $browser_session['playground']['blueprint']['steps'][10]['step'] ?? '' ) && str_contains( (string) ( $browser_session['playground']['blueprint']['steps'][10]['code'] ?? '' ), "require_once '/wordpress/wp-load.php'" ) && str_contains( (string) ( $browser_session['playground']['blueprint']['steps'][10]['code'] ?? '' ), "update_option( 'blogname', 'Browser Preview' )" ) ); +$browser_steps = ! is_wp_error( $browser_session ) ? ( $browser_session['playground']['blueprint']['steps'] ?? array() ) : array(); +$browser_plugin_by_slug = static fn( string $slug ): array => array_values( array_filter( $browser_session['plugins'] ?? array(), static fn( array $plugin ): bool => $slug === ( $plugin['slug'] ?? '' ) ) )[0] ?? array(); +$browser_step_with_plugin_slug = static fn( string $slug ): array => array_values( array_filter( $browser_steps, static fn( array $step ): bool => $slug === ( $step['options']['targetFolderName'] ?? $step['pluginData']['targetFolderName'] ?? $step['pluginData']['slug'] ?? '' ) ) )[0] ?? array(); +$browser_step_with_code = static fn( string $needle ): array => array_values( array_filter( $browser_steps, static fn( array $step ): bool => 'runPHP' === ( $step['step'] ?? '' ) && str_contains( (string) ( $step['code'] ?? '' ), $needle ) ) )[0] ?? array(); +$assert( 'browser Playground session accepts structured runtime dependencies', ! is_wp_error( $browser_session ) && 'wp-codebox/browser-runtime-dependencies/v1' === ( $browser_session['runtime']['schema'] ?? '' ) && 8 === ( $browser_session['runtime']['summary']['plugins'] ?? 0 ) && 2 === ( $browser_session['runtime']['component_plugins'] ?? 0 ) && 1 === ( $browser_session['runtime']['summary']['mu_plugins'] ?? 0 ) && 1 === ( $browser_session['runtime']['summary']['themes'] ?? 0 ) && 1 === ( $browser_session['runtime']['summary']['bootstrap'] ?? 0 ) ); +$assert( 'browser Playground session server-packages remote runtime plugins after required components', ! is_wp_error( $browser_session ) && str_starts_with( (string) ( $browser_plugin_by_slug( 'generic-runtime-helper' )['url'] ?? '' ), 'data:application/zip;base64,' ) && false === ( $browser_plugin_by_slug( 'generic-runtime-helper' )['activate'] ?? true ) && 'runtime-plugin-remote-package' === ( $browser_plugin_by_slug( 'generic-runtime-helper' )['provenance']['source'] ?? '' ) ); +$assert( 'browser Playground session packages release ZIP runtime plugins without exposing source URLs', ! is_wp_error( $browser_session ) && str_starts_with( (string) ( $browser_plugin_by_slug( 'static-site-importer' )['url'] ?? '' ), 'data:application/zip;base64,' ) && ! str_contains( (string) ( $browser_step_with_plugin_slug( 'static-site-importer' )['pluginData']['url'] ?? '' ), 'github.com' ) && 'runtime-plugin-remote-package' === ( $browser_plugin_by_slug( 'static-site-importer' )['provenance']['source'] ?? '' ) ); +$assert( 'browser Playground session packages server runtime plugin paths', ! is_wp_error( $browser_session ) && str_starts_with( (string) ( $browser_plugin_by_slug( 'generic-caller-plugin' )['url'] ?? '' ), 'data:application/zip;base64,' ) && 64 === strlen( (string) ( $browser_plugin_by_slug( 'generic-caller-plugin' )['provenance']['sha256'] ?? '' ) ) && 'runtime-plugin-path' === ( $browser_plugin_by_slug( 'generic-caller-plugin' )['provenance']['source'] ?? '' ) ); +$assert( 'browser Playground session compiles git directory runtime plugins', ! is_wp_error( $browser_session ) && 'git:directory' === ( $browser_step_with_plugin_slug( 'example-git-plugin' )['pluginData']['resource'] ?? '' ) && 'plugins/example-git-plugin' === ( $browser_step_with_plugin_slug( 'example-git-plugin' )['pluginData']['path'] ?? '' ) && 'example-git-plugin' === ( $browser_step_with_plugin_slug( 'example-git-plugin' )['options']['targetFolderName'] ?? '' ) ); +$assert( 'browser Playground session compiles caller mu-plugin runtime dependency', ! is_wp_error( $browser_session ) && str_contains( (string) ( $browser_step_with_code( '/wordpress/wp-content/mu-plugins/caller-runtime.php' )['code'] ?? '' ), 'caller_runtime_task' ) ); +$assert( 'browser Playground session compiles theme runtime dependency', ! is_wp_error( $browser_session ) && str_contains( (string) ( $browser_step_with_code( '/wordpress/wp-content/themes/example-starter/style.css' )['code'] ?? '' ), "require_once '/wordpress/wp-load.php'" ) && str_contains( (string) ( $browser_step_with_code( '/wordpress/wp-content/themes/example-starter/style.css' )['code'] ?? '' ), "require_once ABSPATH . WPINC . '/theme.php'" ) && str_contains( (string) ( $browser_step_with_code( '/wordpress/wp-content/themes/example-starter/style.css' )['code'] ?? '' ), "switch_theme( 'example-starter' )" ) ); +$assert( 'browser Playground session compiles named bootstrap runtime operation', ! is_wp_error( $browser_session ) && str_contains( (string) ( $browser_step_with_code( "update_option( 'blogname', 'Browser Preview' )" )['code'] ?? '' ), "require_once '/wordpress/wp-load.php'" ) ); $assert( 'browser Playground session records trusted origins', ! is_wp_error( $browser_session ) && 'https://playground.automattic.ai' === ( $browser_session['playground']['provenance']['client_module_url']['origin'] ?? '' ) ); $assert( 'browser Playground session records browser plugin provenance', ! is_wp_error( $browser_session ) && 'example.test' === ( $browser_session['plugins'][0]['provenance']['host'] ?? '' ) && str_repeat( 'a', 64 ) === ( $browser_session['plugins'][0]['provenance']['sha256'] ?? '' ) ); $assert( 'browser Playground session includes recipe', ! is_wp_error( $browser_session ) && 'wp-codebox/workspace-recipe/v1' === ( $browser_session['recipe']['schema'] ?? '' ) ); @@ -574,7 +581,7 @@ function get_users( array $args ): array { return array( new WP_User( 11, 'Priva $assert( 'browser Playground recipe normalizes captured runner result', ! is_wp_error( $browser_session ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'wp_codebox_browser_capture_file' ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'wp-codebox/browser-capture/v1' ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'wp-codebox/browser-materialization/v1' ) ); $assert( 'browser Playground recipe keeps ability invocation path generic', ! is_wp_error( $browser_session ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'wp_get_ability( $ability_name )' ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'wp_codebox_browser_ability_unavailable' ) ); $assert( 'browser Playground recipe initializes abilities before invocation', ! is_wp_error( $browser_session ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'wp_abilities_api_categories_init' ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'wp_abilities_api_init' ) ); -$assert( 'browser Playground recipe installs caller mu-plugin before invocation', ! is_wp_error( $browser_session ) && 8 < count( $browser_session['playground']['blueprint']['steps'] ?? array() ) && str_contains( (string) ( $browser_session['playground']['blueprint']['steps'][8]['code'] ?? '' ), 'caller_runtime_task' ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'caller_runtime_task' ) ); +$assert( 'browser Playground recipe installs caller mu-plugin before invocation', ! is_wp_error( $browser_session ) && ! empty( $browser_step_with_code( 'caller_runtime_task' ) ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'caller_runtime_task' ) ); $assert( 'browser Playground recipe keeps invocation fixed after parent validation', ! is_wp_error( $browser_session ) && ! str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), '$payload[\'invocation\']' ) ); $assert( 'browser Playground recipe guards permission bypass to Playground', ! is_wp_error( $browser_session ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), "'/wordpress/' === \$wp_codebox_playground_root" ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'WP_CODEBOX_BROWSER_PLAYGROUND_RUNNER' ) && str_contains( (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'wp_codebox_browser_runner_not_playground' ) ); $assert( 'browser Playground session emits ready-to-code signal only when blueprint prerequisites are present', ! is_wp_error( $browser_session ) && true === ( $browser_session['signals']['ready_to_code']['emitted'] ?? false ) && 'ready_to_code' === ( $browser_session['signals']['ready_to_code']['name'] ?? '' ) && true === ( $browser_session['signals']['ready_to_code']['requirements']['agents_api'] ?? false ) && true === ( $browser_session['signals']['ready_to_code']['requirements']['data_machine'] ?? false ) && true === ( $browser_session['signals']['ready_to_code']['requirements']['data_machine_code'] ?? false ) && true === ( $browser_session['signals']['ready_to_code']['requirements']['provider_secret'] ?? false ) && true === ( $browser_session['signals']['ready_to_code']['requirements']['runtime_dependencies'] ?? false ) ); @@ -639,6 +646,53 @@ function get_users( array $args ): array { return array( new WP_User( 11, 'Priva ) : null; $assert( 'persist-browser-artifact rejects server executable extensions', is_wp_error( $persisted_browser_php ) && 'wp_codebox_browser_artifact_extension_blocked' === $persisted_browser_php->get_error_code() ); +$GLOBALS['wp_codebox_filters']['wp_codebox_default_provider'] = ''; +$GLOBALS['wp_codebox_filters']['wp_codebox_default_model'] = ''; +$GLOBALS['wp_codebox_filters']['wp_codebox_resolve_inheritance'] = function ( array $resolution, array $request ) use ( $root ): array { + $resolution['connectors'] = array( + array( + 'name' => $request['connectors'][0] ?? 'primary-ai', + 'status' => 'resolved', + 'provider' => 'openai', + 'model' => 'gpt-5.5', + 'provider_plugin_paths' => array( $root . '/ai-provider-inherited' ), + 'secret_env_values' => array( 'OPENAI_API_KEY' => 'sk-browser-secret-value' ), + 'credentials' => array( + 'schema' => 'wp-codebox/connector-credentials/v1', + 'connector' => $request['connectors'][0] ?? 'primary-ai', + 'scope' => 'connector', + 'status' => 'available', + 'secrets' => array( + array( + 'name' => 'OPENAI_API_KEY', + 'source' => 'connector', + 'status' => 'available', + 'value' => 'sk-browser-secret-value', + ), + ), + ), + ), + ); + + return $resolution; +}; +$browser_inherited_session = call_user_func( + $browser_session_ability['execute_callback'], + array( + 'goal' => 'Invoke a browser agent with inherited connector metadata.', + 'inherit' => array( 'connectors' => array( 'primary-ai' ) ), + ) +); +$browser_inherited_encoded = ! is_wp_error( $browser_inherited_session ) ? json_encode( $browser_inherited_session, JSON_UNESCAPED_SLASHES ) : ''; +$assert( 'browser Playground session resolves inherited provider and model', ! is_wp_error( $browser_inherited_session ) && 'openai' === ( $browser_inherited_session['provider'] ?? '' ) && 'gpt-5.5' === ( $browser_inherited_session['model'] ?? '' ) ); +$assert( 'browser Playground session packages inherited provider plugin path', ! is_wp_error( $browser_inherited_session ) && 1 === count( array_filter( $browser_inherited_session['plugins'] ?? array(), static fn( array $plugin ): bool => 'ai-provider-inherited' === ( $plugin['slug'] ?? '' ) ) ) ); +$assert( 'browser Playground session embeds first-class browser task payload', ! is_wp_error( $browser_inherited_session ) && 'wp-codebox/browser-agent-task-payload/v1' === ( $browser_inherited_session['task_payload']['schema'] ?? '' ) && 'openai' === ( $browser_inherited_session['recipe']['browser']['task_payload']['provider'] ?? '' ) && 'gpt-5.5' === ( $browser_inherited_session['recipe']['browser']['task_payload']['model'] ?? '' ) ); +$assert( 'browser Playground session records inherited connector credential provenance without values', ! is_wp_error( $browser_inherited_session ) && 'wp-codebox/connector-credentials/v1' === ( $browser_inherited_session['inheritance']['connectors'][0]['credentials']['schema'] ?? '' ) && 'available' === ( $browser_inherited_session['task_payload']['inheritance']['connectors'][0]['credentials']['secrets'][0]['status'] ?? '' ) && str_contains( $browser_inherited_encoded, 'OPENAI_API_KEY' ) && ! str_contains( $browser_inherited_encoded, 'sk-browser-secret-value' ) && ! str_contains( $browser_inherited_encoded, 'secret_env_values' ) ); +$assert( 'browser Playground recipe defaults to agents chat invocation with embedded payload', ! is_wp_error( $browser_inherited_session ) && 'ability' === ( $browser_inherited_session['recipe']['browser']['invocation']['type'] ?? '' ) && 'agents/chat' === ( $browser_inherited_session['recipe']['browser']['invocation']['name'] ?? '' ) && str_contains( (string) ( $browser_inherited_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' ), 'wp_get_ability( $ability_name )' ) ); +$GLOBALS['wp_codebox_filters']['wp_codebox_default_provider'] = 'openai'; +$GLOBALS['wp_codebox_filters']['wp_codebox_default_model'] = 'gpt-5.5'; +unset( $GLOBALS['wp_codebox_filters']['wp_codebox_resolve_inheritance'] ); + $browser_packaged_mu_session = call_user_func( $browser_session_ability['execute_callback'], array(