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
62 changes: 41 additions & 21 deletions .github/scripts/run-agent-task/execute-native-agent-task.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand All @@ -389,20 +391,25 @@ 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)

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)
Expand All @@ -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 {
Expand All @@ -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 = {
Expand All @@ -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 } : {}),
}

Expand Down
11 changes: 11 additions & 0 deletions tests/agent-task-reusable-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>, environment: Record<string, string>) => {
Expand Down
10 changes: 10 additions & 0 deletions tests/execute-native-agent-task-lifecycle.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
`)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Loading