Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion packages/wordpress-plugin/assets/browser-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,29 @@ try {
return `<?php\n${ marker }\n?>\n${ source }`;
};

const normalizePhpPrelude = ( prelude ) => String( prelude || '' )
.replace( /^\s*<\?php\s*/i, '' )
.replace( /\?>\s*$/i, '' );

const withBrowserRunnerPrelude = ( code, recipe ) => {
const source = String( code || '' );
const prelude = recipe?.browser?.runner_contract?.php_prelude;
if ( typeof prelude !== 'string' || prelude.trim() === '' || source.includes( 'function wp_codebox_browser_artifact_environment' ) ) {
return source;
}

return injectPhpPrelude( source, normalizePhpPrelude( prelude ) );
};

const injectPhpPrelude = ( code, prelude ) => {
const source = String( code || '' );
if ( source.startsWith( '<?php' ) ) {
return source.replace( '<?php', `<?php\n${ prelude }` );
}

return `<?php\n${ prelude }\n?>\n${ source }`;
};

const browserSessionRecipe = ( session ) => {
if ( ! session || typeof session !== 'object' ) {
throw new Error( 'WP Codebox browser session output is required.' );
Expand Down Expand Up @@ -980,6 +1003,9 @@ try {
if ( ! payload || typeof payload !== 'object' ) {
throw runtimeError( 'recipe_validate', 'browser_recipe_task_payload_missing', 'WP Codebox browser recipe task payload missing.' );
}
if ( ! recipe?.browser?.runner_contract?.php_prelude && steps.some( ( step ) => ( step?.args || [] ).some( ( arg ) => typeof arg === 'string' && arg.startsWith( 'code=' ) && arg.includes( 'wp_codebox_browser_' ) && ! arg.includes( 'function wp_codebox_browser_artifact_environment' ) ) ) ) {
throw runtimeError( 'recipe_validate', 'browser_recipe_runner_contract_missing', 'Browser recipe PHP references WP Codebox runner helpers but does not include a runner contract.' );
}

const writeResult = await writeFile( client, {
path: taskPath,
Expand All @@ -1006,7 +1032,7 @@ try {

lastResult = await runPhpRequest( client, {
...options,
code: markBrowserPlaygroundRunner( codeArg.slice( 5 ) ),
code: markBrowserPlaygroundRunner( withBrowserRunnerPrelude( codeArg.slice( 5 ), recipe ) ),
name: options.name || 'codebox-recipe',
expectJson: true,
forceRequest: true,
Expand Down Expand Up @@ -1220,6 +1246,18 @@ try {
const result = await runRecipe( client, {
browser: {
task_path: recipeTaskPath,
runner_contract: {
schema: 'wp-codebox/browser-runner-contract/v1',
php_prelude: `<?php
function wp_codebox_browser_artifact_environment( array $payload ): array {
$contract = is_array( $payload['artifacts'] ?? null ) ? $payload['artifacts'] : array();
$root = rtrim( (string) ( $contract['root'] ?? 'wp-codebox-output' ), '/' ) . '/';
return array( 'contract' => $contract, 'root' => $root );
}
function wp_codebox_browser_capture_artifact_bundle( array $payload ): array {
return is_array( $payload['artifacts'] ?? null ) ? $payload['artifacts'] : array();
}`,
},
},
workflow: {
steps: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ private static function browser_agent_recipe( array $task_input, string $session
}
}

$runner_php = self::browser_agent_runner_php( $task_input, $session_id, $task_path, $result_path, $invocation, $captures );
$runner_contract = self::browser_agent_runner_contract( $runner_php );

return array(
'schema' => 'wp-codebox/workspace-recipe/v1',
'runtime' => array(
Expand All @@ -60,7 +63,7 @@ private static function browser_agent_recipe( array $task_input, string $session
array(
'command' => 'wordpress.run-php',
'args' => array(
'code=' . self::browser_agent_runner_php( $task_input, $session_id, $task_path, $result_path, $invocation, $captures ),
'code=' . $runner_php,
),
),
),
Expand All @@ -75,6 +78,7 @@ private static function browser_agent_recipe( array $task_input, string $session
'task_payload' => $task_payload,
'invocation' => self::browser_runner_invocation_metadata( $invocation ),
'captures' => $captures,
'runner_contract' => $runner_contract,
),
);
}
Expand Down Expand Up @@ -1036,6 +1040,7 @@ function wp_codebox_browser_runtime_tool_callback( array $request, array $payloa
}
$invocation_type = (string) ( $invocation[\'type\'] ?? \'ability\' );

/* WP_CODEBOX_BROWSER_RUNNER_BODY_START */
if ( ! $wp_codebox_is_playground ) {
$response = new WP_Error(
\'wp_codebox_browser_runner_not_playground\',
Expand Down Expand Up @@ -1098,6 +1103,7 @@ function wp_codebox_browser_runtime_tool_callback( array $request, array $payloa
}
}

/* WP_CODEBOX_BROWSER_RUNNER_BODY_END */
$captures = array();
foreach ( $capture_paths as $capture ) {
if ( is_array( $capture ) ) {
Expand Down Expand Up @@ -1189,6 +1195,26 @@ function wp_codebox_browser_runtime_tool_callback( array $request, array $payloa
';
}

/** @return array<string,string> */
private static function browser_agent_runner_contract( string $runner_php ): array {
$body_start = '/* WP_CODEBOX_BROWSER_RUNNER_BODY_START */';
$body_end = '/* WP_CODEBOX_BROWSER_RUNNER_BODY_END */';
$start_pos = strpos( $runner_php, $body_start );
$end_pos = strpos( $runner_php, $body_end );

if ( false === $start_pos || false === $end_pos || $end_pos <= $start_pos ) {
return array(
'schema' => 'wp-codebox/browser-runner-contract/v1',
);
}

return array(
'schema' => 'wp-codebox/browser-runner-contract/v1',
'php_prelude' => substr( $runner_php, 0, $start_pos ),
'php_footer' => substr( $runner_php, $end_pos + strlen( $body_end ) ),
);
}

/** @param array<string,mixed> $playground Playground config. */
private static function browser_artifact_base_path( array $playground ): string {
return self::normalize_absolute_browser_path( (string) ( $playground['artifact_base_path'] ?? '/wordpress/wp-content/uploads/wp-codebox/artifacts' ) );
Expand Down
3 changes: 3 additions & 0 deletions tests/smoke-wordpress-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,9 @@ static function ( $result, array $input, array $payload ) use ( $runner_report_p
$runner_php = (string) ( $browser_session['recipe']['workflow']['steps'][0]['args'][0] ?? '' );
$assert( 'browser Playground generated runner has no Studio Web-specific artifact paths', ! str_contains( $runner_php, '/wordpress/wp-content/uploads/studio-web' ) && ! str_contains( $runner_php, 'studio-web/website' ) );
$assert( 'browser Playground generated runner defaults to generic Codebox artifacts path', str_contains( $runner_php, '/wordpress/wp-content/uploads/wp-codebox/artifacts' ) && str_contains( $runner_php, 'wp-codebox-output/' ) );
$runner_contract = $browser_session['recipe']['browser']['runner_contract'] ?? array();
$assert( 'browser Playground recipe exposes reusable runner PHP contract prelude', is_array( $runner_contract ) && 'wp-codebox/browser-runner-contract/v1' === ( $runner_contract['schema'] ?? '' ) && str_contains( (string) ( $runner_contract['php_prelude'] ?? '' ), 'function wp_codebox_browser_artifact_environment' ) && str_contains( (string) ( $runner_contract['php_prelude'] ?? '' ), '$wp_codebox_is_playground' ) && str_contains( (string) ( $runner_contract['php_prelude'] ?? '' ), '$capture_paths' ) );
$assert( 'browser Playground recipe exposes reusable runner PHP contract footer', is_array( $runner_contract ) && str_contains( (string) ( $runner_contract['php_footer'] ?? '' ), 'wp_codebox_browser_artifact_capture_diagnostics' ) && str_contains( (string) ( $runner_contract['php_footer'] ?? '' ), 'file_put_contents( $result_path' ) );
$runner_php = preg_replace( '/^code=/', '', $runner_php ) ?? $runner_php;
$runner_php = preg_replace( '/^<\?php\s*/', '', $runner_php ) ?? $runner_php;
$runner_php = str_replace( "require_once '/wordpress/wp-load.php';", '', $runner_php );
Expand Down