diff --git a/.github/scripts/run-agent-task/execute-native-agent-task.mjs b/.github/scripts/run-agent-task/execute-native-agent-task.mjs index 0c58897c0..548f3ffeb 100644 --- a/.github/scripts/run-agent-task/execute-native-agent-task.mjs +++ b/.github/scripts/run-agent-task/execute-native-agent-task.mjs @@ -367,9 +367,11 @@ const taskInput = { metadata: { workload: request.workload, ...(materializedPackage ? { imported_agent: materializedPackage.identity } : {}), runtime_sources: materializedRuntimeSources?.descriptors ?? [] }, }, }, - }, - 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/**"] } }] : [], - } + // agent-task-run unwraps task_input before building the recipe. Keep the + // runner seed on that canonical input rather than on the request envelope. + 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/**"] } }] : [], + }, + } await writeFile(executionInputPath, `${JSON.stringify(taskInput, null, 2)}\n`) @@ -389,12 +391,17 @@ const runtimeResult = sanitizeRuntimeSourceValue(nativeRuntimeResult, privateRun assertNoRuntimeSourcePaths(runtimeResult, privateRuntimeSourceRootForSanitization) let workspaceApply = { status: "no-op", changedFiles: [] } let runnerWorkspaceCore = null +let downstreamFailure = null if (execution.code === 0 && runtimeResult.success === true && request.runner_workspace?.enabled) { - runnerWorkspaceCore = await import(pathToFileURL(join(codeboxRoot, "packages/runtime-core/dist/runner-workspace-apply.js")).href) - const publicCore = await import(pathToFileURL(join(codeboxRoot, "packages/runtime-core/dist/public.js")).href) - const refs = publicCore.normalizePublicArtifactRefDTOs(runtimeResult) - .filter((ref) => ref.kind === "codebox-patch" || ref.kind === "codebox-changed-files") - workspaceApply = await runnerWorkspaceCore.applyRunnerWorkspacePatch({ artifactRoot: artifactsPath, artifactRefs: refs, workspaceRoot: workspace, writablePaths: String(request.writable_paths || "").split(",").map((value) => value.trim()).filter(Boolean) }) + try { + runnerWorkspaceCore = await import(pathToFileURL(join(codeboxRoot, "packages/runtime-core/dist/runner-workspace-apply.js")).href) + const publicCore = await import(pathToFileURL(join(codeboxRoot, "packages/runtime-core/dist/public.js")).href) + const refs = publicCore.normalizePublicArtifactRefDTOs(runtimeResult) + .filter((ref) => ref.kind === "codebox-patch" || ref.kind === "codebox-changed-files") + workspaceApply = await runnerWorkspaceCore.applyRunnerWorkspacePatch({ artifactRoot: artifactsPath, artifactRefs: refs, workspaceRoot: workspace, writablePaths: String(request.writable_paths || "").split(",").map((value) => value.trim()).filter(Boolean) }) + } catch (error) { + downstreamFailure = { stage: "apply", message: bounded(error instanceof Error ? error.message : String(error), MAX_OUTPUT_CHARS) } + } } await cleanupPrivateRuntimeSources() assertNoRuntimeSourcePaths(runtimeResult, privateRuntimeSourceRootForSanitization) @@ -402,7 +409,7 @@ assertNoRuntimeSourcePaths(runtimeResult, privateRuntimeSourceRootForSanitizatio await redactArtifactFiles(artifactsPath) const verification = [] -if (execution.code === 0 && request.run_agent && !request.dry_run) { +if (execution.code === 0 && request.run_agent && !request.dry_run && !downstreamFailure) { const validationDependencies = string(request.validation_dependencies) if (validationDependencies) { const checkResult = await command("bash", ["-lc", validationDependencies], workspace) @@ -419,21 +426,30 @@ if (execution.code === 0 && request.run_agent && !request.dry_run) { } const verificationPassed = verification.every((check) => check.success) +if (!verificationPassed) { + downstreamFailure ??= { stage: "verification", message: "Runner workspace verification did not pass." } +} const runtimeRecord = record(runtimeResult) const agentResult = record(runtimeRecord.agent_task_run_result) let publication = resultValue(runtimeRecord, "metadata.runner_workspace_publication") if (execution.code === 0 && runtimeRecord.success === true && verificationPassed && workspaceApply.status === "applied") { - await runnerWorkspaceCore.verifyRunnerWorkspaceIntegrity(workspaceApply.integrity) - const testPublisher = string(process.env.WP_CODEBOX_TEST_PUBLISHER_MODULE) - const publisher = testPublisher ? (await import(pathToFileURL(resolve(testPublisher)).href)).publishRunnerWorkspace : publishRunnerWorkspace - const testHook = testPublisher && process.env.WP_CODEBOX_TEST_PUBLISHER_HOOK - ? JSON.parse(process.env.WP_CODEBOX_TEST_PUBLISHER_HOOK) - : undefined - publication = await publisher({ request, changedFiles: workspaceApply.changedFiles, publicationFiles: workspaceApply.publicationFiles, token: process.env.ACCESS_TOKEN || process.env.GITHUB_TOKEN, ...(testHook ? { testHook } : {}) }) - runtimeRecord.metadata = { ...record(runtimeRecord.metadata), runner_workspace_publication: publication } - runtimeRecord.outputs = { ...record(runtimeRecord.outputs), runner_workspace_publication: publication } -} -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.") + try { + await runnerWorkspaceCore.verifyRunnerWorkspaceIntegrity(workspaceApply.integrity) + const testPublisher = string(process.env.WP_CODEBOX_TEST_PUBLISHER_MODULE) + const publisher = testPublisher ? (await import(pathToFileURL(resolve(testPublisher)).href)).publishRunnerWorkspace : publishRunnerWorkspace + const testHook = testPublisher && process.env.WP_CODEBOX_TEST_PUBLISHER_HOOK + ? JSON.parse(process.env.WP_CODEBOX_TEST_PUBLISHER_HOOK) + : undefined + publication = await publisher({ request, changedFiles: workspaceApply.changedFiles, publicationFiles: workspaceApply.publicationFiles, token: process.env.ACCESS_TOKEN || process.env.GITHUB_TOKEN, ...(testHook ? { testHook } : {}) }) + runtimeRecord.metadata = { ...record(runtimeRecord.metadata), runner_workspace_publication: publication } + runtimeRecord.outputs = { ...record(runtimeRecord.outputs), runner_workspace_publication: publication } + } catch (error) { + downstreamFailure = { stage: "publication", message: bounded(error instanceof Error ? error.message : String(error), MAX_OUTPUT_CHARS) } + } +} +if (request.success?.requires_pr === true && workspaceApply.status === "no-op" && !publication) { + downstreamFailure ??= { stage: "no-op", message: "success_requires_pr cannot succeed for a no-op runner workspace task." } +} let evaluatedProjections = {} let projectionError = "" try { @@ -446,8 +462,11 @@ const publicationVerification = publicationRequired && execution.code === 0 && r ? await verifyPublishedPullRequest(publication, request.target_repo, workspace) : { valid: !publicationRequired, error: "" } const publicationPassed = publicationVerification.valid +if (publicationRequired && !publicationPassed) { + downstreamFailure ??= { stage: "publication", message: publicationVerification.error || "Runner workspace publication did not pass verification." } +} const success = request.run_agent && !request.dry_run - ? execution.code === 0 && runtimeRecord.success === true && verificationPassed && publicationPassed && !projectionError + ? execution.code === 0 && runtimeRecord.success === true && verificationPassed && publicationPassed && !projectionError && !downstreamFailure : true const status = request.run_agent && !request.dry_run ? (success ? "succeeded" : "failed") : "skipped" const result = { @@ -470,6 +489,7 @@ const result = { 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 } }, ...(publicationRequired ? { publication_verification: publicationVerification } : {}), ...(publicationRequired && !publicationPassed ? { publication_error: "success_requires_pr requires a valid published runner-workspace pull request for target_repo." } : {}), + ...(downstreamFailure ? { failure: { code: "wp-codebox.agent-task.downstream", classification: "downstream", ...downstreamFailure } } : {}), ...(projectionError ? { projection_error: projectionError } : {}), } diff --git a/tests/agent-task-reusable-workflow.test.ts b/tests/agent-task-reusable-workflow.test.ts index 7ba385b37..020d34057 100644 --- a/tests/agent-task-reusable-workflow.test.ts +++ b/tests/agent-task-reusable-workflow.test.ts @@ -225,6 +225,17 @@ assert.deepEqual(nativeTaskInput.task_input.sandbox_tool_policy.tools, hostedDoc allowed: true, runtime: { environment: "runtime_local", capability_scope: "runtime_local" }, })), "Hosted Docs Agent run 29298164272 must emit canonical sandbox-local workspace metadata") +assert.deepEqual(nativeTaskInput.task_input.workspaces, [{ + target: "/workspace", + mode: "readwrite", + sourceMode: "repo-backed", + seed: { + type: "directory", + source: tmp, + excludePaths: [".git/**", ".codebox/**", "node_modules/**", "vendor/**", "dist/**", "build/**", "coverage/**", ".cache/**"], + }, +}], "Hosted failures 29324157852 and 29324563665 placed the runner seed outside task_input, so agent-task-run discarded it") +assert.equal(nativeTaskInput.workspaces, undefined, "Runner workspace configuration must only use the canonical task_input.workspaces field") const executeNativeAgentTask = new URL("../.github/scripts/run-agent-task/execute-native-agent-task.mjs", import.meta.url).pathname const executeAccessCase = async (candidate: Record, environment: Record) => { diff --git a/tests/execute-native-agent-task-lifecycle.test.mjs b/tests/execute-native-agent-task-lifecycle.test.mjs index ceeb5f679..98b2b1ab5 100644 --- a/tests/execute-native-agent-task-lifecycle.test.mjs +++ b/tests/execute-native-agent-task-lifecycle.test.mjs @@ -65,6 +65,7 @@ async function run(mode = "success") { import { mkdir, readFile, writeFile } from "node:fs/promises" import { join } from "node:path" const output = process.argv[process.argv.indexOf("--result-file") + 1] +const input = JSON.parse(await readFile(process.argv[process.argv.indexOf("--input-file") + 1], "utf8")) const root = join(process.cwd(), ".codebox", "agent-task-artifacts", "files") await mkdir(root, { recursive: true }) const patch = ${noOp} ? "" : "--- a/README.md\\n+++ b/README.md\\n@@ -1 +1 @@\\n-before\\n+after\\n" @@ -94,6 +95,7 @@ const result = { patch: { artifact: "patch.diff", bytes: ${noOp} ? 0 : 1 }, }, metadata: { imported_agent: { slug: "fixture-agent" }, tool_contract: { tools: ["workspace_read", "workspace_edit"] } }, + 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" }, } await writeFile(output, JSON.stringify(result)) `) @@ -163,10 +165,16 @@ assert.equal(success.code, 0, success.stderr) assert.equal(await readFile(join(success.workspace, "README.md"), "utf8"), "after\n") assert.match(success.order, /runtime\nvalidation\nverification\ndrift\npublish\n/) assert.equal(success.result.runtime_result.metadata.runner_workspace_publication.pull_request.url, "https://github.com/owner/repo/pull/1") +assert.equal(success.result.runtime_result.agent_runtime_diagnostics.prepared_paths.workspaces[0].target, "/workspace") +assert.equal(success.result.runtime_result.agent_runtime_diagnostics.prepared_paths.workspaces[0].mode, "readwrite") +assert.equal(success.result.runtime_result.agent_runtime_diagnostics.sandbox_workspace.mounts[0].target, "/workspace") +assert.equal(success.result.runtime_result.agent_runtime_diagnostics.local_executor_root, "/workspace") const failedVerification = await run("verify-fail") assert.equal(failedVerification.code, 1) assert(!failedVerification.order.includes("publish")) +assert.equal(failedVerification.result.failure.stage, "verification") +assert.equal(failedVerification.result.runtime_result.success, true, "verification failures retain the normalized runtime result") const denied = await run("deny") assert.equal(denied.code, 1) @@ -175,6 +183,8 @@ assert.equal(denied.order, "runtime\n") const noOpRequired = await run("no-op") assert.equal(noOpRequired.code, 1) assert(!noOpRequired.order.includes("publish")) +assert.equal(noOpRequired.result.failure.stage, "no-op") +assert.equal(noOpRequired.result.runtime_result.success, true, "downstream failures retain the normalized runtime result") const noOpMaintenance = await run("no-op-maintenance") assert.equal(noOpMaintenance.code, 0)