11import { rmSync } from "node:fs"
22import { appendFile , mkdir , readFile , rm , writeFile } from "node:fs/promises"
3- import { join , relative , resolve } from "node:path"
3+ import { isAbsolute , join , relative , resolve } from "node:path"
44import { pathToFileURL } from "node:url"
55import { spawn } from "node:child_process"
6- import { materializeExternalNativePackage , materializeRuntimeSources , normalizeExternalPackageSource , normalizeRuntimeSources , parseExternalPackageSourcePolicy , validateRuntimeSourceModel } from "./materialize-external-native-package.mjs"
6+ import { canonicalExternalNativeAgentIdentity , materializeExternalNativePackage , materializeRuntimeSources , normalizeExternalPackageSource , normalizeRuntimeSources , parseExternalPackageSourcePolicy , sha256BytesV1 , validateRuntimeSourceModel } from "./materialize-external-native-package.mjs"
77import { readNativeResult } from "./native-result-file.mjs"
88import { assertNoRuntimeSourcePaths , sanitizeRuntimeSourceJson , sanitizeRuntimeSourceText , sanitizeRuntimeSourceValue } from "./runtime-source-sanitizer.mjs"
99import { publishRunnerWorkspace } from "./runner-workspace-publisher.mjs"
10+ import { createRunnerWorkspaceSeedSnapshot , RUNNER_WORKSPACE_SEED_EXCLUDES } from "./runner-workspace-seed-snapshot.mjs"
1011
1112const requestPath = process . env . AGENT_TASK_REQUEST_PATH || ".codebox/agent-task-request.json"
1213const workspace = resolve ( process . env . AGENT_TASK_WORKSPACE || process . cwd ( ) )
@@ -18,6 +19,42 @@ const MAX_OUTPUT_CHARS = 8192
1819const 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 )
1920let privateRuntimeSourceRoot = ""
2021let privateRuntimeSourceRootForSanitization = ""
22+ let runnerWorkspaceSeedSnapshot
23+
24+ const SIGNAL_EXIT_CODES = { SIGINT : 130 , SIGTERM : 143 , SIGHUP : 129 }
25+ let materializedSourceCleanup
26+ function claimMaterializedSourcePaths ( ) {
27+ const paths = [ ]
28+ if ( runnerWorkspaceSeedSnapshot ) {
29+ paths . push ( runnerWorkspaceSeedSnapshot . source )
30+ runnerWorkspaceSeedSnapshot = undefined
31+ }
32+ if ( privateRuntimeSourceRoot ) {
33+ paths . push ( privateRuntimeSourceRoot )
34+ privateRuntimeSourceRoot = ""
35+ }
36+ return paths
37+ }
38+ // Single idempotent cleanup coordinator for the private runtime source
39+ // materialization root and the runner workspace seed snapshot. Every
40+ // completion path (normal, failure, signal) awaits this coordinator; repeat
41+ // invocations chain onto the in-flight cleanup instead of racing it.
42+ function cleanupMaterializedSources ( ) {
43+ materializedSourceCleanup = ( materializedSourceCleanup ?? Promise . resolve ( ) )
44+ . then ( ( ) => Promise . all ( claimMaterializedSourcePaths ( ) . map ( ( path ) => rm ( path , { recursive : true , force : true } ) ) ) )
45+ return materializedSourceCleanup
46+ }
47+ process . once ( "exit" , ( ) => {
48+ // Bounded synchronous best-effort fallback for abrupt exits; on every
49+ // awaited path the coordinator has already claimed these roots.
50+ for ( const path of claimMaterializedSourcePaths ( ) ) {
51+ try { rmSync ( path , { recursive : true , force : true , maxRetries : 0 } ) } catch { /* best effort */ }
52+ }
53+ } )
54+ for ( const signal of [ "SIGINT" , "SIGTERM" , "SIGHUP" ] ) {
55+ process . once ( signal , ( ) => { cleanupMaterializedSources ( ) . finally ( ( ) => process . exit ( SIGNAL_EXIT_CODES [ signal ] ) ) } )
56+ }
57+
2158function redact ( value ) {
2259 if ( typeof value === "string" ) return secretValues . reduce ( ( output , secret ) => output . split ( secret ) . join ( "[REDACTED]" ) , value )
2360 if ( Array . isArray ( value ) ) return value . map ( redact )
@@ -197,6 +234,55 @@ function requiredArtifacts(declarations) {
197234 } ) ) )
198235}
199236
237+ async function testRuntimeFixtures ( externalPackageSource ) {
238+ if ( process . env . NODE_ENV !== "test" ) return { }
239+ const packagePath = string ( process . env . WP_CODEBOX_TEST_EXTERNAL_PACKAGE_PATH )
240+ const runtimeSourceInputs = string ( process . env . WP_CODEBOX_TEST_RUNTIME_SOURCE_INPUTS )
241+ if ( ! packagePath && ! runtimeSourceInputs ) return { }
242+ const fixtures = { }
243+ if ( packagePath ) {
244+ const bytes = await readFile ( packagePath )
245+ if ( sha256BytesV1 ( bytes ) !== externalPackageSource . digest ) throw new Error ( "Test external package fixture digest does not match the request." )
246+ fixtures . materializedPackage = { bytes, descriptor : externalPackageSource , identity : canonicalExternalNativeAgentIdentity ( bytes ) }
247+ }
248+ if ( runtimeSourceInputs ) {
249+ try {
250+ fixtures . runtimeSourceInputs = JSON . parse ( runtimeSourceInputs )
251+ } catch {
252+ throw new Error ( "WP_CODEBOX_TEST_RUNTIME_SOURCE_INPUTS must be valid JSON." )
253+ }
254+ }
255+ return fixtures
256+ }
257+
258+ // Test-only interruption hook: inert in production (requires NODE_ENV=test and
259+ // an explicit marker path). Publishes the seed snapshot location, then holds a
260+ // bounded window so a harness can deliver a termination signal.
261+ async function testPauseAfterSeedSnapshot ( seedSnapshot ) {
262+ if ( process . env . NODE_ENV !== "test" ) return
263+ const markerPath = string ( process . env . WP_CODEBOX_TEST_SEED_SNAPSHOT_PAUSE_FILE )
264+ if ( ! markerPath ) return
265+ await writeFile ( markerPath , `${ JSON . stringify ( { schema : "wp-codebox/test-seed-snapshot-pause/v1" , seed_snapshot_source : seedSnapshot ?. source ?? "" } ) } \n` )
266+ await new Promise ( ( resolvePause ) => setTimeout ( resolvePause , 120_000 ) )
267+ }
268+
269+ function runtimeSourceFixtureRoot ( value ) {
270+ const paths = [ ]
271+ const collect = ( entry ) => {
272+ if ( typeof entry === "string" ) {
273+ if ( isAbsolute ( entry ) ) paths . push ( resolve ( entry ) )
274+ return
275+ }
276+ if ( Array . isArray ( entry ) ) return entry . forEach ( collect )
277+ if ( entry && typeof entry === "object" ) Object . values ( entry ) . forEach ( collect )
278+ }
279+ collect ( value )
280+ if ( paths . length === 0 ) return ""
281+
282+ const shared = paths . map ( ( path ) => path . split ( "/" ) . filter ( Boolean ) ) . reduce ( ( prefix , parts ) => prefix . filter ( ( part , index ) => parts [ index ] === part ) )
283+ return shared . length > 0 ? `/${ shared . join ( "/" ) } ` : ""
284+ }
285+
200286function workflowPath ( path ) {
201287 const relativePath = relative ( workspace , resolve ( path ) )
202288 return relativePath && ! relativePath . startsWith ( ".." ) ? relativePath . replaceAll ( "\\" , "/" ) : ".codebox/agent-task-workflow-result.json"
@@ -261,23 +347,12 @@ const externalPackagePolicy = parseExternalPackageSourcePolicy(string(process.en
261347const externalPackageSource = normalizeExternalPackageSource ( request . external_package_source , externalPackagePolicy )
262348const runtimeSources = normalizeRuntimeSources ( request . runtime_sources ?? [ ] , externalPackagePolicy )
263349const requestedModel = validateRuntimeSourceModel ( request . model , runtimeSources )
350+ const writablePaths = String ( request . writable_paths || "" ) . split ( "," ) . map ( ( value ) => value . trim ( ) ) . filter ( Boolean )
264351const artifactsPath = join ( workspace , ".codebox" , "agent-task-artifacts" )
265352const runtimeInputPath = join ( workspace , ".codebox" , "native-agent-task-input.json" )
266353const resultPath = join ( workspace , ".codebox" , "agent-task-workflow-result.json" )
267354const controlledCodeboxPath = resolve ( requestPath , ".." )
268- const nativeResultPath = join ( controlledCodeboxPath , "native-agent-task-result.json" )
269- let cleaningPrivateRuntimeSources = false
270- async function cleanupPrivateRuntimeSources ( ) {
271- if ( cleaningPrivateRuntimeSources || ! privateRuntimeSourceRoot ) return
272- cleaningPrivateRuntimeSources = true
273- const root = privateRuntimeSourceRoot
274- privateRuntimeSourceRoot = ""
275- await rm ( root , { recursive : true , force : true } )
276- }
277- process . once ( "exit" , ( ) => { if ( privateRuntimeSourceRoot ) rmSync ( privateRuntimeSourceRoot , { recursive : true , force : true } ) } )
278- for ( const signal of [ "SIGINT" , "SIGTERM" , "SIGHUP" ] ) {
279- process . once ( signal , ( ) => { cleanupPrivateRuntimeSources ( ) . finally ( ( ) => process . exit ( 128 ) ) } )
280- }
355+ const nativeResultPath = join ( controlledCodeboxPath , "native-agent-task-result.json" )
281356 const runnerWorkspaceTools = [
282357 "workspace_read" , "workspace_ls" , "workspace_grep" , "workspace_write" , "workspace_edit" , "workspace_apply_patch" ,
283358 "workspace_show" , "workspace_git_status" , "workspace_git_diff" ,
@@ -291,17 +366,21 @@ function runtimeMetadataForExecutionLocation(executionLocation) {
291366
292367await mkdir ( artifactsPath , { recursive : true } )
293368
294- const accessError = accessFailure ( request )
369+ const accessError = accessFailure ( request )
295370if ( accessError ) {
296371 const error = new Error ( accessError )
297372 error . code = "wp-codebox.agent-task.policy"
298- throw error
299- }
373+ throw error
374+ }
375+
376+ if ( request . runner_workspace ?. enabled ) runnerWorkspaceSeedSnapshot = await createRunnerWorkspaceSeedSnapshot ( workspace )
377+ await testPauseAfterSeedSnapshot ( runnerWorkspaceSeedSnapshot )
300378
301- const skipMaterialization = process . env . NODE_ENV === "test" && process . env . WP_CODEBOX_TEST_SKIP_MATERIALIZATION === "true"
379+ const testFixtures = await testRuntimeFixtures ( externalPackageSource )
380+ const skipMaterialization = process . env . NODE_ENV === "test" && ( process . env . WP_CODEBOX_TEST_SKIP_MATERIALIZATION === "true" || Boolean ( testFixtures . materializedPackage || testFixtures . runtimeSourceInputs ) )
302381const materializedPackage = request . run_agent && ! request . dry_run && ! skipMaterialization
303382 ? await materializeExternalNativePackage ( externalPackageSource , { policy : externalPackagePolicy } )
304- : undefined
383+ : testFixtures . materializedPackage
305384const materializedRuntimeSources = request . run_agent && ! request . dry_run && ! skipMaterialization
306385 ? await materializeRuntimeSources ( runtimeSources , { policy : externalPackagePolicy , forbiddenRoots : [ workspace , artifactsPath ] } )
307386 : undefined
@@ -311,7 +390,8 @@ await output("runtime_source_root", privateRuntimeSourceRoot)
311390const runtimeSourceInputs = ( materializedRuntimeSources ?. lowered ?? [ ] ) . reduce ( ( input , lowered ) => {
312391 for ( const [ key , entries ] of Object . entries ( lowered ) ) input [ key ] = [ ...( input [ key ] ?? [ ] ) , ...entries ]
313392 return input
314- } , { } )
393+ } , testFixtures . runtimeSourceInputs ?? { } )
394+ const runtimeSourceOutputRoots = [ privateRuntimeSourceRoot , runtimeSourceFixtureRoot ( testFixtures . runtimeSourceInputs ) ]
315395// Runtime source preparation must remain beside the private checkout, never in
316396// the target artifact directory that is collected after the run.
317397const privatePreparationRoot = privateRuntimeSourceRoot ? join ( privateRuntimeSourceRoot , "prepared-runtime-sources" ) : ""
@@ -358,27 +438,27 @@ const taskInput = {
358438 model : requestedModel . name ,
359439 runner_workspace : { ...record ( request . runner_workspace ) , allowed_repos : request . access . allowed_repos } ,
360440 target_repo : request . target_repo ,
361- writable_paths : request . writable_paths ,
362- runner_workspace_policy : { allowed_repos : request . access . allowed_repos , writable_paths : request . writable_paths } ,
441+ writable_paths : writablePaths ,
442+ runner_workspace_policy : { allowed_repos : request . access . allowed_repos , writable_paths : writablePaths } ,
363443 } ,
364444 artifact_declarations : request . artifacts ?. declarations || [ ] ,
365445 required_artifacts : requiredArtifacts ( request . artifacts ?. declarations ) ,
366446 output_projections : [ ] ,
367- metadata : { workload : request . workload , ...( materializedPackage ? { imported_agent : materializedPackage . identity } : { } ) , runtime_sources : materializedRuntimeSources ?. descriptors ?? [ ] } ,
447+ metadata : { workload : request . workload , ...( materializedPackage ? { imported_agent : materializedPackage . identity } : { } ) , runtime_sources : materializedRuntimeSources ?. descriptors ?? [ ] , ... ( runnerWorkspaceSeedSnapshot ? { runner_workspace_seed : runnerWorkspaceSeedSnapshot . provenance } : { } ) } ,
368448 } ,
369449 } ,
370- // agent-task-run unwraps task_input before building the recipe. Keep the
371- // runner seed on that canonical input rather than on the request envelope .
372- 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/**" ] } } ] : [ ] ,
450+ // agent-task-run unwraps task_input before building the recipe. The
451+ // external snapshot prevents the recipe from ever mounting the checkout .
452+ workspaces : runnerWorkspaceSeedSnapshot ? [ { target : "/workspace" , mode : "readwrite" , sourceMode : "repo-backed" , seed : { type : "directory" , source : runnerWorkspaceSeedSnapshot . source , excludePaths : RUNNER_WORKSPACE_SEED_EXCLUDES } } ] : [ ] ,
373453 } ,
374454 }
375455
376456await writeFile ( executionInputPath , `${ JSON . stringify ( taskInput , null , 2 ) } \n` )
377457
378458let execution = { code : 0 , stdout : "" , stderr : "" , stdout_truncated : false , stderr_truncated : false }
379- if ( request . run_agent && ! request . dry_run ) {
380- execution = await command ( "node" , [ codeboxCliPath , "agent-task-run" , "--input-file" , executionInputPath , "--result-file" , nativeResultPath ] , workspace , agentEnvironment ( ) )
381- }
459+ if ( request . run_agent && ! request . dry_run ) {
460+ execution = await command ( "node" , [ codeboxCliPath , "agent-task-run" , "--input-file" , executionInputPath , "--result-file" , nativeResultPath ] , workspace , agentEnvironment ( ) )
461+ }
382462
383463// Public package bytes are embedded in the runtime recipe and consumed only by
384464// the Playground bootstrap before the agent's tools are resolved.
@@ -387,7 +467,7 @@ const nativeRuntimeResult = request.run_agent && !request.dry_run
387467 ? await readNativeResult ( nativeResultPath , controlledCodeboxPath , secretValues , redact )
388468 : { }
389469await rm ( nativeResultPath , { force : true } )
390- const runtimeResult = sanitizeRuntimeSourceValue ( nativeRuntimeResult , privateRuntimeSourceRootForSanitization )
470+ let runtimeResult = sanitizeRuntimeSourceValue ( nativeRuntimeResult , privateRuntimeSourceRootForSanitization )
391471assertNoRuntimeSourcePaths ( runtimeResult , privateRuntimeSourceRootForSanitization )
392472let workspaceApply = { status : "no-op" , changedFiles : [ ] }
393473let runnerWorkspaceCore = null
@@ -398,12 +478,13 @@ if (execution.code === 0 && runtimeResult.success === true && request.runner_wor
398478 const publicCore = await import ( pathToFileURL ( join ( codeboxRoot , "packages/runtime-core/dist/public.js" ) ) . href )
399479 const refs = publicCore . normalizePublicArtifactRefDTOs ( runtimeResult )
400480 . filter ( ( ref ) => ref . kind === "codebox-patch" || ref . kind === "codebox-changed-files" )
401- workspaceApply = await runnerWorkspaceCore . applyRunnerWorkspacePatch ( { artifactRoot : artifactsPath , artifactRefs : refs , workspaceRoot : workspace , writablePaths : String ( request . writable_paths || "" ) . split ( "," ) . map ( ( value ) => value . trim ( ) ) . filter ( Boolean ) } )
481+ workspaceApply = await runnerWorkspaceCore . applyRunnerWorkspacePatch ( { artifactRoot : artifactsPath , artifactRefs : refs , workspaceRoot : workspace , writablePaths } )
402482 } catch ( error ) {
403483 downstreamFailure = { stage : "apply" , message : bounded ( error instanceof Error ? error . message : String ( error ) , MAX_OUTPUT_CHARS ) }
404484 }
405485}
406- await cleanupPrivateRuntimeSources ( )
486+ runtimeResult = sanitizeRuntimeSourceValue ( runtimeResult , runtimeSourceOutputRoots )
487+ privateRuntimeSourceRootForSanitization = runtimeSourceOutputRoots
407488assertNoRuntimeSourcePaths ( runtimeResult , privateRuntimeSourceRootForSanitization )
408489
409490await redactArtifactFiles ( artifactsPath )
@@ -511,15 +592,9 @@ if (!success) process.exitCode = 1
511592try {
512593 await executeNativeAgentTask ( )
513594} catch ( error ) {
514- try {
515- await writeNormalizedFailure ( error )
516- } finally {
517- if ( privateRuntimeSourceRoot ) {
518- const root = privateRuntimeSourceRoot
519- privateRuntimeSourceRoot = ""
520- await rm ( root , { recursive : true , force : true } )
521- }
522- }
595+ await writeNormalizedFailure ( error )
523596 console . error ( bounded ( error instanceof Error ? error . message : String ( error ) , MAX_OUTPUT_CHARS ) )
524597 process . exitCode = 1
598+ } finally {
599+ await cleanupMaterializedSources ( )
525600}
0 commit comments