Skip to content

Commit 06a4f55

Browse files
authored
Materialize immutable native runtime sources (#1768)
* Materialize immutable native runtime sources * Harden runtime source isolation * Support immutable runtime ZIP artifacts * Harden immutable runtime source isolation
1 parent 988e2f2 commit 06a4f55

16 files changed

Lines changed: 759 additions & 22 deletions

.github/scripts/run-agent-task/build-codebox-task-request.mjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { mkdirSync, writeFileSync, appendFileSync } from "node:fs"
2-
import { normalizeExternalPackageSource, parseExternalPackageSourcePolicy } from "./materialize-external-native-package.mjs"
2+
import { normalizeExternalPackageSource, normalizeRuntimeSources, parseExternalPackageSourcePolicy } from "./materialize-external-native-package.mjs"
33

44
function parseJson(name, fallback, expected) {
55
const raw = process.env[name]
@@ -64,9 +64,11 @@ function commandList(name) {
6464
}
6565

6666
const artifactDeclarations = parseJson("ARTIFACT_DECLARATIONS", [], "array")
67+
const externalPackagePolicy = parseExternalPackageSourcePolicy(requiredString("EXTERNAL_PACKAGE_SOURCE_POLICY"))
6768
const request = {
6869
schema: "wp-codebox/agent-task-workflow-request/v1",
69-
external_package_source: normalizeExternalPackageSource(parseJson("EXTERNAL_PACKAGE_SOURCE", {}, "object"), parseExternalPackageSourcePolicy(requiredString("EXTERNAL_PACKAGE_SOURCE_POLICY"))),
70+
external_package_source: normalizeExternalPackageSource(parseJson("EXTERNAL_PACKAGE_SOURCE", {}, "object"), externalPackagePolicy),
71+
runtime_sources: normalizeRuntimeSources(parseJson("RUNTIME_SOURCES", [], "array"), externalPackagePolicy),
7072
workload: {
7173
id: process.env.WORKLOAD_ID || "agent-task",
7274
label: process.env.WORKLOAD_LABEL || "Run Agent Task",

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

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { rmSync } from "node:fs"
12
import { appendFile, mkdir, readFile, rm, writeFile } from "node:fs/promises"
23
import { join, resolve } from "node:path"
34
import { spawn } from "node:child_process"
4-
import { materializeExternalNativePackage, normalizeExternalPackageSource, parseExternalPackageSourcePolicy } from "./materialize-external-native-package.mjs"
5+
import { materializeExternalNativePackage, materializeRuntimeSources, normalizeExternalPackageSource, normalizeRuntimeSources, parseExternalPackageSourcePolicy } from "./materialize-external-native-package.mjs"
56
import { readNativeResult } from "./native-result-file.mjs"
67

78
const requestPath = process.env.AGENT_TASK_REQUEST_PATH || ".codebox/agent-task-request.json"
@@ -12,6 +13,7 @@ const outputPath = process.env.GITHUB_OUTPUT
1213
const MAX_CAPTURE_BYTES = 32768
1314
const MAX_OUTPUT_CHARS = 8192
1415
const secretValues = ["OPENAI_API_KEY", "MODEL_PROVIDER_SECRET_1", "MODEL_PROVIDER_SECRET_2", "MODEL_PROVIDER_SECRET_3", "MODEL_PROVIDER_SECRET_4", "MODEL_PROVIDER_SECRET_5", "GITHUB_TOKEN", "GH_TOKEN", "ACCESS_TOKEN", "EXTERNAL_PACKAGE_SOURCE_POLICY"].map((name) => process.env[name]).filter(Boolean)
16+
const PRIVATE_RUNTIME_PATH_FIELDS = new Set(["source", "path", "sourceRoot", "originalSource", "preparedPath", "requestedPath", "source_package_root", "artifacts_path", "runtime_input_path", "task_path", "result_path", "event_stream_path", "materialization_result_path"])
1517
function redact(value) {
1618
if (typeof value === "string") return secretValues.reduce((output, secret) => output.split(secret).join("[REDACTED]"), value)
1719
if (Array.isArray(value)) return value.map(redact)
@@ -94,6 +96,27 @@ function record(value) {
9496
return value && typeof value === "object" && !Array.isArray(value) ? value : {}
9597
}
9698

99+
function isPrivateRuntimePath(value) {
100+
if (!privateRuntimeSourceRoot || typeof value !== "string") return false
101+
const path = resolve(value)
102+
return path === privateRuntimeSourceRoot || path.startsWith(`${privateRuntimeSourceRoot}/`)
103+
}
104+
105+
function omitPrivateRuntimeSourcePaths(value) {
106+
if (Array.isArray(value)) return value.map(omitPrivateRuntimeSourcePaths)
107+
if (!value || typeof value !== "object") return value
108+
return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => {
109+
if (PRIVATE_RUNTIME_PATH_FIELDS.has(key) && isPrivateRuntimePath(entry)) return []
110+
return [[key, omitPrivateRuntimeSourcePaths(entry)]]
111+
}))
112+
}
113+
114+
function assertNoPrivateRuntimePaths(value) {
115+
if (privateRuntimeSourceRoot && JSON.stringify(value).includes(privateRuntimeSourceRoot)) {
116+
throw new Error("Runtime source paths must never be persisted in workflow results or artifacts.")
117+
}
118+
}
119+
97120
function string(value) {
98121
return typeof value === "string" ? value.trim() : ""
99122
}
@@ -189,11 +212,25 @@ const driftChecks = commandEntries(request.drift_checks, "drift_checks")
189212
const runId = `${request.workload?.id || "agent-task"}-${process.env.GITHUB_RUN_ID || "local"}`.replace(/[^A-Za-z0-9._-]+/g, "-")
190213
const externalPackagePolicy = parseExternalPackageSourcePolicy(string(process.env.EXTERNAL_PACKAGE_SOURCE_POLICY))
191214
const externalPackageSource = normalizeExternalPackageSource(request.external_package_source, externalPackagePolicy)
215+
const runtimeSources = normalizeRuntimeSources(request.runtime_sources ?? [], externalPackagePolicy)
192216
const artifactsPath = join(workspace, ".codebox", "agent-task-artifacts")
193217
const runtimeInputPath = join(workspace, ".codebox", "native-agent-task-input.json")
194218
const resultPath = join(workspace, ".codebox", "agent-task-workflow-result.json")
195219
const controlledCodeboxPath = resolve(requestPath, "..")
196220
const nativeResultPath = join(controlledCodeboxPath, "native-agent-task-result.json")
221+
let privateRuntimeSourceRoot = ""
222+
let cleaningPrivateRuntimeSources = false
223+
async function cleanupPrivateRuntimeSources() {
224+
if (cleaningPrivateRuntimeSources || !privateRuntimeSourceRoot) return
225+
cleaningPrivateRuntimeSources = true
226+
const root = privateRuntimeSourceRoot
227+
privateRuntimeSourceRoot = ""
228+
await rm(root, { recursive: true, force: true })
229+
}
230+
process.once("exit", () => { if (privateRuntimeSourceRoot) rmSync(privateRuntimeSourceRoot, { recursive: true, force: true }) })
231+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
232+
process.once(signal, () => { cleanupPrivateRuntimeSources().finally(() => process.exit(128)) })
233+
}
197234
const runnerWorkspaceTools = [
198235
"workspace-read", "workspace-ls", "workspace-grep", "workspace-write", "workspace-edit", "workspace-apply-patch",
199236
"workspace-git-status", "workspace-git-diff", "workspace-git-add", "workspace-git-commit", "workspace-git-push",
@@ -220,11 +257,22 @@ if (accessError) {
220257
const materializedPackage = request.run_agent && !request.dry_run
221258
? await materializeExternalNativePackage(externalPackageSource, { policy: externalPackagePolicy })
222259
: undefined
260+
const materializedRuntimeSources = request.run_agent && !request.dry_run
261+
? await materializeRuntimeSources(runtimeSources, { policy: externalPackagePolicy, forbiddenRoots: [workspace, artifactsPath] })
262+
: undefined
263+
privateRuntimeSourceRoot = materializedRuntimeSources?.root ?? ""
264+
const runtimeSourceInputs = (materializedRuntimeSources?.lowered ?? []).reduce((input, lowered) => {
265+
for (const [key, entries] of Object.entries(lowered)) input[key] = [...(input[key] ?? []), ...entries]
266+
return input
267+
}, {})
268+
const sourcePackageRoot = privateRuntimeSourceRoot ? join(privateRuntimeSourceRoot, "prepared-packages") : artifactsPath
269+
const executionInputPath = privateRuntimeSourceRoot ? join(privateRuntimeSourceRoot, "native-agent-task-input.json") : runtimeInputPath
223270

224271
const taskInput = {
225272
schema: "wp-codebox/agent-task-run-request/v1",
226273
task_id: runId,
227-
artifacts_path: artifactsPath,
274+
artifacts_path: artifactsPath,
275+
source_package_root: sourcePackageRoot,
228276
callback_data: record(request.callback_data),
229277
task_input: {
230278
schema: "wp-codebox/task-input/v1",
@@ -235,6 +283,7 @@ const taskInput = {
235283
provider: request.model?.provider,
236284
model: request.model?.name,
237285
secret_env: ["OPENAI_API_KEY", "MODEL_PROVIDER_SECRET_1", "MODEL_PROVIDER_SECRET_2", "MODEL_PROVIDER_SECRET_3", "MODEL_PROVIDER_SECRET_4", "MODEL_PROVIDER_SECRET_5"].filter((name) => process.env[name]),
286+
...runtimeSourceInputs,
238287
allowed_tools: runnerWorkspaceTools,
239288
sandbox_tool_policy: {
240289
schema: "wp-codebox/sandbox-tool-policy/v1",
@@ -265,26 +314,30 @@ const taskInput = {
265314
artifact_declarations: request.artifacts?.declarations || [],
266315
required_artifacts: request.artifacts?.expected || [],
267316
output_projections: [],
268-
metadata: { workload: request.workload, ...(materializedPackage ? { imported_agent: materializedPackage.identity } : {}) },
317+
metadata: { workload: request.workload, ...(materializedPackage ? { imported_agent: materializedPackage.identity } : {}), runtime_sources: materializedRuntimeSources?.descriptors ?? [] },
269318
},
270319
},
271320
},
272321
}
273322

274-
await writeFile(runtimeInputPath, `${JSON.stringify(taskInput, null, 2)}\n`)
323+
await writeFile(executionInputPath, `${JSON.stringify(taskInput, null, 2)}\n`)
275324

276325
let execution = { code: 0, stdout: "", stderr: "", stdout_truncated: false, stderr_truncated: false }
277326
if (request.run_agent && !request.dry_run) {
278-
execution = await command("node", [codeboxCliPath, "agent-task-run", "--input-file", runtimeInputPath, "--result-file", nativeResultPath], workspace, agentEnvironment())
327+
execution = await command("node", [codeboxCliPath, "agent-task-run", "--input-file", executionInputPath, "--result-file", nativeResultPath], workspace, agentEnvironment())
279328
}
280329

281330
// Public package bytes are embedded in the runtime recipe and consumed only by
282331
// the Playground bootstrap before the agent's tools are resolved.
283332

284-
const runtimeResult = request.run_agent && !request.dry_run
333+
const nativeRuntimeResult = request.run_agent && !request.dry_run
285334
? await readNativeResult(nativeResultPath, controlledCodeboxPath, secretValues, redact)
286335
: {}
287336
await rm(nativeResultPath, { force: true })
337+
assertNoPrivateRuntimePaths(nativeRuntimeResult)
338+
const runtimeResult = omitPrivateRuntimeSourcePaths(nativeRuntimeResult)
339+
assertNoPrivateRuntimePaths(runtimeResult)
340+
await cleanupPrivateRuntimeSources()
288341

289342
await redactArtifactFiles(artifactsPath)
290343

0 commit comments

Comments
 (0)