Skip to content

Commit 400d764

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/1793-semantic-editor-readiness
2 parents 2e1cf8d + 8af1572 commit 400d764

12 files changed

Lines changed: 277 additions & 20 deletions

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

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { rmSync } from "node:fs"
2-
import { appendFile, mkdir, readFile, rm, writeFile } from "node:fs/promises"
2+
import { appendFile, lstat, mkdir, readFile, realpath, rm, writeFile } from "node:fs/promises"
33
import { isAbsolute, join, relative, resolve } from "node:path"
44
import { pathToFileURL } from "node:url"
55
import { spawn } from "node:child_process"
6+
import { createHash } from "node:crypto"
67
import { canonicalExternalNativeAgentIdentity, materializeExternalNativePackage, materializeRuntimeSources, normalizeExternalPackageSource, normalizeRuntimeSources, parseExternalPackageSourcePolicy, sha256BytesV1, validateRuntimeSourceModel } from "./materialize-external-native-package.mjs"
78
import { readNativeResult } from "./native-result-file.mjs"
89
import { assertNoRuntimeSourcePaths, sanitizeRuntimeSourceJson, sanitizeRuntimeSourceText, sanitizeRuntimeSourceValue } from "./runtime-source-sanitizer.mjs"
@@ -20,6 +21,7 @@ const secretValues = ["OPENAI_API_KEY", "MODEL_PROVIDER_SECRET_1", "MODEL_PROVID
2021
let privateRuntimeSourceRoot = ""
2122
let privateRuntimeSourceRootForSanitization = ""
2223
let runnerWorkspaceSeedSnapshot
24+
let reviewerEvidence
2325

2426
const SIGNAL_EXIT_CODES = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 }
2527
let materializedSourceCleanup
@@ -312,6 +314,7 @@ async function writeNormalizedFailure(error, request = {}) {
312314
success: false,
313315
request_path: workflowPath(requestPath),
314316
failure: { ...failure, message },
317+
...(reviewerEvidence ? { reviewer_evidence: reviewerEvidence } : {}),
315318
...(accessError ? { access: { authorized: false, error: message } } : {}),
316319
}
317320
await mkdir(join(workspace, ".codebox"), { recursive: true })
@@ -322,11 +325,59 @@ async function writeNormalizedFailure(error, request = {}) {
322325
await output("result_path", ".codebox/agent-task-workflow-result.json")
323326
}
324327

325-
async function redactArtifactFiles(directory) {
328+
function underRoot(root, path) {
329+
const contained = relative(root, path)
330+
return contained !== ".." && !contained.startsWith(`..${String.fromCharCode(47)}`) && !isAbsolute(contained)
331+
}
332+
333+
async function canonicalReviewerTranscript(nativeRuntimeResult, artifactsPath) {
334+
const publicCore = await import(pathToFileURL(join(codeboxRoot, "packages/runtime-core/dist/public.js")).href)
335+
const refs = publicCore.normalizePublicArtifactRefDTOs(nativeRuntimeResult)
336+
.filter((ref) => ref.kind === "codebox-transcript" && typeof ref.path === "string" && ref.path)
337+
if (refs.length === 0) return undefined
338+
339+
const root = await realpath(artifactsPath)
340+
const artifactRoot = resolve(artifactsPath)
341+
const transcripts = new Map()
342+
for (const ref of refs) {
343+
// Resolve first so containment, rather than spelling, defines a trusted path.
344+
const requested = resolve(artifactRoot, ref.path)
345+
const canonical = await realpath(requested).catch((error) => error?.code === "ENOENT" ? "" : Promise.reject(error))
346+
if (!canonical) continue
347+
if (!underRoot(root, canonical)) throw new Error("Canonical transcript escapes the trusted artifact root.")
348+
const requestedRelative = relative(artifactRoot, requested)
349+
if (!underRoot(artifactRoot, requested)) throw new Error("Canonical transcript escapes the trusted artifact root.")
350+
let current = artifactRoot
351+
for (const part of requestedRelative.split("/").filter(Boolean)) {
352+
current = join(current, part)
353+
const metadata = await lstat(current)
354+
if (metadata.isSymbolicLink()) throw new Error("Canonical transcript must not traverse symlinks.")
355+
}
356+
const metadata = await lstat(current)
357+
if (!metadata.isFile()) throw new Error("Canonical transcript must be a regular file.")
358+
const source = await realpath(current)
359+
const bytes = await readFile(source)
360+
const raw = JSON.parse(bytes.toString("utf8"))
361+
if (raw?.schema !== "wp-codebox/agent-transcript/v1") throw new Error("Canonical transcript must use wp-codebox/agent-transcript/v1.")
362+
transcripts.set(source, {
363+
schema: raw.schema,
364+
kind: "codebox-transcript",
365+
path: relative(root, source).replaceAll("\\", "/"),
366+
source_sha256: createHash("sha256").update(bytes).digest("hex"),
367+
size_bytes: bytes.length,
368+
})
369+
}
370+
if (transcripts.size === 0) return undefined
371+
if (transcripts.size !== 1) throw new Error("Canonical transcript requires exactly one distinct existing file.")
372+
return { transcript: [...transcripts.values()][0] }
373+
}
374+
375+
async function redactArtifactFiles(directory, artifactRoot = directory) {
326376
const { readdir, stat } = await import("node:fs/promises")
327377
for (const entry of await readdir(directory, { withFileTypes: true })) {
328378
const path = join(directory, entry.name)
329-
if (entry.isDirectory()) await redactArtifactFiles(path)
379+
if (reviewerEvidence?.transcript && path === resolve(artifactRoot, reviewerEvidence.transcript.path)) continue
380+
if (entry.isDirectory()) await redactArtifactFiles(path, artifactRoot)
330381
if (entry.isFile() && path.endsWith(".json") && (await stat(path)).size <= 4 * 1024 * 1024) {
331382
const contents = await readFile(path, "utf8").catch(() => null)
332383
if (contents !== null) {
@@ -467,6 +518,7 @@ const nativeRuntimeResult = request.run_agent && !request.dry_run
467518
? await readNativeResult(nativeResultPath, controlledCodeboxPath, secretValues, redact)
468519
: {}
469520
await rm(nativeResultPath, { force: true })
521+
reviewerEvidence = await canonicalReviewerTranscript(nativeRuntimeResult, artifactsPath)
470522
let runtimeResult = sanitizeRuntimeSourceValue(nativeRuntimeResult, privateRuntimeSourceRootForSanitization)
471523
assertNoRuntimeSourcePaths(runtimeResult, privateRuntimeSourceRootForSanitization)
472524
let workspaceApply = { status: "no-op", changedFiles: [] }
@@ -559,6 +611,7 @@ const result = {
559611
runtime_input_path: ".codebox/native-agent-task-input.json",
560612
execution: { stdout_truncated: execution.stdout_truncated, stderr_truncated: execution.stderr_truncated },
561613
runtime_result: redact(runtimeRecord),
614+
...(reviewerEvidence ? { reviewer_evidence: reviewerEvidence } : {}),
562615
verification,
563616
publication,
564617
transcript: { artifact_name: request.artifacts?.transcript_name || "agent-task-transcript" },

0 commit comments

Comments
 (0)