11import { 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"
33import { isAbsolute , join , relative , resolve } from "node:path"
44import { pathToFileURL } from "node:url"
55import { spawn } from "node:child_process"
6+ import { createHash } from "node:crypto"
67import { canonicalExternalNativeAgentIdentity , materializeExternalNativePackage , materializeRuntimeSources , normalizeExternalPackageSource , normalizeRuntimeSources , parseExternalPackageSourcePolicy , sha256BytesV1 , validateRuntimeSourceModel } from "./materialize-external-native-package.mjs"
78import { readNativeResult } from "./native-result-file.mjs"
89import { assertNoRuntimeSourcePaths , sanitizeRuntimeSourceJson , sanitizeRuntimeSourceText , sanitizeRuntimeSourceValue } from "./runtime-source-sanitizer.mjs"
@@ -20,6 +21,7 @@ const secretValues = ["OPENAI_API_KEY", "MODEL_PROVIDER_SECRET_1", "MODEL_PROVID
2021let privateRuntimeSourceRoot = ""
2122let privateRuntimeSourceRootForSanitization = ""
2223let runnerWorkspaceSeedSnapshot
24+ let reviewerEvidence
2325
2426const SIGNAL_EXIT_CODES = { SIGINT : 130 , SIGTERM : 143 , SIGHUP : 129 }
2527let 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 : { }
469520await rm ( nativeResultPath , { force : true } )
521+ reviewerEvidence = await canonicalReviewerTranscript ( nativeRuntimeResult , artifactsPath )
470522let runtimeResult = sanitizeRuntimeSourceValue ( nativeRuntimeResult , privateRuntimeSourceRootForSanitization )
471523assertNoRuntimeSourcePaths ( runtimeResult , privateRuntimeSourceRootForSanitization )
472524let 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