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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ Redaction is part of the contract. The default `redaction: "safe"` avoids user
identity fields and requires explicit allowlists for options and users.
`redaction: "none"` is an opt-in export mode for trusted callers that need identity fields.
Consumers should verify artifact SHA-256 values before replay and should branch
on `schema` and `version` instead of projecting per-consumer legacy state shapes.
on `schema` and `version` instead of projecting per-consumer state shapes.

Replay is bounded to the generic runtime contract. A consumer can replay a step
by creating a compatible backend runtime, applying the same mounts/artifact
Expand Down Expand Up @@ -696,7 +696,7 @@ The canonical schema source is `createWorkspaceRecipeJsonSchema()` in `@automatt
npm run wp-codebox -- schema recipe --json
```

The schema covers `runtime`, `inputs.mounts`, `inputs.workspaces`, `inputs.extra_plugins` / `inputs.extraPlugins`, `inputs.secretEnv`, `workflow.steps`, and `artifacts`.
The schema covers `runtime`, `inputs.mounts`, `inputs.workspaces`, `inputs.extra_plugins`, `inputs.secretEnv`, `workflow.steps`, and `artifacts`.

### `run`

Expand Down Expand Up @@ -734,7 +734,7 @@ Supported runtime commands today:

For synthetic web performance investigations, enable `capture=performance` and use `profile=low-end-mobile-slow-4g` or `throttle=low-end-mobile-slow-4g` to apply a deterministic Chromium CPU/network profile before navigation. Performance artifacts include navigation timing, TTFB, FCP, LCP, resource timing, long tasks, CLS, and DOM metrics. Budget assertions may use promoted `browser_*` metric names or short web-performance aliases, for example `assert=lcp_ms<=2500`, `assert=fcp_ms<=1800`, `assert=ttfb_ms<=800`, and `assert=nav_duration_ms<=5000`.

`wordpress.browser-actions` drives the preview with an ordered interaction script so Codebox can prove a plugin still *works* under interaction, not just that it renders. Pass the script as `steps-json=<array>` (inline JSON, or `@<path>` to read it from a file); the legacy `actions-json=<array>` shape is still accepted and normalized to steps. Each step is a thin, stable mapping over a Playwright locator action — this is not a test-runner DSL.
`wordpress.browser-actions` drives the preview with an ordered interaction script so Codebox can prove a plugin still *works* under interaction, not just that it renders. Pass the script as `steps-json=<array>` (inline JSON, or `@<path>` to read it from a file). Each step is a thin, stable mapping over a Playwright locator action — this is not a test-runner DSL.

Step kinds: `navigate` (`url`, optional `waitFor=domcontentloaded|load|networkidle`), `click`/`hover` (`selector` or `text`), `fill`/`type` (`selector`, `value`), `press` (`key`, optional `selector`), `drag` (`from` selector, `to` as `{ "selector": ... }` or `{ "x": n, "y": n }`), `select` (`selector`, `value` or `values`), `waitFor` (`selector` or `waitFor=domcontentloaded|load|networkidle|duration|selector:<sel>`), `evaluate` (`expression`, optional `assert` to deep-equal the result), `expect` (`selector`, optional `state=visible|hidden|attached|detached|enabled|disabled|checked|unchecked|editable`), and `screenshot` (optional `name` for a named capture). Every step may set its own `timeout=<n>s`; the command also accepts a global `step-timeout=<n>s` (per step) and `timeout=<n>s` (total-script budget). Both are bounded and deterministic — the run stops cleanly on the first failing step, with no silent partial success.

Expand Down Expand Up @@ -881,7 +881,7 @@ External sources are explicit and CI-safe. WP Codebox validates URL-shaped sourc
```json
{
"inputs": {
"extraPlugins": [
"extra_plugins": [
{
"source": "https://downloads.wordpress.org/plugin/bbpress.latest-stable.zip",
"pluginFile": "bbpress/bbpress.php",
Expand Down
6 changes: 3 additions & 3 deletions docs/portable-wp-codebox.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Recipes can declare generic heavyweight plugin runtime needs with `inputs.plugin
"schema": "wp-codebox/workspace-recipe/v1",
"runtime": { "wp": "7.0" },
"inputs": {
"extraPlugins": [
"extra_plugins": [
{
"source": "./plugins/page-builder",
"slug": "page-builder",
Expand Down Expand Up @@ -119,9 +119,9 @@ Recipes can declare generic heavyweight plugin runtime needs with `inputs.plugin

Execution order is stable:

1. Mount recipe workspaces and `extraPlugins`.
1. Mount recipe workspaces and `extra_plugins`.
2. Install recipe-declared mu-plugin loaders.
3. Activate `extraPlugins` in recipe order.
3. Activate `extra_plugins` in recipe order.
4. Run `pluginRuntime.setup` steps in declared order.
5. Run `pluginRuntime.healthProbes` in declared order.
6. Import site seeds, then run normal workflow steps.
Expand Down
2 changes: 1 addition & 1 deletion docs/transfer-readiness-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ generic substrate boundary.

Transfer checklist:

- Support caller-provided plugins through `extraPlugins` with explicit source,
- Support caller-provided plugins through `extra_plugins` with explicit source,
slug, plugin file, activation behavior, optional hash, and `loadAs` mode.
- Support caller-provided MU plugins through the same generic runtime dependency
path; prove load order and invocation through issue #317.
Expand Down
2 changes: 1 addition & 1 deletion examples/recipes/bench-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
}
},
"inputs": {
"extraPlugins": [
"extra_plugins": [
{
"source": "../bench-plugin",
"slug": "bench-plugin",
Expand Down
2 changes: 1 addition & 1 deletion examples/recipes/cookbook/codex-agent-smoke.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
]
},
"inputs": {
"extraPlugins": [
"extra_plugins": [
{
"source": "/path/to/agents-api",
"slug": "agents-api",
Expand Down
2 changes: 1 addition & 1 deletion examples/recipes/cookbook/headless-browser-agent-task.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
}
},
"inputs": {
"extraPlugins": [
"extra_plugins": [
{
"source": "./headless-browser-agent-task-plugin",
"pluginFile": "headless-browser-agent-task-plugin/headless-browser-agent-task.php",
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/agent-task-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export interface AgentTaskRunOptions {

export interface AgentTaskRunInput {
goal?: string
task?: string
agent?: string
mode?: string
provider?: string
Expand Down Expand Up @@ -185,7 +184,7 @@ export function buildAgentTaskRecipe(input: AgentTaskRunInput, taskInput: Return
inputs: stripUndefined({
mounts: Array.isArray(input.mounts) ? input.mounts : [],
workspaces: Array.isArray(input.workspaces) ? input.workspaces : [],
extraPlugins: [
extra_plugins: [
...componentPlugins(input.component_contracts, artifacts),
...providerPlugins,
].filter(Boolean),
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/recipe-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface WordPressBenchBuilderOptions {
blueprint?: unknown
wordpressVersion?: string
mounts?: WorkspaceRecipeMount[]
extraPlugins?: WorkspaceRecipeExtraPlugin[]
extra_plugins?: WorkspaceRecipeExtraPlugin[]
componentId?: string
pluginSlug: string
iterations?: number
Expand Down Expand Up @@ -83,7 +83,7 @@ function buildRecipe(recipeType: RecipeBuildOptions["recipeType"], options: Word
blueprint: options.blueprint,
wordpressVersion: stringOrUndefined(options.wordpressVersion),
mounts: Array.isArray(options.mounts) ? options.mounts : [],
extraPlugins: Array.isArray((options as WordPressBenchBuilderOptions).extraPlugins) ? (options as WordPressBenchBuilderOptions).extraPlugins : [],
extra_plugins: Array.isArray((options as WordPressBenchBuilderOptions).extra_plugins) ? (options as WordPressBenchBuilderOptions).extra_plugins : [],
componentId: stringOrUndefined((options as WordPressBenchBuilderOptions).componentId),
pluginSlug: requiredString(options.pluginSlug, "pluginSlug"),
iterations: integerOrUndefined((options as WordPressBenchBuilderOptions).iterations),
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/recipe-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,7 +922,7 @@ function titleFromSlug(slug: string): string {
}

export function recipeExtraPlugins(recipe: WorkspaceRecipe): WorkspaceRecipeExtraPlugin[] {
return recipe.inputs?.extra_plugins ?? recipe.inputs?.extraPlugins ?? []
return recipe.inputs?.extra_plugins ?? []
}

export function recipeSource(sourceRef: string, expectedSha256?: string): ParsedRecipeSource {
Expand Down
14 changes: 6 additions & 8 deletions packages/cli/src/recipe-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,6 @@ export function parseWorkspaceRecipeJson(raw: string): WorkspaceRecipe {
}

export function normalizeWorkspaceRecipeCompatibility(recipe: WorkspaceRecipe): WorkspaceRecipe {
if (recipe.inputs?.extraPlugins !== undefined) {
recipe.inputs.extra_plugins ??= recipe.inputs.extraPlugins
delete recipe.inputs.extraPlugins
}

return recipe
}

Expand Down Expand Up @@ -99,6 +94,10 @@ export function validateWorkspaceRecipeShape(recipe: WorkspaceRecipe, recipePath
validateRecipeRuntimePreview(recipe.runtime?.preview, recipePath)
validateRecipeMounts(recipe.inputs?.mounts, "mounts", recipePath)

if (recipe.inputs && "extraPlugins" in recipe.inputs) {
throw new Error(`Recipe inputs.extraPlugins is unsupported; use inputs.extra_plugins: ${recipePath}`)
}

const workspaces = recipe.inputs?.workspaces ?? []
if (!Array.isArray(workspaces)) {
throw new Error(`Recipe workspaces must be an array: ${recipePath}`)
Expand Down Expand Up @@ -1086,10 +1085,9 @@ async function validateRecipeStepArgs(step: WorkspaceRecipe["workflow"]["steps"]

if (step.command === "wordpress.browser-actions") {
const stepsJson = recipeStepArgValue(step.args ?? [], "steps-json")
const actionsJson = recipeStepArgValue(step.args ?? [], "actions-json")
const url = recipeStepArgValue(step.args ?? [], "url")?.trim()
if (!stepsJson && !actionsJson && !url) {
addIssue("missing-steps", `${path}.args`, "wordpress.browser-actions requires steps-json=<array> (or actions-json=<array>) or url=<path-or-url>.")
if (!stepsJson && !url) {
addIssue("missing-steps", `${path}.args`, "wordpress.browser-actions requires steps-json=<array> or url=<path-or-url>.")
}

if (stepsJson && !stepsJson.startsWith("@")) {
Expand Down
1 change: 0 additions & 1 deletion packages/runtime-core/src/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,6 @@ export const commandRegistry = [
acceptedArgs: [
{ name: "url", description: "Initial preview path or absolute URL to visit when the script omits an initial navigate step.", format: "path or URL" },
{ name: "steps-json", description: "Ordered interaction script: navigate, click, fill, type, press, drag, hover, select, waitFor, evaluate, expect, screenshot, and capture steps. waitFor and screenshot steps support generic painted-readiness waits: painted, frame-painted:<iframe-selector>, and frame-url-painted:<url-fragment>.", format: "JSON array (inline or @<path>)" },
{ name: "actions-json", description: "Back-compat alias for steps-json accepting the legacy navigate/click/fill/press/wait/capture action shape.", format: "JSON array" },
{ name: "step-timeout", description: "Per-step timeout applied to each interaction step.", format: "duration, e.g. 5s or 500ms" },
{ name: "timeout", description: "Total-script timeout bounding the whole interaction run.", format: "duration, e.g. 30s or 1500ms" },
{ name: "auth", description: "Optional in-memory browser authentication mode. Use wordpress-admin to bootstrap WordPress admin cookies from PHP without writing token-bearing storage-state artifacts.", format: "wordpress-admin" },
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-core/src/recipe-builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface WordPressBenchRecipeOptions {
wordpressVersion?: string
blueprint?: unknown
mounts?: WorkspaceRecipeMount[]
extraPlugins?: WorkspaceRecipeExtraPlugin[]
extra_plugins?: WorkspaceRecipeExtraPlugin[]
componentId?: string
pluginSlug: string
iterations?: number
Expand Down Expand Up @@ -116,7 +116,7 @@ export function buildWordPressBenchRecipe(options: WordPressBenchRecipeOptions):
blueprint: blueprintWithWpConfigDefines(options.blueprint ?? {}, options.wpConfigDefines ?? {}),
},
inputs: {
extraPlugins: normalizeExtraPlugins(options.extraPlugins),
extra_plugins: normalizeExtraPlugins(options.extra_plugins),
mounts: normalizeRecipeMounts(options.mounts, { defaultMode: "readonly" }),
},
workflow: {
Expand Down
4 changes: 0 additions & 4 deletions packages/runtime-core/src/recipe-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,6 @@ export function createWorkspaceRecipeJsonSchema(options: WorkspaceRecipeJsonSche
type: "array",
items: { $ref: "#/$defs/extraPlugin" },
},
extraPlugins: {
type: "array",
items: { $ref: "#/$defs/extraPlugin" },
},
secretEnv: {
type: "array",
items: { type: "string", pattern: "^[A-Z_][A-Z0-9_]*$" },
Expand Down
6 changes: 3 additions & 3 deletions packages/runtime-core/src/runtime-action-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ async function runRuntimeFilesystemAction(
}

async function runRuntimeBrowserAction(episode: RuntimeEpisode, action: RuntimeBrowserAction): Promise<RuntimeActionObservation> {
const args = [`actions-json=${JSON.stringify([runtimeBrowserCommandAction(action)])}`]
const args = [`steps-json=${JSON.stringify([runtimeBrowserCommandStep(action)])}`]
if (action.url && action.operation !== "navigate") {
args.unshift(`url=${action.url}`)
}
Expand Down Expand Up @@ -324,8 +324,8 @@ async function runRuntimeBrowserAction(episode: RuntimeEpisode, action: RuntimeB
})
}

function runtimeBrowserCommandAction(action: RuntimeBrowserAction): Record<string, unknown> {
const commandAction: Record<string, unknown> = { type: action.operation }
function runtimeBrowserCommandStep(action: RuntimeBrowserAction): Record<string, unknown> {
const commandAction: Record<string, unknown> = { kind: action.operation === "wait" ? "waitFor" : action.operation }
for (const key of ["url", "selector", "text", "value", "key", "duration"] as const) {
if (typeof action[key] === "string") {
commandAction[key] = action[key]
Expand Down
1 change: 0 additions & 1 deletion packages/runtime-core/src/runtime-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,6 @@ export interface WorkspaceRecipe {
workspaces?: WorkspaceRecipeWorkspace[]
mounts?: WorkspaceRecipeMount[]
extra_plugins?: WorkspaceRecipeExtraPlugin[]
extraPlugins?: WorkspaceRecipeExtraPlugin[]
secretEnv?: string[]
pluginRuntime?: WorkspaceRecipePluginRuntime
fixtureDatabases?: WorkspaceRecipeFixtureDatabase[]
Expand Down
25 changes: 9 additions & 16 deletions packages/runtime-core/src/sandbox-tool-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ export function validateSandboxToolPolicySnapshot(input: unknown): SandboxToolPo
}
const id = typeof tool.id === "string" ? tool.id.trim() : ""
const runtimeToolId = typeof tool.runtime_tool_id === "string" ? tool.runtime_tool_id.trim() : ""
const location = typeof tool.execution_location === "string" ? tool.execution_location.trim() : ""
const visibility = typeof tool.transport_visibility === "string" ? tool.transport_visibility.trim() : ""
const runtime = isPlainObject(tool.runtime) ? tool.runtime : {}
const runtimeEnvironment = typeof runtime[AGENTS_API_RUNTIME_ENVIRONMENT] === "string" ? runtime[AGENTS_API_RUNTIME_ENVIRONMENT].trim() : ""
const runtimeCapabilityScope = typeof runtime[AGENTS_API_RUNTIME_CAPABILITY_SCOPE] === "string" ? runtime[AGENTS_API_RUNTIME_CAPABILITY_SCOPE].trim() : ""
if (!id) {
issues.push({ code: "invalid-tool", field: `${field}.id`, message: `${field}.id must be a non-empty string.` })
} else if (seen.has(id)) {
Expand All @@ -109,11 +110,11 @@ export function validateSandboxToolPolicySnapshot(input: unknown): SandboxToolPo
if (!runtimeToolId) {
issues.push({ code: "invalid-tool", field: `${field}.runtime_tool_id`, message: `${field}.runtime_tool_id must be a non-empty string.` })
}
if (!location) {
issues.push({ code: "invalid-tool", field: `${field}.execution_location`, message: `${field}.execution_location must be a non-empty string.` })
if (!runtimeEnvironment) {
issues.push({ code: "invalid-tool", field: `${field}.runtime.environment`, message: `${field}.runtime.environment must be a non-empty string.` })
}
if (!visibility) {
issues.push({ code: "invalid-tool", field: `${field}.transport_visibility`, message: `${field}.transport_visibility must be a non-empty string.` })
if (!runtimeCapabilityScope) {
issues.push({ code: "invalid-tool", field: `${field}.runtime.capability_scope`, message: `${field}.runtime.capability_scope must be a non-empty string.` })
}
if (typeof tool.allowed !== "boolean") {
issues.push({ code: "invalid-tool", field: `${field}.allowed`, message: `${field}.allowed must be boolean.` })
Expand All @@ -140,21 +141,13 @@ export function sandboxToolRuntimeMetadata(tool: SandboxToolPolicyTool): Require
const runtime = isPlainObject(tool.runtime) ? tool.runtime : {}
const environment = typeof runtime[AGENTS_API_RUNTIME_ENVIRONMENT] === "string"
? runtime[AGENTS_API_RUNTIME_ENVIRONMENT]
: legacyExecutionEnvironment(tool.execution_location)
: ""
const capabilityScope = typeof runtime[AGENTS_API_RUNTIME_CAPABILITY_SCOPE] === "string"
? runtime[AGENTS_API_RUNTIME_CAPABILITY_SCOPE]
: legacyCapabilityScope(tool.transport_visibility)
: ""

return {
environment,
capability_scope: capabilityScope,
}
}

function legacyExecutionEnvironment(location: string): AgentsApiRuntimeEnvironment {
return location === "sandbox" ? AGENTS_API_RUNTIME_LOCAL : AGENTS_API_CONTROL_PLANE
}

function legacyCapabilityScope(visibility: string): AgentsApiRuntimeEnvironment {
return ["sandbox", "both"].includes(visibility) ? AGENTS_API_RUNTIME_LOCAL : AGENTS_API_CONTROL_PLANE
}
16 changes: 6 additions & 10 deletions packages/runtime-core/src/task-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { SandboxToolPolicySnapshot } from "./sandbox-tool-policy.js"
export type TaskTargetKind = "repo" | "site" | "plugin" | "theme" | (string & {})

export const TASK_INPUT_SCHEMA = "wp-codebox/task-input/v1" as const
export const AGENTS_API_TASK_INPUT_SCHEMA = "agents-api/task-input/v1" as const
export const TASK_INPUT_VERSION = 1 as const

export interface TaskTarget {
Expand All @@ -31,7 +30,7 @@ export interface TaskInputAgentBundle {
}

export interface TaskInput {
schema: typeof TASK_INPUT_SCHEMA | typeof AGENTS_API_TASK_INPUT_SCHEMA
schema: typeof TASK_INPUT_SCHEMA
version: typeof TASK_INPUT_VERSION
goal: string
target: Partial<TaskTarget>
Expand All @@ -45,16 +44,14 @@ export interface TaskInput {

export type TaskInputRequest = Partial<Omit<TaskInput, "schema" | "version" | "goal">> & {
goal?: string
task?: string
sandboxToolPolicy?: SandboxToolPolicySnapshot
}

export const TASK_INPUT_JSON_SCHEMA = {
$id: TASK_INPUT_SCHEMA,
type: "object",
required: ["schema", "version", "goal", "target", "allowed_tools", "expected_artifacts", "agent_bundles", "sandbox_tool_policy", "policy", "context"],
properties: {
schema: { enum: [TASK_INPUT_SCHEMA, AGENTS_API_TASK_INPUT_SCHEMA], description: "Task input contract schema id. The legacy WP Codebox schema remains accepted alongside the generic Agents API schema." },
schema: { const: TASK_INPUT_SCHEMA, description: "Task input contract schema id." },
version: { const: TASK_INPUT_VERSION, description: "Task input contract version." },
goal: { type: "string", description: "User-facing outcome the sandboxed coding agent should accomplish." },
target: {
Expand Down Expand Up @@ -109,9 +106,8 @@ export const TASK_INPUT_JSON_SCHEMA = {
} as const

export function normalizeTaskInput(input: TaskInputRequest): TaskInput {
const goal = String(input.goal ?? input.task ?? "").trim()
if (goal === "") throw new Error("goal or task is required.")
const rawPolicy = (input as Record<string, unknown>).sandbox_tool_policy ?? (input as Record<string, unknown>).sandboxToolPolicy
const goal = String(input.goal ?? "").trim()
if (goal === "") throw new Error("goal is required.")

return {
schema: TASK_INPUT_SCHEMA,
Expand All @@ -120,8 +116,8 @@ export function normalizeTaskInput(input: TaskInputRequest): TaskInput {
target: isPlainObject(input.target) ? input.target : {},
allowed_tools: stringList(input.allowed_tools),
expected_artifacts: stringList(input.expected_artifacts),
agent_bundles: normalizeAgentBundles((input as Record<string, unknown>).agent_bundles ?? (input as Record<string, unknown>).agentBundles),
sandbox_tool_policy: isPlainObject(rawPolicy) ? rawPolicy as unknown as SandboxToolPolicySnapshot : {},
agent_bundles: normalizeAgentBundles(input.agent_bundles),
sandbox_tool_policy: isPlainObject(input.sandbox_tool_policy) ? input.sandbox_tool_policy as unknown as SandboxToolPolicySnapshot : {},
policy: isPlainObject(input.policy) ? input.policy : {},
context: isPlainObject(input.context) ? input.context : {},
}
Expand Down
Loading