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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ For WordPress, this means a control plane such as Studio, Data Machine, or WordP
- `@chubes4/sandbox-runtime-core`: backend-agnostic runtime interfaces and shared types.
- `@chubes4/sandbox-runtime-playground`: first backend adapter shaped around WordPress Playground.
- `@chubes4/sandbox-runtime-cli`: `sandbox-runtime` command for external consumers.
- `packages/wordpress-plugin`: WordPress ability surface for parent sites that launch sandboxed agent tasks.

## CLI

Expand Down Expand Up @@ -68,6 +69,18 @@ Use `--wp trunk`, `--wp nightly`, or a numeric WordPress version when a mounted

The fixture plugin is documented in [`examples/simple-plugin/README.md`](examples/simple-plugin/README.md).

## WordPress Ability Surface

The WordPress plugin in `packages/wordpress-plugin` registers:

- `sandbox-runtime/run-agent-task`

This is the parent-site control-plane surface for frontend/chat integrations. A chat agent can be granted this ability without receiving raw shell or parent-site filesystem access. The ability launches `sandbox-runtime agent-sandbox-run`, which boots a disposable WordPress Playground runtime, mounts the configured agent stack, executes the task, and returns artifact metadata.

Component paths come from ability input, the `sandbox_runtime_component_paths` option, or the `sandbox_runtime_component_paths` filter. Data Machine Code is the mounted coding-tools component for file-editing agent sandboxes; it provides the workspace/file/GitHub tools inside the sandbox, while Sandbox Runtime owns the parent-site control plane and sandbox lifecycle.

Apply-back is intentionally separate: sandbox task execution returns artifacts and proposed outputs, while applying changes to the real site should use a distinct reviewed permission path.

## v0 Runtime Policy

`RuntimePolicy` is a portable declaration that every backend receives with `RuntimeCreateSpec`. The core package exposes `validateRuntimePolicy()`, `assertRuntimePolicy()`, and `assertRuntimeCommandAllowed()` so backends and control planes can validate the v0 policy shape before work starts.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"build": "tsc -b packages/runtime-core packages/runtime-playground packages/cli",
"policy-validation-smoke": "tsx scripts/policy-validation-smoke.ts",
"sandbox-runtime": "node packages/cli/dist/index.js",
"check": "npm run build && npm run policy-validation-smoke && npm run sandbox-runtime -- run --mount ./examples/simple-plugin:/wordpress/wp-content/plugins/simple-plugin --command wordpress.run-php --arg code-file=./examples/simple-plugin/probe.php --artifacts ./artifacts --json"
"wordpress-plugin-smoke": "php tests/smoke-wordpress-plugin.php",
"check": "npm run build && npm run policy-validation-smoke && npm run wordpress-plugin-smoke && npm run sandbox-runtime -- run --mount ./examples/simple-plugin:/wordpress/wp-content/plugins/simple-plugin --command wordpress.run-php --arg code-file=./examples/simple-plugin/probe.php --artifacts ./artifacts --json"
},
"workspaces": [
"packages/*"
Expand Down
166 changes: 159 additions & 7 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ interface AgentRuntimeProbeOptions {
json: boolean
}

interface AgentSandboxRunOptions extends AgentRuntimeProbeOptions {
task: string
code?: string
codeFile?: string
}

const defaultPolicy: RuntimePolicy = {
network: "deny",
filesystem: "readwrite-mounts",
Expand Down Expand Up @@ -70,6 +76,23 @@ async function main(args: string[]): Promise<number> {
return output.success ? 0 : 1
}

if (command === "agent-sandbox-run") {
const options = parseAgentSandboxRunOptions(args)
const runOptions = await agentSandboxRunOptions(options)
const execute = () => run(runOptions)

if (!options.json) {
const output = await execute()
printHumanOutput(output)
return output.success ? 0 : 1
}

const { result, logs } = await captureStdout(execute)
const output = logs.length > 0 ? { ...result, logs } : result
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`)
return output.success ? 0 : 1
}

if (command !== "run") {
console.error(`Unknown command: ${command}`)
printHelp()
Expand All @@ -93,12 +116,7 @@ async function main(args: string[]): Promise<number> {

function agentRuntimeProbeRunOptions(options: AgentRuntimeProbeOptions): RunOptions {
return {
mounts: [
{ source: resolve(options.agentsApiPath), target: "/wordpress/wp-content/plugins/agents-api", mode: "readwrite" },
{ source: resolve(options.dataMachinePath), target: "/wordpress/wp-content/plugins/data-machine", mode: "readwrite" },
{ source: resolve(options.dataMachineCodePath), target: "/wordpress/wp-content/plugins/data-machine-code", mode: "readwrite" },
{ source: resolve(options.openaiProviderPath), target: "/wordpress/wp-content/plugins/ai-provider-for-openai", mode: "readwrite" },
],
mounts: agentRuntimeMounts(options),
command: "wordpress.run-php",
args: [`code=${agentRuntimeProbeCode()}`],
wpVersion: options.wpVersion ?? "trunk",
Expand All @@ -107,7 +125,27 @@ function agentRuntimeProbeRunOptions(options: AgentRuntimeProbeOptions): RunOpti
}
}

function parseAgentRuntimeProbeOptions(args: string[]): AgentRuntimeProbeOptions {
async function agentSandboxRunOptions(options: AgentSandboxRunOptions): Promise<RunOptions> {
return {
mounts: agentRuntimeMounts(options),
command: "wordpress.run-php",
args: [`code=${agentSandboxRunCode(options.task, await resolveSandboxTaskCode(options))}`],
wpVersion: options.wpVersion ?? "trunk",
artifactsDirectory: options.artifactsDirectory,
json: options.json,
}
}

function agentRuntimeMounts(options: AgentRuntimeProbeOptions): RunOptions["mounts"] {
return [
{ source: resolve(options.agentsApiPath), target: "/wordpress/wp-content/plugins/agents-api", mode: "readwrite" },
{ source: resolve(options.dataMachinePath), target: "/wordpress/wp-content/plugins/data-machine", mode: "readwrite" },
{ source: resolve(options.dataMachineCodePath), target: "/wordpress/wp-content/plugins/data-machine-code", mode: "readwrite" },
{ source: resolve(options.openaiProviderPath), target: "/wordpress/wp-content/plugins/ai-provider-for-openai", mode: "readwrite" },
]
}

function parseAgentRuntimeProbeOptions(args: string[], extraOptions: string[] = []): AgentRuntimeProbeOptions {
const options: Partial<AgentRuntimeProbeOptions> = { json: false }

for (let index = 0; index < args.length; index++) {
Expand Down Expand Up @@ -145,6 +183,9 @@ function parseAgentRuntimeProbeOptions(args: string[]): AgentRuntimeProbeOptions
options.artifactsDirectory = value
break
default:
if (extraOptions.includes(name)) {
break
}
throw new Error(`Unknown option: ${name}`)
}
}
Expand All @@ -163,6 +204,38 @@ function parseAgentRuntimeProbeOptions(args: string[]): AgentRuntimeProbeOptions
return options as AgentRuntimeProbeOptions
}

function parseAgentSandboxRunOptions(args: string[]): AgentSandboxRunOptions {
const options = parseAgentRuntimeProbeOptions(args, ["--task", "--code", "--code-file"]) as Partial<AgentSandboxRunOptions>

for (let index = 0; index < args.length; index++) {
const arg = args[index]
const [name, inlineValue] = arg.split("=", 2)
const value = inlineValue ?? args[index + 1]

switch (name) {
case "--task":
options.task = value
break
case "--code":
options.code = value
break
case "--code-file":
options.codeFile = value
break
}
}

if (!options.task) {
throw new Error("Missing required option: --task")
}

if (options.code && options.codeFile) {
throw new Error("Use either --code or --code-file, not both")
}

return options as AgentSandboxRunOptions
}

async function run(options: RunOptions): Promise<RunOutput> {
let runtime: Awaited<ReturnType<typeof createRuntime>> | undefined
let execution: ExecutionResult | undefined
Expand Down Expand Up @@ -352,6 +425,7 @@ function printHelp(): void {
console.log(`Usage:
sandbox-runtime run --mount <host>:<vfs> --command <id> [options]
sandbox-runtime agent-runtime-probe --agents-api <path> --data-machine <path> --data-machine-code <path> --openai-provider <path> [options]
sandbox-runtime agent-sandbox-run --agents-api <path> --data-machine <path> --data-machine-code <path> --openai-provider <path> --task <text> [options]

Options:
--mount <host:vfs> Mount a host path into the runtime. Repeatable.
Expand All @@ -368,10 +442,88 @@ Agent runtime probe options:
--data-machine-code <path> Local Data Machine Code plugin checkout.
--openai-provider <path> Local AI Provider for OpenAI plugin checkout.

Agent sandbox run options:
--task <text> Task description recorded in the sandbox run.
--code <php> Optional PHP body to run after the agent stack boots.
--code-file <path> Optional PHP file to run after the agent stack boots.

Example:
sandbox-runtime run --mount ./examples/simple-plugin:/wordpress/wp-content/plugins/simple-plugin --command wordpress.run-php --arg code-file=./examples/simple-plugin/probe.php --artifacts ./artifacts --json`)
}

async function resolveSandboxTaskCode(options: AgentSandboxRunOptions): Promise<string> {
if (options.code) {
return options.code
}

if (options.codeFile) {
return readFile(resolve(options.codeFile), "utf8")
}

return `echo json_encode(array('task_received' => true), JSON_PRETTY_PRINT);`
}

function agentSandboxRunCode(task: string, code: string): string {
return `<?php
require_once ABSPATH . 'wp-admin/includes/plugin.php';

$plugins = array(
'agents-api/agents-api.php',
'data-machine/data-machine.php',
'data-machine-code/data-machine-code.php',
'ai-provider-for-openai/plugin.php',
);

$activation_results = array();

foreach ($plugins as $plugin) {
$result = activate_plugin($plugin);
$activation_results[$plugin] = array(
'active' => is_plugin_active($plugin),
'error' => is_wp_error($result) ? $result->get_error_message() : null,
);
}

do_action('plugins_loaded');
do_action('init');
do_action('wp_abilities_api_categories_init');
do_action('wp_abilities_api_init');

$sandbox_task = ${JSON.stringify(task)};
$sandbox_stack = array(
'plugins' => $activation_results,
'signals' => array(
'agents_api_loaded' => defined('AGENTS_API_LOADED'),
'agents_registry_class' => class_exists('WP_Agents_Registry'),
'data_machine_version' => defined('DATAMACHINE_VERSION') ? DATAMACHINE_VERSION : null,
'data_machine_permission_helper' => class_exists('DataMachine\\Abilities\\PermissionHelper'),
'data_machine_code_version' => defined('DATAMACHINE_CODE_VERSION') ? DATAMACHINE_CODE_VERSION : null,
'data_machine_code_workspace' => class_exists('DataMachineCode\\Workspace\\Workspace'),
'openai_provider_plugin_loaded' => function_exists('WordPress\\OpenAiAiProvider\\register_provider'),
),
);

ob_start();
${phpBody(code)}
$sandbox_output = ob_get_clean();

echo json_encode(
array(
'command' => 'agent-sandbox.run',
'task' => $sandbox_task,
'wp_loaded' => function_exists('wp_insert_post'),
'stack' => $sandbox_stack,
'output' => $sandbox_output,
),
JSON_PRETTY_PRINT
);
`
}

function phpBody(code: string): string {
return code.trimStart().replace(/^<\?php\s*/, "")
}

function agentRuntimeProbeCode(): string {
return `<?php
require_once ABSPATH . 'wp-admin/includes/plugin.php';
Expand Down
35 changes: 35 additions & 0 deletions packages/wordpress-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Sandbox Runtime WordPress Plugin

Registers the WordPress ability surface for launching isolated Sandbox Runtime
agent sandboxes from a parent site.

## Ability

- `sandbox-runtime/run-agent-task`

The ability runs `sandbox-runtime agent-sandbox-run`, which boots a disposable
WordPress Playground runtime, mounts the agent stack components, executes the
task, and returns artifact metadata.

## Configuration

Component paths can be supplied by ability input, the
`sandbox_runtime_component_paths` option, or the `sandbox_runtime_component_paths`
filter.

Expected component keys:

- `agents_api`
- `data_machine`
- `data_machine_code`
- `openai_provider`

The CLI binary can be supplied by ability input, the `sandbox_runtime_bin` option,
or the `sandbox_runtime_bin` filter.

## Boundary

Data Machine Code is the mounted coding-tools component for file-editing agent
sandboxes. It provides workspace/file/GitHub tools inside the isolated runtime.
This plugin owns the parent-site ability surface and sandbox lifecycle boundary;
DMC does not own that control plane.
26 changes: 26 additions & 0 deletions packages/wordpress-plugin/sandbox-runtime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* Plugin Name: Sandbox Runtime
* Plugin URI: https://github.com/chubes4/sandbox-runtime
* Description: WordPress ability surface for launching isolated Sandbox Runtime agent sandboxes.
* Version: 0.1.0
* Requires at least: 6.9
* Requires PHP: 8.2
* Author: Chris Huber
* License: GPL-2.0-or-later
* Text Domain: sandbox-runtime
*/

if ( ! defined( 'WPINC' ) ) {
die;
}

define( 'SANDBOX_RUNTIME_PLUGIN_VERSION', '0.1.0' );
define( 'SANDBOX_RUNTIME_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );

require_once __DIR__ . '/src/class-sandbox-runtime-agent-sandbox-runner.php';
require_once __DIR__ . '/src/class-sandbox-runtime-abilities.php';

add_action( 'plugins_loaded', static function (): void {
new Sandbox_Runtime_Abilities();
}, 20 );
Loading