Skip to content

Commit b236c52

Browse files
authored
Canonicalize generated agent task mount paths (#1801)
1 parent f2dd340 commit b236c52

3 files changed

Lines changed: 50 additions & 26 deletions

File tree

packages/cli/src/agent-sandbox.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ export async function recipeExecutionSpec(step: WorkspaceRecipe["workflow"]["ste
119119
const originalArgs = step.args ?? []
120120
const resolvedStep = { ...step, args: rewriteRecipeExecutionArgs(step.command, originalArgs, options.inputMountPathMap) }
121121
const finish = (spec: ExecutionSpec & { args?: string[] }): ResolvedRecipeExecutionSpec => {
122-
const resolvedArgs = spec.args ?? []
122+
// Commands can generate PHP and serialized payloads after their source args
123+
// are resolved, so canonicalize the generated execution spec as well.
124+
const resolvedArgs = rewriteInputMountPathArgs(spec.args ?? [], options.inputMountPathMap)
123125
assertResolvedInputMountPathArgs(resolvedArgs, options.inputMountPathMap, `Recipe command ${step.command}`)
124126
return {
125127
...spec,

packages/cli/src/input-mount-paths.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,7 @@ export function rewriteInputMountPathArgs(args: readonly string[] = [], mappings
1919
if (mappings.length === 0) {
2020
return [...args]
2121
}
22-
return args.map((arg) => {
23-
const separator = arg.indexOf("=")
24-
if (separator <= 0) {
25-
return arg
26-
}
27-
const value = arg.slice(separator + 1)
28-
if (!value.startsWith("/")) {
29-
return arg
30-
}
31-
const rewritten = rewriteInputMountPath(value, mappings)
32-
return rewritten === value ? arg : `${arg.slice(0, separator + 1)}${rewritten}`
33-
})
22+
return args.map((arg) => rewriteInputMountPathReferences(arg, mappings))
3423
}
3524

3625
export function rewriteInputMountPathJsonArgs(args: readonly string[] = [], names: readonly string[] = [], mappings: readonly InputMountPathMapping[] = []): string[] {
@@ -95,8 +84,14 @@ function rewriteInputMountPathsInJsonValue(value: unknown, mappings: readonly In
9584
return value
9685
}
9786

98-
function originalPathReferencePattern(path: string): RegExp {
99-
return new RegExp(`${escapeRegExp(path)}(?=$|[\\/\\s'"\\]\\}\\),:;])`)
87+
function rewriteInputMountPathReferences(value: string, mappings: readonly InputMountPathMapping[]): string {
88+
return [...mappings]
89+
.sort((a, b) => b.originalTarget.length - a.originalTarget.length)
90+
.reduce((rewritten, mapping) => rewritten.replace(originalPathReferencePattern(mapping.originalTarget, "g"), mapping.canonicalTarget), value)
91+
}
92+
93+
function originalPathReferencePattern(path: string, flags = ""): RegExp {
94+
return new RegExp(`${escapeRegExp(path)}(?=$|[\\/\\s'"\\]\\}\\),:;])`, flags)
10095
}
10196

10297
// Mounts targeting the WordPress install tree (ABSPATH is `/wordpress/` in the

tests/recipe-runtime-setup-staged-materialization.test.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"
33
import { tmpdir } from "node:os"
44
import { join } from "node:path"
55

6+
import { recipeExecutionSpec } from "../packages/cli/src/agent-sandbox.js"
67
import { applyRecipeRuntimeSetup, assertResolvedInputMountPathArgs, recipeInputMountPathMap, rewriteInputMountPathArgs, type PreparedRecipeRuntimeSetup } from "../packages/cli/src/commands/recipe-runtime-setup.js"
78
import { executeRecipeWorkflowStep } from "../packages/cli/src/commands/recipe-run-workflow-evidence.js"
89
import type { ExecutionSpec, Runtime, WorkspaceRecipe } from "../packages/runtime-core/src/public.js"
@@ -201,17 +202,43 @@ assert.throws(
201202
/still references original input mount target.*\/home\/wpcom\/public_html/s,
202203
)
203204

204-
await assert.rejects(
205-
() => executeRecipeWorkflowStep(workflowRuntime, {
206-
phase: "steps",
207-
index: 1,
208-
step: {
209-
command: "wordpress.run-php",
210-
args: ["code=require '/home/wpcom/public_html/bin/tests/i18n-tools/bootstrap.php';"],
211-
},
212-
}, process.cwd(), undefined, undefined, undefined, wpcomPathMap),
213-
/still references original input mount target.*\/home\/wpcom\/public_html/s,
214-
)
205+
const agentMountPathMap = recipeInputMountPathMap({
206+
schema: "wp-codebox/workspace-recipe/v1",
207+
inputs: {
208+
mounts: [{ source: "/workspace/example", target: "/workspace/example", mode: "readonly" }],
209+
},
210+
workflow: { steps: [] },
211+
})
212+
const agentRuntimeTask = {
213+
bootstrap: "require '/workspace/example/bootstrap.php';",
214+
nested: { source: "/workspace/example/task.json" },
215+
}
216+
const rewrittenAgentTaskArg = rewriteInputMountPathArgs([`runtime-task-json=${JSON.stringify(agentRuntimeTask)}`], agentMountPathMap)[0]
217+
assert.deepEqual(JSON.parse(rewrittenAgentTaskArg.slice("runtime-task-json=".length)), {
218+
bootstrap: `require '${agentMountPathMap[0].canonicalTarget}/bootstrap.php';`,
219+
nested: { source: `${agentMountPathMap[0].canonicalTarget}/task.json` },
220+
}, "serialized agent task payloads rewrite embedded input mount references")
221+
222+
const agentSandboxSpec = await recipeExecutionSpec({
223+
command: "wp-codebox.agent-sandbox-run",
224+
args: [
225+
"task=Inspect /workspace/example/task.json",
226+
"code=require '/workspace/example/bootstrap.php';",
227+
rewrittenAgentTaskArg,
228+
],
229+
}, process.cwd(), undefined, { inputMountPathMap: agentMountPathMap })
230+
assert.equal(agentSandboxSpec.args?.some((arg) => arg.includes("/workspace/example")), false, "generated agent sandbox bootstrap uses canonical input mount paths")
231+
assert.match(agentSandboxSpec.args?.[0] ?? "", new RegExp(agentMountPathMap[0].canonicalTarget.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")))
232+
233+
const embeddedPhpExecution = await executeRecipeWorkflowStep(workflowRuntime, {
234+
phase: "steps",
235+
index: 1,
236+
step: {
237+
command: "wordpress.run-php",
238+
args: ["code=require '/home/wpcom/public_html/bin/tests/i18n-tools/bootstrap.php';"],
239+
},
240+
}, process.cwd(), undefined, undefined, undefined, wpcomPathMap)
241+
assert.deepEqual(embeddedPhpExecution.args, [`code=require '${wpcomPathMap[0].canonicalTarget}/bin/tests/i18n-tools/bootstrap.php';`])
215242

216243
executedWorkflowSpecs.length = 0
217244
const nestedWorkloadExecution = await executeRecipeWorkflowStep(workflowRuntime, {

0 commit comments

Comments
 (0)