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,7 @@ 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
2123function redact ( value ) {
2224 if ( typeof value === "string" ) return secretValues . reduce ( ( output , secret ) => output . split ( secret ) . join ( "[REDACTED]" ) , value )
2325 if ( Array . isArray ( value ) ) return value . map ( redact )
@@ -197,6 +199,44 @@ function requiredArtifacts(declarations) {
197199 } ) ) )
198200}
199201
202+ async function testRuntimeFixtures ( externalPackageSource ) {
203+ if ( process . env . NODE_ENV !== "test" ) return { }
204+ const packagePath = string ( process . env . WP_CODEBOX_TEST_EXTERNAL_PACKAGE_PATH )
205+ const runtimeSourceInputs = string ( process . env . WP_CODEBOX_TEST_RUNTIME_SOURCE_INPUTS )
206+ if ( ! packagePath && ! runtimeSourceInputs ) return { }
207+ const fixtures = { }
208+ if ( packagePath ) {
209+ const bytes = await readFile ( packagePath )
210+ if ( sha256BytesV1 ( bytes ) !== externalPackageSource . digest ) throw new Error ( "Test external package fixture digest does not match the request." )
211+ fixtures . materializedPackage = { bytes, descriptor : externalPackageSource , identity : canonicalExternalNativeAgentIdentity ( bytes ) }
212+ }
213+ if ( runtimeSourceInputs ) {
214+ try {
215+ fixtures . runtimeSourceInputs = JSON . parse ( runtimeSourceInputs )
216+ } catch {
217+ throw new Error ( "WP_CODEBOX_TEST_RUNTIME_SOURCE_INPUTS must be valid JSON." )
218+ }
219+ }
220+ return fixtures
221+ }
222+
223+ function runtimeSourceFixtureRoot ( value ) {
224+ const paths = [ ]
225+ const collect = ( entry ) => {
226+ if ( typeof entry === "string" ) {
227+ if ( isAbsolute ( entry ) ) paths . push ( resolve ( entry ) )
228+ return
229+ }
230+ if ( Array . isArray ( entry ) ) return entry . forEach ( collect )
231+ if ( entry && typeof entry === "object" ) Object . values ( entry ) . forEach ( collect )
232+ }
233+ collect ( value )
234+ if ( paths . length === 0 ) return ""
235+
236+ const shared = paths . map ( ( path ) => path . split ( "/" ) . filter ( Boolean ) ) . reduce ( ( prefix , parts ) => prefix . filter ( ( part , index ) => parts [ index ] === part ) )
237+ return shared . length > 0 ? `/${ shared . join ( "/" ) } ` : ""
238+ }
239+
200240function workflowPath ( path ) {
201241 const relativePath = relative ( workspace , resolve ( path ) )
202242 return relativePath && ! relativePath . startsWith ( ".." ) ? relativePath . replaceAll ( "\\" , "/" ) : ".codebox/agent-task-workflow-result.json"
@@ -261,19 +301,26 @@ const externalPackagePolicy = parseExternalPackageSourcePolicy(string(process.en
261301const externalPackageSource = normalizeExternalPackageSource ( request . external_package_source , externalPackagePolicy )
262302const runtimeSources = normalizeRuntimeSources ( request . runtime_sources ?? [ ] , externalPackagePolicy )
263303const requestedModel = validateRuntimeSourceModel ( request . model , runtimeSources )
304+ const writablePaths = String ( request . writable_paths || "" ) . split ( "," ) . map ( ( value ) => value . trim ( ) ) . filter ( Boolean )
264305const artifactsPath = join ( workspace , ".codebox" , "agent-task-artifacts" )
265306const runtimeInputPath = join ( workspace , ".codebox" , "native-agent-task-input.json" )
266307const resultPath = join ( workspace , ".codebox" , "agent-task-workflow-result.json" )
267308const controlledCodeboxPath = resolve ( requestPath , ".." )
268- const nativeResultPath = join ( controlledCodeboxPath , "native-agent-task-result.json" )
309+ const nativeResultPath = join ( controlledCodeboxPath , "native-agent-task-result.json" )
269310let cleaningPrivateRuntimeSources = false
270- async function cleanupPrivateRuntimeSources ( ) {
311+ async function cleanupPrivateRuntimeSources ( ) {
271312 if ( cleaningPrivateRuntimeSources || ! privateRuntimeSourceRoot ) return
272313 cleaningPrivateRuntimeSources = true
273314 const root = privateRuntimeSourceRoot
274315 privateRuntimeSourceRoot = ""
275316 await rm ( root , { recursive : true , force : true } )
276- }
317+ }
318+ async function cleanupRunnerWorkspaceSeedSnapshot ( ) {
319+ if ( ! runnerWorkspaceSeedSnapshot ) return
320+ const source = runnerWorkspaceSeedSnapshot . source
321+ runnerWorkspaceSeedSnapshot = undefined
322+ await rm ( source , { recursive : true , force : true } )
323+ }
277324process . once ( "exit" , ( ) => { if ( privateRuntimeSourceRoot ) rmSync ( privateRuntimeSourceRoot , { recursive : true , force : true } ) } )
278325for ( const signal of [ "SIGINT" , "SIGTERM" , "SIGHUP" ] ) {
279326 process . once ( signal , ( ) => { cleanupPrivateRuntimeSources ( ) . finally ( ( ) => process . exit ( 128 ) ) } )
@@ -291,17 +338,20 @@ function runtimeMetadataForExecutionLocation(executionLocation) {
291338
292339await mkdir ( artifactsPath , { recursive : true } )
293340
294- const accessError = accessFailure ( request )
341+ const accessError = accessFailure ( request )
295342if ( accessError ) {
296343 const error = new Error ( accessError )
297344 error . code = "wp-codebox.agent-task.policy"
298- throw error
299- }
345+ throw error
346+ }
300347
301- const skipMaterialization = process . env . NODE_ENV === "test" && process . env . WP_CODEBOX_TEST_SKIP_MATERIALIZATION === "true"
348+ if ( request . runner_workspace ?. enabled ) runnerWorkspaceSeedSnapshot = await createRunnerWorkspaceSeedSnapshot ( workspace )
349+
350+ const testFixtures = await testRuntimeFixtures ( externalPackageSource )
351+ const skipMaterialization = process . env . NODE_ENV === "test" && ( process . env . WP_CODEBOX_TEST_SKIP_MATERIALIZATION === "true" || Boolean ( testFixtures . materializedPackage || testFixtures . runtimeSourceInputs ) )
302352const materializedPackage = request . run_agent && ! request . dry_run && ! skipMaterialization
303353 ? await materializeExternalNativePackage ( externalPackageSource , { policy : externalPackagePolicy } )
304- : undefined
354+ : testFixtures . materializedPackage
305355const materializedRuntimeSources = request . run_agent && ! request . dry_run && ! skipMaterialization
306356 ? await materializeRuntimeSources ( runtimeSources , { policy : externalPackagePolicy , forbiddenRoots : [ workspace , artifactsPath ] } )
307357 : undefined
@@ -311,7 +361,8 @@ await output("runtime_source_root", privateRuntimeSourceRoot)
311361const runtimeSourceInputs = ( materializedRuntimeSources ?. lowered ?? [ ] ) . reduce ( ( input , lowered ) => {
312362 for ( const [ key , entries ] of Object . entries ( lowered ) ) input [ key ] = [ ...( input [ key ] ?? [ ] ) , ...entries ]
313363 return input
314- } , { } )
364+ } , testFixtures . runtimeSourceInputs ?? { } )
365+ const runtimeSourceOutputRoots = [ privateRuntimeSourceRoot , runtimeSourceFixtureRoot ( testFixtures . runtimeSourceInputs ) ]
315366// Runtime source preparation must remain beside the private checkout, never in
316367// the target artifact directory that is collected after the run.
317368const privatePreparationRoot = privateRuntimeSourceRoot ? join ( privateRuntimeSourceRoot , "prepared-runtime-sources" ) : ""
@@ -358,27 +409,28 @@ const taskInput = {
358409 model : requestedModel . name ,
359410 runner_workspace : { ...record ( request . runner_workspace ) , allowed_repos : request . access . allowed_repos } ,
360411 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 } ,
412+ writable_paths : writablePaths ,
413+ runner_workspace_policy : { allowed_repos : request . access . allowed_repos , writable_paths : writablePaths } ,
363414 } ,
364415 artifact_declarations : request . artifacts ?. declarations || [ ] ,
365416 required_artifacts : requiredArtifacts ( request . artifacts ?. declarations ) ,
366417 output_projections : [ ] ,
367- metadata : { workload : request . workload , ...( materializedPackage ? { imported_agent : materializedPackage . identity } : { } ) , runtime_sources : materializedRuntimeSources ?. descriptors ?? [ ] } ,
418+ metadata : { workload : request . workload , ...( materializedPackage ? { imported_agent : materializedPackage . identity } : { } ) , runtime_sources : materializedRuntimeSources ?. descriptors ?? [ ] , ... ( runnerWorkspaceSeedSnapshot ? { runner_workspace_seed : runnerWorkspaceSeedSnapshot . provenance } : { } ) } ,
368419 } ,
369420 } ,
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/**" ] } } ] : [ ] ,
421+ // agent-task-run unwraps task_input before building the recipe. The
422+ // external snapshot prevents the recipe from ever mounting the checkout .
423+ workspaces : runnerWorkspaceSeedSnapshot ? [ { target : "/workspace" , mode : "readwrite" , sourceMode : "repo-backed" , seed : { type : "directory" , source : runnerWorkspaceSeedSnapshot . source , excludePaths : RUNNER_WORKSPACE_SEED_EXCLUDES } } ] : [ ] ,
373424 } ,
374425 }
375426
376427await writeFile ( executionInputPath , `${ JSON . stringify ( taskInput , null , 2 ) } \n` )
377428
378429let 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- }
430+ if ( request . run_agent && ! request . dry_run ) {
431+ execution = await command ( "node" , [ codeboxCliPath , "agent-task-run" , "--input-file" , executionInputPath , "--result-file" , nativeResultPath ] , workspace , agentEnvironment ( ) )
432+ }
433+ await cleanupRunnerWorkspaceSeedSnapshot ( )
382434
383435// Public package bytes are embedded in the runtime recipe and consumed only by
384436// the Playground bootstrap before the agent's tools are resolved.
@@ -387,7 +439,7 @@ const nativeRuntimeResult = request.run_agent && !request.dry_run
387439 ? await readNativeResult ( nativeResultPath , controlledCodeboxPath , secretValues , redact )
388440 : { }
389441await rm ( nativeResultPath , { force : true } )
390- const runtimeResult = sanitizeRuntimeSourceValue ( nativeRuntimeResult , privateRuntimeSourceRootForSanitization )
442+ let runtimeResult = sanitizeRuntimeSourceValue ( nativeRuntimeResult , privateRuntimeSourceRootForSanitization )
391443assertNoRuntimeSourcePaths ( runtimeResult , privateRuntimeSourceRootForSanitization )
392444let workspaceApply = { status : "no-op" , changedFiles : [ ] }
393445let runnerWorkspaceCore = null
@@ -398,12 +450,14 @@ if (execution.code === 0 && runtimeResult.success === true && request.runner_wor
398450 const publicCore = await import ( pathToFileURL ( join ( codeboxRoot , "packages/runtime-core/dist/public.js" ) ) . href )
399451 const refs = publicCore . normalizePublicArtifactRefDTOs ( runtimeResult )
400452 . 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 ) } )
453+ workspaceApply = await runnerWorkspaceCore . applyRunnerWorkspacePatch ( { artifactRoot : artifactsPath , artifactRefs : refs , workspaceRoot : workspace , writablePaths } )
402454 } catch ( error ) {
403455 downstreamFailure = { stage : "apply" , message : bounded ( error instanceof Error ? error . message : String ( error ) , MAX_OUTPUT_CHARS ) }
404456 }
405457}
406458await cleanupPrivateRuntimeSources ( )
459+ runtimeResult = sanitizeRuntimeSourceValue ( runtimeResult , runtimeSourceOutputRoots )
460+ privateRuntimeSourceRootForSanitization = runtimeSourceOutputRoots
407461assertNoRuntimeSourcePaths ( runtimeResult , privateRuntimeSourceRootForSanitization )
408462
409463await redactArtifactFiles ( artifactsPath )
@@ -514,6 +568,11 @@ try {
514568 try {
515569 await writeNormalizedFailure ( error )
516570 } finally {
571+ if ( runnerWorkspaceSeedSnapshot ) {
572+ const source = runnerWorkspaceSeedSnapshot . source
573+ runnerWorkspaceSeedSnapshot = undefined
574+ await rm ( source , { recursive : true , force : true } )
575+ }
517576 if ( privateRuntimeSourceRoot ) {
518577 const root = privateRuntimeSourceRoot
519578 privateRuntimeSourceRoot = ""
0 commit comments