Skip to content

Commit 0227ca7

Browse files
authored
Preserve verification failure evidence (#1890)
1 parent 12a5bb1 commit 0227ca7

3 files changed

Lines changed: 58 additions & 3 deletions

File tree

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,19 @@ function underRoot(root, path) {
589589
return contained !== ".." && !contained.startsWith(`..${String.fromCharCode(47)}`) && !isAbsolute(contained)
590590
}
591591

592+
async function appliedWorkspaceEvidence(refs, artifactsPath) {
593+
const root = await realpath(resolve(artifactsPath))
594+
const evidence = {}
595+
for (const [kind, key] of [["codebox-patch", "patch"], ["codebox-changed-files", "changed_files"]]) {
596+
const ref = refs.find((entry) => entry.kind === kind && typeof entry.path === "string" && entry.path)
597+
if (!ref) continue
598+
const path = await realpath(resolve(root, ref.path)).catch((error) => error?.code === "ENOENT" ? "" : Promise.reject(error))
599+
if (!path || !underRoot(root, path)) continue
600+
evidence[key] = { artifact_path: relative(root, path).replaceAll("\\", "/") }
601+
}
602+
return Object.keys(evidence).length === 2 ? evidence : undefined
603+
}
604+
592605
async function canonicalReviewerTranscript(nativeRuntimeResult, artifactsPath) {
593606
const publicCore = await import(pathToFileURL(join(codeboxRoot, "packages/runtime-core/dist/public.js")).href)
594607
const refs = publicCore.normalizePublicArtifactRefDTOs(nativeRuntimeResult)
@@ -792,6 +805,7 @@ if (toolObservability) {
792805
}
793806
assertNoRuntimeSourcePaths(runtimeResult, privateRuntimeSourceRootForSanitization)
794807
let workspaceApply = { status: "no-op", changedFiles: [] }
808+
let workspaceEvidence
795809
let runnerWorkspaceCore = null
796810
let downstreamFailure = null
797811
if (execution.code === 0 && runtimeResult.success === true && request.runner_workspace?.enabled) {
@@ -800,6 +814,7 @@ if (execution.code === 0 && runtimeResult.success === true && request.runner_wor
800814
const publicCore = await import(pathToFileURL(join(codeboxRoot, "packages/runtime-core/dist/public.js")).href)
801815
const refs = publicCore.normalizePublicArtifactRefDTOs(runtimeResult)
802816
.filter((ref) => ref.kind === "codebox-patch" || ref.kind === "codebox-changed-files")
817+
workspaceEvidence = await appliedWorkspaceEvidence(refs, artifactsPath)
803818
const trustedArtifacts = await trustedArtifactApplyRefs(trustedApplyArtifactRoot, refs)
804819
workspaceApply = await runnerWorkspaceCore.applyRunnerWorkspacePatch({ artifactRoot: trustedArtifacts.root, artifactRefs: trustedArtifacts.refs, workspaceRoot: workspace, writablePaths, seedIdentity: runnerWorkspaceSeedSnapshot?.provenance.identity })
805820
} catch (error) {
@@ -835,7 +850,21 @@ if (execution.code === 0 && request.run_agent && !request.dry_run && !downstream
835850
const verificationPassed = verification.every((check) => check.success)
836851
if (!verificationPassed) {
837852
const artifactError = verification.find((check) => check.artifact_error)?.artifact_error
838-
downstreamFailure ??= { stage: "verification", message: artifactError?.message || "Runner workspace verification did not pass.", ...(artifactError ? { artifact_error: artifactError } : {}) }
853+
const failedCheck = verification.find((check) => !check.success)
854+
const diagnostic = failedCheck ? {
855+
kind: failedCheck.kind,
856+
command: failedCheck.command,
857+
exit_code: failedCheck.exit_code,
858+
stdout_tail: bounded(failedCheck.stdout, MAX_WORKFLOW_OUTPUT_BYTES),
859+
stderr_tail: bounded(failedCheck.stderr, MAX_WORKFLOW_OUTPUT_BYTES),
860+
} : undefined
861+
downstreamFailure ??= {
862+
stage: "verification",
863+
message: artifactError?.message || "Runner workspace verification did not pass.",
864+
...(artifactError ? { artifact_error: artifactError } : {}),
865+
...(diagnostic ? { diagnostic } : {}),
866+
...(workspaceApply.status === "applied" && workspaceEvidence ? { evidence: workspaceEvidence } : {}),
867+
}
839868
}
840869
const runtimeRecord = record(runtimeResult)
841870
const agentResult = record(runtimeRecord.agent_task_run_result)

.github/scripts/run-agent-task/prepare-agent-task-upload.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,14 @@ function projectWorkflowResult(value) {
163163
const reviewerEvidence = record(result.reviewer_evidence)
164164
const verification = Array.isArray(result.verification) ? result.verification.slice(0, MAX_REVIEW_COLLECTION_ENTRIES).map((value) => {
165165
const check = record(value)
166-
return projectReviewValue(Object.fromEntries(["kind", "command", "description", "success", "exit_code", "stdout_truncated", "stderr_truncated", "artifact", "artifact_error"].flatMap((key) => check[key] === undefined ? [] : [[key, check[key]]])))
166+
const diagnostic = check.success === false ? {
167+
stdout_tail: check.stdout,
168+
stderr_tail: check.stderr,
169+
} : {}
170+
return projectReviewValue(Object.fromEntries(Object.entries({
171+
...Object.fromEntries(["kind", "command", "description", "success", "exit_code", "stdout_truncated", "stderr_truncated", "artifact", "artifact_error"].flatMap((key) => check[key] === undefined ? [] : [[key, check[key]]])),
172+
...diagnostic,
173+
}).filter(([, entry]) => entry !== undefined)))
167174
}) : undefined
168175
return projectReviewValue(Object.fromEntries(Object.entries({
169176
schema: result.schema,

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ async function run(mode = "success") {
3737
: mode === "artifact-oversized"
3838
? `echo verification >> .codebox/order && node -e 'require("node:fs").writeFileSync(".codebox/agent-task-artifacts/completion-report.json", Buffer.alloc(4 * 1024 * 1024 + 1, 120))'`
3939
: `echo verification >> .codebox/order && printf '%s\\n' '{"ok":true}' > .codebox/agent-task-artifacts/completion-report.json${mode === "artifact-command-fail" ? " && false" : mode === "artifact-readonly" ? " && chmod 444 .codebox/agent-task-artifacts/completion-report.json" : ""}`
40-
const verificationCommand = artifactModes.has(mode) && mode !== "artifact-drift" ? artifactCommand : (mode === "verify-fail" ? "false" : "echo verification >> .codebox/order")
40+
const verificationCommand = artifactModes.has(mode) && mode !== "artifact-drift" ? artifactCommand : (mode === "verify-fail" ? "printf 'verification stdout\\n'; printf 'verification stderr\\n' >&2; false" : "echo verification >> .codebox/order")
4141
const driftCommand = mode === "artifact-drift"
4242
? artifactCommand
4343
: mode === "artifact-replaced"
@@ -303,6 +303,25 @@ assert.equal(failedVerification.code, 1)
303303
assert(!failedVerification.order.includes("publish"))
304304
assert.equal(failedVerification.result.failure.stage, "verification")
305305
assert.equal(failedVerification.result.runtime_result.success, true, "verification failures retain the normalized runtime result")
306+
assert.equal(failedVerification.result.failure.diagnostic.stdout_tail, "verification stdout\n")
307+
assert.equal(failedVerification.result.failure.diagnostic.stderr_tail, "verification stderr\n")
308+
assert.equal(failedVerification.result.failure.evidence.patch.artifact_path, "files/patch.diff")
309+
assert.equal(failedVerification.result.failure.evidence.changed_files.artifact_path, "files/changed-files.json")
310+
await execFileAsync(process.execPath, [uploader], {
311+
cwd: failedVerification.workspace,
312+
env: {
313+
...process.env,
314+
AGENT_TASK_WORKSPACE: failedVerification.workspace,
315+
AGENT_TASK_REQUEST_PATH: join(failedVerification.workspace, ".codebox", "agent-task-request.json"),
316+
AGENT_TASK_UPLOAD_PATH: join(failedVerification.workspace, ".codebox", "agent-task-upload"),
317+
},
318+
})
319+
const uploadedFailedVerification = JSON.parse(await readFile(join(failedVerification.workspace, ".codebox", "agent-task-upload", ".codebox", "agent-task-workflow-result.json"), "utf8"))
320+
assert.equal(uploadedFailedVerification.verification.find((check) => !check.success).stdout_tail, "verification stdout\n")
321+
assert.equal(uploadedFailedVerification.verification.find((check) => !check.success).stderr_tail, "verification stderr\n")
322+
assert.equal(uploadedFailedVerification.failure.diagnostic.stderr_tail, "verification stderr\n")
323+
assert.match(await readFile(join(failedVerification.workspace, ".codebox", "agent-task-upload", ".codebox", "agent-task-artifacts", "apply-failure", "rejected.patch"), "utf8"), /-before/)
324+
assert.match(await readFile(join(failedVerification.workspace, ".codebox", "agent-task-upload", ".codebox", "agent-task-artifacts", "apply-failure", "changed-files.json"), "utf8"), /README\.md/)
306325

307326
const denied = await run("deny")
308327
assert.equal(denied.code, 1)

0 commit comments

Comments
 (0)