diff --git a/packages/wordpress-plugin/README.md b/packages/wordpress-plugin/README.md index ec50e494..e0e34204 100644 --- a/packages/wordpress-plugin/README.md +++ b/packages/wordpress-plugin/README.md @@ -331,6 +331,17 @@ an external adapter can consume the verified artifact payload and record adapter metadata, PR URL, branch, commit, and artifact digest without WP Codebox calling any product-specific apply-back system. +## Agent Context Section + +The plugin registers a `wp-codebox` section for the `agents-md` context on the +upstream context section registry (`WP_Agent_Context_Section_Registry`), so any +host that composes `AGENTS.md` from registered sections surfaces Codebox routing, +safety, and discovery guidance to coding agents. Registration is a no-op when the +registry class is not loaded; no host-specific composer is referenced. + +The rendered WP-CLI prefix defaults to `wp --path=` and can be adjusted +through the `wp_codebox_agents_md_wp_cli_cmd` filter. + ## Configuration Runtime components can be supplied by ability input, the diff --git a/packages/wordpress-plugin/src/class-wp-codebox-agents-md-section.php b/packages/wordpress-plugin/src/class-wp-codebox-agents-md-section.php new file mode 100644 index 00000000..59365339 --- /dev/null +++ b/packages/wordpress-plugin/src/class-wp-codebox-agents-md-section.php @@ -0,0 +1,104 @@ + 'WP Codebox', + 'description' => 'Disposable WordPress sandbox CLI and abilities.', + 'meta' => array( + 'owner' => 'wp-codebox', + 'freshness' => 'generated', + 'conditions' => 'Registered when wp-codebox is active and the Agents API context section registry is loaded.', + ), + ) + ); + } + + /** + * WP-CLI invocation prefix used in rendered guidance. + * + * Defaults to a path-pinned invocation. Hosts that need a different + * prefix (flags, wrapper binaries, remote routing) adjust it through the + * `wp_codebox_agents_md_wp_cli_cmd` filter. + */ + public static function wp_cli_cmd(): string { + $default = 'wp --path=' . rtrim( (string) ABSPATH, '/\\' ); + + $cmd = apply_filters( 'wp_codebox_agents_md_wp_cli_cmd', $default ); + + return is_string( $cmd ) && '' !== trim( $cmd ) ? trim( $cmd ) : $default; + } + + /** + * Render the section body. + */ + public static function render(): string { + $wp = self::wp_cli_cmd(); + + $lines = array( + '## WP Codebox', + '', + 'WP Codebox launches disposable, isolated WordPress sandboxes for coding-agent tasks, runtime workloads, and fuzzing. Sandboxes cannot touch the host site; they produce artifact bundles (changed files, patches, evidence) that the host reviews and applies back explicitly. Abilities live under `wp-codebox/*`; the CLI mirrors the same PHP service layer.', + '', + '**Default routing**', + '- Run a bounded coding task in a sandbox: `' . $wp . ' codebox run-agent-task --goal=\'...\' --format=json`', + '- Independent task waves: `' . $wp . ' codebox run-agent-task-batch --input-file=` or `' . $wp . ' codebox run-agent-task-fanout --input-file=`', + '- Run a runtime task or WordPress workload: `' . $wp . ' codebox run-runtime-task --input-json=\'{...}\'` or `' . $wp . ' codebox run-wordpress-workload --input-file=`', + '- Check runtime/provider readiness before dispatch: `' . $wp . ' codebox resolve-runtime-requirements --format=json`', + '- Review what a sandbox produced: `' . $wp . ' codebox artifacts list --format=json`, then `' . $wp . ' codebox artifacts inspect --format=json`', + '- Apply reviewed changes to the host: `' . $wp . ' codebox artifacts preflight-apply ` → `' . $wp . ' codebox artifacts stage-apply --approved-files=...` → `' . $wp . ' codebox artifacts apply --approved-files=...`', + '- Browser-executed Playground session: `' . $wp . ' codebox browser-session create --goal=\'...\' --format=json`', + '', + '**Safety**', + '- `artifacts apply` mutates the host through the configured apply-back adapter. Always `preflight-apply` and review `inspect` output first; pass only the files you approved via `--approved-files`.', + '- Provider credentials reach sandboxes through `secret_env` names. Never print secret values in prompts, task payloads, logs, or artifacts.', + '- WP-CLI runs in trusted operator context and bypasses ability permission callbacks; shell access is the permission boundary.', + '', + '**Discovery**', + 'Use `' . $wp . ' codebox --help` and `' . $wp . ' codebox --help` for the live command contract; `--input-json` / `--input-file` carry complex payloads and CLI flags override payload fields. Inspect `' . $wp . ' codebox runtime descriptor --format=json` for the registered runtime profile. Live `--help` output is authoritative.', + ); + + return implode( "\n", $lines ) . "\n"; + } +} diff --git a/packages/wordpress-plugin/wp-codebox.php b/packages/wordpress-plugin/wp-codebox.php index 9062c8ea..8ae0c42e 100644 --- a/packages/wordpress-plugin/wp-codebox.php +++ b/packages/wordpress-plugin/wp-codebox.php @@ -77,6 +77,7 @@ require_once __DIR__ . '/src/class-wp-codebox-runtime-package-executor.php'; require_once __DIR__ . '/src/class-wp-codebox-api.php'; require_once __DIR__ . '/src/class-wp-codebox-abilities.php'; +require_once __DIR__ . '/src/class-wp-codebox-agents-md-section.php'; if ( defined( 'WP_CLI' ) && WP_CLI ) { require_once __DIR__ . '/src/class-wp-codebox-cli-command.php'; @@ -94,6 +95,7 @@ add_action( 'plugins_loaded', array( WP_Codebox_Php_Ai_Client_Browser_Provider_Adapter::class, 'register' ), 20 ); new WP_Codebox_Abilities(); WP_Codebox_Browser_Provider_Bridge::register(); +WP_Codebox_Agents_Md_Section::register(); if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_Codebox_CLI_Command::register(); diff --git a/tests/php-agents-md-section.test.ts b/tests/php-agents-md-section.test.ts new file mode 100644 index 00000000..f8da89cf --- /dev/null +++ b/tests/php-agents-md-section.test.ts @@ -0,0 +1,103 @@ +import assert from "node:assert/strict" +import { execFileSync } from "node:child_process" + +// Proves the plugin registers its own `agents-md` context section on a +// minimal stand-in for WP_Agent_Context_Section_Registry (the Agents API +// substrate), that the render body carries routing/safety/discovery guidance +// with the host-filtered WP-CLI prefix, and that registration is a no-op +// when the substrate class is absent. +const output = execFileSync( + "php", + [ + "-r", + String.raw` +define('ABSPATH', '/srv/site/'); + +$GLOBALS['actions'] = array(); +$GLOBALS['filters'] = array(); +function add_action(string $hook, callable $cb, int $priority = 10, int $accepted_args = 1): void { $GLOBALS['actions'][$hook][] = $cb; } +function do_action(string $hook, ...$args): void { foreach ($GLOBALS['actions'][$hook] ?? array() as $cb) { $cb(); } } +function add_filter(string $hook, callable $cb, int $priority = 10, int $accepted_args = 1): void { $GLOBALS['filters'][$hook][] = $cb; } +function apply_filters(string $hook, $value, ...$args) { foreach ($GLOBALS['filters'][$hook] ?? array() as $cb) { $value = $cb($value); } return $value; } + +require __DIR__ . '/packages/wordpress-plugin/src/class-wp-codebox-agents-md-section.php'; + +// 1. Without the substrate class, firing the hook registers nothing and does not fatal. +WP_Codebox_Agents_Md_Section::register(); +do_action('agents_api_context_sections'); +$without_substrate_ok = true; + +$registered_before_substrate = isset($GLOBALS['registered']) ? count($GLOBALS['registered']) : 0; + +// 2. With a minimal substrate stand-in, the section registers with the expected shape. +// Declared inside a block so PHP does not hoist it above step 1. +if (true) { + final class WP_Agent_Context_Section_Registry { + public static array $registered = array(); + public static function register(string $context, string $slug, int $priority, callable $callback, array $args = array()) { + self::$registered[] = compact('context', 'slug', 'priority', 'callback', 'args'); + return self::$registered[count(self::$registered) - 1]; + } + } +} +do_action('agents_api_context_sections'); + +add_filter('wp_codebox_agents_md_wp_cli_cmd', static function ($cmd) { return 'wp --allow-root --path=/srv/site'; }); + +$reg = WP_Agent_Context_Section_Registry::$registered[0] ?? null; +$rendered = $reg ? ($reg['callback'])(array(), array()) : ''; + +echo json_encode(array( + 'without_substrate_ok' => $without_substrate_ok && 0 === $registered_before_substrate, + 'count' => count(WP_Agent_Context_Section_Registry::$registered), + 'context' => $reg['context'] ?? null, + 'slug' => $reg['slug'] ?? null, + 'priority' => $reg['priority'] ?? null, + 'label' => $reg['args']['label'] ?? null, + 'owner' => $reg['args']['meta']['owner'] ?? null, + 'freshness' => $reg['args']['meta']['freshness'] ?? null, + 'default_cmd' => 'wp --path=/srv/site', + 'rendered' => $rendered, +), JSON_UNESCAPED_SLASHES); +`, + ], + { cwd: new URL("..", import.meta.url), encoding: "utf8" } +) + +const result = JSON.parse(output) + +assert.equal(result.without_substrate_ok, true) +assert.equal(result.count, 1, "registers exactly one section") +assert.equal(result.context, "agents-md") +assert.equal(result.slug, "wp-codebox") +assert.equal(result.priority, 40) +assert.equal(result.label, "WP Codebox") +assert.equal(result.owner, "wp-codebox") +assert.equal(result.freshness, "generated") + +const rendered: string = result.rendered +assert.ok(rendered.startsWith("## WP Codebox\n"), "section starts with its heading") +for (const heading of ["**Default routing**", "**Safety**", "**Discovery**"]) { + assert.ok(rendered.includes(heading), `contains ${heading}`) +} +for (const verb of [ + "codebox run-agent-task", + "codebox run-agent-task-fanout", + "codebox run-wordpress-workload", + "codebox resolve-runtime-requirements", + "codebox artifacts inspect", + "codebox artifacts preflight-apply", + "codebox artifacts apply", + "codebox browser-session create", + "codebox --help", +]) { + assert.ok(rendered.includes(verb), `routes to ${verb}`) +} +assert.ok( + rendered.includes("wp --allow-root --path=/srv/site codebox"), + "uses the host-filtered WP-CLI prefix" +) +assert.ok(!rendered.includes(result.default_cmd + " codebox"), "filtered prefix replaces the default") +assert.ok(!/datamachine|DataMachine/.test(rendered), "no host composer names leak into generic guidance") + +console.log("php-agents-md-section: OK")