Skip to content

Commit 81d32d9

Browse files
authored
Propagate runner workspaces into agent tasks (#1787)
1 parent 11e680a commit 81d32d9

3 files changed

Lines changed: 62 additions & 21 deletions

File tree

.github/scripts/run-agent-task/execute-native-agent-task.mjs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,11 @@ const taskInput = {
367367
metadata: { workload: request.workload, ...(materializedPackage ? { imported_agent: materializedPackage.identity } : {}), runtime_sources: materializedRuntimeSources?.descriptors ?? [] },
368368
},
369369
},
370-
},
371-
workspaces: request.runner_workspace?.enabled ? [{ target: "/workspace", mode: "readwrite", sourceMode: "repo-backed", seed: { type: "directory", source: workspace, excludePaths: [".git/**", ".codebox/**", "node_modules/**", "vendor/**", "dist/**", "build/**", "coverage/**", ".cache/**"] } }] : [],
372-
}
370+
// agent-task-run unwraps task_input before building the recipe. Keep the
371+
// runner seed on that canonical input rather than on the request envelope.
372+
workspaces: request.runner_workspace?.enabled ? [{ target: "/workspace", mode: "readwrite", sourceMode: "repo-backed", seed: { type: "directory", source: workspace, excludePaths: [".git/**", ".codebox/**", "node_modules/**", "vendor/**", "dist/**", "build/**", "coverage/**", ".cache/**"] } }] : [],
373+
},
374+
}
373375

374376
await writeFile(executionInputPath, `${JSON.stringify(taskInput, null, 2)}\n`)
375377

@@ -389,20 +391,25 @@ const runtimeResult = sanitizeRuntimeSourceValue(nativeRuntimeResult, privateRun
389391
assertNoRuntimeSourcePaths(runtimeResult, privateRuntimeSourceRootForSanitization)
390392
let workspaceApply = { status: "no-op", changedFiles: [] }
391393
let runnerWorkspaceCore = null
394+
let downstreamFailure = null
392395
if (execution.code === 0 && runtimeResult.success === true && request.runner_workspace?.enabled) {
393-
runnerWorkspaceCore = await import(pathToFileURL(join(codeboxRoot, "packages/runtime-core/dist/runner-workspace-apply.js")).href)
394-
const publicCore = await import(pathToFileURL(join(codeboxRoot, "packages/runtime-core/dist/public.js")).href)
395-
const refs = publicCore.normalizePublicArtifactRefDTOs(runtimeResult)
396-
.filter((ref) => ref.kind === "codebox-patch" || ref.kind === "codebox-changed-files")
397-
workspaceApply = await runnerWorkspaceCore.applyRunnerWorkspacePatch({ artifactRoot: artifactsPath, artifactRefs: refs, workspaceRoot: workspace, writablePaths: String(request.writable_paths || "").split(",").map((value) => value.trim()).filter(Boolean) })
396+
try {
397+
runnerWorkspaceCore = await import(pathToFileURL(join(codeboxRoot, "packages/runtime-core/dist/runner-workspace-apply.js")).href)
398+
const publicCore = await import(pathToFileURL(join(codeboxRoot, "packages/runtime-core/dist/public.js")).href)
399+
const refs = publicCore.normalizePublicArtifactRefDTOs(runtimeResult)
400+
.filter((ref) => ref.kind === "codebox-patch" || ref.kind === "codebox-changed-files")
401+
workspaceApply = await runnerWorkspaceCore.applyRunnerWorkspacePatch({ artifactRoot: artifactsPath, artifactRefs: refs, workspaceRoot: workspace, writablePaths: String(request.writable_paths || "").split(",").map((value) => value.trim()).filter(Boolean) })
402+
} catch (error) {
403+
downstreamFailure = { stage: "apply", message: bounded(error instanceof Error ? error.message : String(error), MAX_OUTPUT_CHARS) }
404+
}
398405
}
399406
await cleanupPrivateRuntimeSources()
400407
assertNoRuntimeSourcePaths(runtimeResult, privateRuntimeSourceRootForSanitization)
401408

402409
await redactArtifactFiles(artifactsPath)
403410

404411
const verification = []
405-
if (execution.code === 0 && request.run_agent && !request.dry_run) {
412+
if (execution.code === 0 && request.run_agent && !request.dry_run && !downstreamFailure) {
406413
const validationDependencies = string(request.validation_dependencies)
407414
if (validationDependencies) {
408415
const checkResult = await command("bash", ["-lc", validationDependencies], workspace)
@@ -419,21 +426,30 @@ if (execution.code === 0 && request.run_agent && !request.dry_run) {
419426
}
420427

421428
const verificationPassed = verification.every((check) => check.success)
429+
if (!verificationPassed) {
430+
downstreamFailure ??= { stage: "verification", message: "Runner workspace verification did not pass." }
431+
}
422432
const runtimeRecord = record(runtimeResult)
423433
const agentResult = record(runtimeRecord.agent_task_run_result)
424434
let publication = resultValue(runtimeRecord, "metadata.runner_workspace_publication")
425435
if (execution.code === 0 && runtimeRecord.success === true && verificationPassed && workspaceApply.status === "applied") {
426-
await runnerWorkspaceCore.verifyRunnerWorkspaceIntegrity(workspaceApply.integrity)
427-
const testPublisher = string(process.env.WP_CODEBOX_TEST_PUBLISHER_MODULE)
428-
const publisher = testPublisher ? (await import(pathToFileURL(resolve(testPublisher)).href)).publishRunnerWorkspace : publishRunnerWorkspace
429-
const testHook = testPublisher && process.env.WP_CODEBOX_TEST_PUBLISHER_HOOK
430-
? JSON.parse(process.env.WP_CODEBOX_TEST_PUBLISHER_HOOK)
431-
: undefined
432-
publication = await publisher({ request, changedFiles: workspaceApply.changedFiles, publicationFiles: workspaceApply.publicationFiles, token: process.env.ACCESS_TOKEN || process.env.GITHUB_TOKEN, ...(testHook ? { testHook } : {}) })
433-
runtimeRecord.metadata = { ...record(runtimeRecord.metadata), runner_workspace_publication: publication }
434-
runtimeRecord.outputs = { ...record(runtimeRecord.outputs), runner_workspace_publication: publication }
435-
}
436-
if (request.success?.requires_pr === true && workspaceApply.status === "no-op" && !publication) throw new Error("success_requires_pr cannot succeed for a no-op runner workspace task.")
436+
try {
437+
await runnerWorkspaceCore.verifyRunnerWorkspaceIntegrity(workspaceApply.integrity)
438+
const testPublisher = string(process.env.WP_CODEBOX_TEST_PUBLISHER_MODULE)
439+
const publisher = testPublisher ? (await import(pathToFileURL(resolve(testPublisher)).href)).publishRunnerWorkspace : publishRunnerWorkspace
440+
const testHook = testPublisher && process.env.WP_CODEBOX_TEST_PUBLISHER_HOOK
441+
? JSON.parse(process.env.WP_CODEBOX_TEST_PUBLISHER_HOOK)
442+
: undefined
443+
publication = await publisher({ request, changedFiles: workspaceApply.changedFiles, publicationFiles: workspaceApply.publicationFiles, token: process.env.ACCESS_TOKEN || process.env.GITHUB_TOKEN, ...(testHook ? { testHook } : {}) })
444+
runtimeRecord.metadata = { ...record(runtimeRecord.metadata), runner_workspace_publication: publication }
445+
runtimeRecord.outputs = { ...record(runtimeRecord.outputs), runner_workspace_publication: publication }
446+
} catch (error) {
447+
downstreamFailure = { stage: "publication", message: bounded(error instanceof Error ? error.message : String(error), MAX_OUTPUT_CHARS) }
448+
}
449+
}
450+
if (request.success?.requires_pr === true && workspaceApply.status === "no-op" && !publication) {
451+
downstreamFailure ??= { stage: "no-op", message: "success_requires_pr cannot succeed for a no-op runner workspace task." }
452+
}
437453
let evaluatedProjections = {}
438454
let projectionError = ""
439455
try {
@@ -446,8 +462,11 @@ const publicationVerification = publicationRequired && execution.code === 0 && r
446462
? await verifyPublishedPullRequest(publication, request.target_repo, workspace)
447463
: { valid: !publicationRequired, error: "" }
448464
const publicationPassed = publicationVerification.valid
465+
if (publicationRequired && !publicationPassed) {
466+
downstreamFailure ??= { stage: "publication", message: publicationVerification.error || "Runner workspace publication did not pass verification." }
467+
}
449468
const success = request.run_agent && !request.dry_run
450-
? execution.code === 0 && runtimeRecord.success === true && verificationPassed && publicationPassed && !projectionError
469+
? execution.code === 0 && runtimeRecord.success === true && verificationPassed && publicationPassed && !projectionError && !downstreamFailure
451470
: true
452471
const status = request.run_agent && !request.dry_run ? (success ? "succeeded" : "failed") : "skipped"
453472
const result = {
@@ -470,6 +489,7 @@ const result = {
470489
access: { authorized: true, credential_mode: process.env.GITHUB_TOKEN ? "runner-access-token" : (process.env.OPENAI_API_KEY ? "runner-provider-credentials" : "runner-default-credentials"), policy: { allowed_repos: request.access.allowed_repos } },
471490
...(publicationRequired ? { publication_verification: publicationVerification } : {}),
472491
...(publicationRequired && !publicationPassed ? { publication_error: "success_requires_pr requires a valid published runner-workspace pull request for target_repo." } : {}),
492+
...(downstreamFailure ? { failure: { code: "wp-codebox.agent-task.downstream", classification: "downstream", ...downstreamFailure } } : {}),
473493
...(projectionError ? { projection_error: projectionError } : {}),
474494
}
475495

tests/agent-task-reusable-workflow.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,17 @@ assert.deepEqual(nativeTaskInput.task_input.sandbox_tool_policy.tools, hostedDoc
225225
allowed: true,
226226
runtime: { environment: "runtime_local", capability_scope: "runtime_local" },
227227
})), "Hosted Docs Agent run 29298164272 must emit canonical sandbox-local workspace metadata")
228+
assert.deepEqual(nativeTaskInput.task_input.workspaces, [{
229+
target: "/workspace",
230+
mode: "readwrite",
231+
sourceMode: "repo-backed",
232+
seed: {
233+
type: "directory",
234+
source: tmp,
235+
excludePaths: [".git/**", ".codebox/**", "node_modules/**", "vendor/**", "dist/**", "build/**", "coverage/**", ".cache/**"],
236+
},
237+
}], "Hosted failures 29324157852 and 29324563665 placed the runner seed outside task_input, so agent-task-run discarded it")
238+
assert.equal(nativeTaskInput.workspaces, undefined, "Runner workspace configuration must only use the canonical task_input.workspaces field")
228239

229240
const executeNativeAgentTask = new URL("../.github/scripts/run-agent-task/execute-native-agent-task.mjs", import.meta.url).pathname
230241
const executeAccessCase = async (candidate: Record<string, unknown>, environment: Record<string, string>) => {

tests/execute-native-agent-task-lifecycle.test.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ async function run(mode = "success") {
6565
import { mkdir, readFile, writeFile } from "node:fs/promises"
6666
import { join } from "node:path"
6767
const output = process.argv[process.argv.indexOf("--result-file") + 1]
68+
const input = JSON.parse(await readFile(process.argv[process.argv.indexOf("--input-file") + 1], "utf8"))
6869
const root = join(process.cwd(), ".codebox", "agent-task-artifacts", "files")
6970
await mkdir(root, { recursive: true })
7071
const patch = ${noOp} ? "" : "--- a/README.md\\n+++ b/README.md\\n@@ -1 +1 @@\\n-before\\n+after\\n"
@@ -94,6 +95,7 @@ const result = {
9495
patch: { artifact: "patch.diff", bytes: ${noOp} ? 0 : 1 },
9596
},
9697
metadata: { imported_agent: { slug: "fixture-agent" }, tool_contract: { tools: ["workspace_read", "workspace_edit"] } },
98+
agent_runtime_diagnostics: { prepared_paths: { workspaces: input.task_input.workspaces.map((workspace) => ({ target: workspace.target, mode: workspace.mode, metadata: { baselineSource: "fixture-baseline", workspaceRoot: "/workspace", sourceMode: workspace.sourceMode } })) }, sandbox_workspace: { mounts: input.task_input.workspaces.map((workspace) => ({ target: workspace.target })) }, local_executor_root: "/workspace" },
9799
}
98100
await writeFile(output, JSON.stringify(result))
99101
`)
@@ -163,10 +165,16 @@ assert.equal(success.code, 0, success.stderr)
163165
assert.equal(await readFile(join(success.workspace, "README.md"), "utf8"), "after\n")
164166
assert.match(success.order, /runtime\nvalidation\nverification\ndrift\npublish\n/)
165167
assert.equal(success.result.runtime_result.metadata.runner_workspace_publication.pull_request.url, "https://github.com/owner/repo/pull/1")
168+
assert.equal(success.result.runtime_result.agent_runtime_diagnostics.prepared_paths.workspaces[0].target, "/workspace")
169+
assert.equal(success.result.runtime_result.agent_runtime_diagnostics.prepared_paths.workspaces[0].mode, "readwrite")
170+
assert.equal(success.result.runtime_result.agent_runtime_diagnostics.sandbox_workspace.mounts[0].target, "/workspace")
171+
assert.equal(success.result.runtime_result.agent_runtime_diagnostics.local_executor_root, "/workspace")
166172

167173
const failedVerification = await run("verify-fail")
168174
assert.equal(failedVerification.code, 1)
169175
assert(!failedVerification.order.includes("publish"))
176+
assert.equal(failedVerification.result.failure.stage, "verification")
177+
assert.equal(failedVerification.result.runtime_result.success, true, "verification failures retain the normalized runtime result")
170178

171179
const denied = await run("deny")
172180
assert.equal(denied.code, 1)
@@ -175,6 +183,8 @@ assert.equal(denied.order, "runtime\n")
175183
const noOpRequired = await run("no-op")
176184
assert.equal(noOpRequired.code, 1)
177185
assert(!noOpRequired.order.includes("publish"))
186+
assert.equal(noOpRequired.result.failure.stage, "no-op")
187+
assert.equal(noOpRequired.result.runtime_result.success, true, "downstream failures retain the normalized runtime result")
178188

179189
const noOpMaintenance = await run("no-op-maintenance")
180190
assert.equal(noOpMaintenance.code, 0)

0 commit comments

Comments
 (0)