@@ -6,6 +6,7 @@ import * as dockerImage from "../../adapters/docker/image";
66import * as agentDefs from "../../agent/defs" ;
77import * as agentOnboard from "../../agent/onboard" ;
88import * as gatewayRuntime from "../../gateway-runtime-action" ;
9+ import type { SandboxBaseImageResolutionMetadata } from "../../sandbox-base-image" ;
910import * as sandboxState from "../../state/sandbox" ;
1011import * as userManagedFilesProbe from "../../state/user-managed-files-probe" ;
1112import {
@@ -135,7 +136,9 @@ describe("rebuild agent base image preflight", () => {
135136 } ) ;
136137
137138 function mockBaseImagePreflight ( imageRef : string ) {
138- vi . spyOn ( agentDefs , "loadAgent" ) . mockReturnValue ( { name : "hermes" } as never ) ;
139+ const loadAgent = vi
140+ . spyOn ( agentDefs , "loadAgent" )
141+ . mockReturnValue ( { name : "hermes" , displayName : "Hermes Agent" } as never ) ;
139142 const ensureAgentBaseImage = vi
140143 . spyOn ( agentOnboard , "ensureAgentBaseImage" )
141144 . mockReturnValue ( { imageTag : imageRef , built : true } ) ;
@@ -145,39 +148,57 @@ describe("rebuild agent base image preflight", () => {
145148 const pinAgentSandboxBaseImageRef = vi
146149 . spyOn ( agentOnboard , "pinAgentSandboxBaseImageRef" )
147150 . mockImplementation ( ( _agentName , ref ) => String ( ref ) ) ;
151+ const bindLocalAgentBaseImageHandoffToResolution = vi
152+ . spyOn ( agentOnboard , "bindLocalAgentBaseImageHandoffToResolution" )
153+ . mockReturnValue ( null ) ;
148154 const dockerRmi = vi . spyOn ( dockerImage , "dockerRmi" ) . mockReturnValue ( { status : 0 } as never ) ;
149155 return {
156+ loadAgent,
150157 ensureAgentBaseImage,
151158 bindLocalAgentBaseImageToPinnedProvenance,
159+ bindLocalAgentBaseImageHandoffToResolution,
152160 pinAgentSandboxBaseImageRef,
153161 dockerRmi,
154162 } ;
155163 }
156164
157165 it ( "forces a repository-local build and returns its exact ref when no override exists" , ( ) => {
158- const imageRef = "nemoclaw-hermes-sandbox-base-local:12345678" ;
166+ const imageRef = `nemoclaw-hermes-sandbox-base-local:image-${ "a" . repeat ( 64 ) } ` ;
167+ const trustedLocalOverride = {
168+ ref : imageRef ,
169+ provenance : `${ "b" . repeat ( 64 ) } .${ "c" . repeat ( 64 ) } ` ,
170+ } ;
159171 const { ensureAgentBaseImage } = mockBaseImagePreflight ( imageRef ) ;
172+ ensureAgentBaseImage . mockReturnValue ( {
173+ imageTag : imageRef ,
174+ built : true ,
175+ trustedLocalOverride,
176+ } ) ;
160177
161178 const result = ensureRebuildAgentBaseImage ( "hermes" , makeBail ( ) ) ;
162179
163180 expect ( ensureAgentBaseImage ) . toHaveBeenCalledWith ( expect . objectContaining ( { name : "hermes" } ) , {
164181 forceBaseImageRebuild : true ,
165182 } ) ;
166- expect ( result ) . toEqual ( { ok : true , imageRef, overrideEnvVar } ) ;
183+ expect ( result ) . toEqual ( {
184+ ok : true ,
185+ imageRef,
186+ overrideEnvVar,
187+ trustedLocalOverride,
188+ } ) ;
167189 } ) ;
168190
169- it ( "resolves an explicit caller override instead of replacing it during preflight " , ( ) => {
191+ it ( "fails closed when an explicit local result lacks validated outer metadata " , ( ) => {
170192 process . env [ overrideEnvVar ] = "nemoclaw-hermes-sandbox-base-local:caller" ;
171193 const mutableRef = "nemoclaw-hermes-sandbox-base-local:resolved" ;
172- const immutableRef = `nemoclaw-hermes-sandbox-base-local:image-${ "a" . repeat ( 64 ) } ` ;
173- const {
174- ensureAgentBaseImage,
175- bindLocalAgentBaseImageToPinnedProvenance,
176- pinAgentSandboxBaseImageRef,
177- } = mockBaseImagePreflight ( mutableRef ) ;
178- pinAgentSandboxBaseImageRef . mockReturnValue ( immutableRef ) ;
194+ const rebuildRef = `nemoclaw-hermes-sandbox-base-local:rebuild-343338-${ "b" . repeat ( 16 ) } -image-${ "a" . repeat ( 64 ) } ` ;
195+ const { ensureAgentBaseImage, pinAgentSandboxBaseImageRef, dockerRmi } =
196+ mockBaseImagePreflight ( mutableRef ) ;
197+ pinAgentSandboxBaseImageRef . mockReturnValue ( rebuildRef ) ;
179198
180- const result = ensureRebuildAgentBaseImage ( "hermes" , makeBail ( ) ) ;
199+ expect ( ( ) => ensureRebuildAgentBaseImage ( "hermes" , makeBail ( ) ) ) . toThrow (
200+ "could not be bound to its rebuild handoff" ,
201+ ) ;
181202
182203 expect ( ensureAgentBaseImage ) . toHaveBeenCalledWith ( expect . objectContaining ( { name : "hermes" } ) , {
183204 forceBaseImageRebuild : false ,
@@ -186,17 +207,10 @@ describe("rebuild agent base image preflight", () => {
186207 forceLocal : true ,
187208 temporary : true ,
188209 } ) ;
189- expect ( bindLocalAgentBaseImageToPinnedProvenance ) . toHaveBeenCalledWith (
190- expect . objectContaining ( { name : "hermes" } ) ,
191- immutableRef ,
192- ) ;
193- expect ( result ) . toEqual ( {
194- ok : true ,
195- imageRef : immutableRef ,
196- overrideEnvVar,
197- disposeImageRef : expect . any ( Function ) ,
210+ expect ( dockerRmi ) . toHaveBeenCalledWith ( rebuildRef , {
211+ ignoreError : true ,
212+ suppressOutput : true ,
198213 } ) ;
199- expect ( disposeRebuildAgentBaseImagePreflight ( result ) ) . toBe ( true ) ;
200214 } ) ;
201215
202216 it ( "proves a caller alias before resolving it as the pinned remote image (#7144)" , ( ) => {
@@ -252,6 +266,158 @@ describe("rebuild agent base image preflight", () => {
252266 } ) ;
253267 } ) ;
254268
269+ it ( "leases a temporary local handoff only from the stable outer resolution" , ( ) => {
270+ const inputHashRef = "nemoclaw-hermes-sandbox-base-local:3ef2ca87" ;
271+ const rebuildRef = `nemoclaw-hermes-sandbox-base-local:rebuild-343338-${ "c" . repeat ( 16 ) } -image-${ "d" . repeat ( 64 ) } ` ;
272+ const provenance = `${ "e" . repeat ( 64 ) } .${ "f" . repeat ( 64 ) } ` ;
273+ const resolutionMetadata = {
274+ ref : inputHashRef ,
275+ digest : null ,
276+ source : "local" ,
277+ imageId : `sha256:${ "d" . repeat ( 64 ) } ` ,
278+ } as SandboxBaseImageResolutionMetadata ;
279+ const mocks = mockBaseImagePreflight ( inputHashRef ) ;
280+ const {
281+ bindLocalAgentBaseImageHandoffToResolution,
282+ ensureAgentBaseImage,
283+ pinAgentSandboxBaseImageRef,
284+ } = mocks ;
285+ pinAgentSandboxBaseImageRef . mockReturnValue ( rebuildRef ) ;
286+ ensureAgentBaseImage . mockReturnValue ( {
287+ imageTag : inputHashRef ,
288+ built : false ,
289+ resolutionMetadata,
290+ reusedResolutionHint : resolutionMetadata ,
291+ } ) ;
292+ bindLocalAgentBaseImageHandoffToResolution . mockReturnValue ( {
293+ ref : rebuildRef ,
294+ provenance,
295+ } ) ;
296+
297+ try {
298+ const result = ensureRebuildAgentBaseImage ( "hermes" , makeBail ( ) , {
299+ resolutionHint : resolutionMetadata ,
300+ } ) ;
301+
302+ expect ( bindLocalAgentBaseImageHandoffToResolution ) . toHaveBeenCalledWith (
303+ expect . objectContaining ( { name : "hermes" } ) ,
304+ inputHashRef ,
305+ rebuildRef ,
306+ resolutionMetadata ,
307+ resolutionMetadata ,
308+ ) ;
309+ expect ( result ) . toMatchObject ( {
310+ imageRef : rebuildRef ,
311+ resolutionMetadata,
312+ trustedLocalOverride : { ref : rebuildRef , provenance } ,
313+ } ) ;
314+ } finally {
315+ for ( const mock of Object . values ( mocks ) ) mock . mockRestore ( ) ;
316+ }
317+ } ) ;
318+
319+ it ( "force-rebuilds instead of trusting fresh fallback metadata after a local tag moved" , ( ) => {
320+ const sourceRef = "nemoclaw-hermes-sandbox-base-local:3ef2ca87" ;
321+ const imageId = `sha256:${ "d" . repeat ( 64 ) } ` ;
322+ const canonicalRef = `nemoclaw-hermes-sandbox-base-local:image-${ "d" . repeat ( 64 ) } ` ;
323+ const persistedHint = {
324+ ref : sourceRef ,
325+ digest : null ,
326+ source : "local" ,
327+ imageId : `sha256:${ "a" . repeat ( 64 ) } ` ,
328+ } as SandboxBaseImageResolutionMetadata ;
329+ const freshFallbackMetadata = {
330+ ...persistedHint ,
331+ imageId,
332+ } ;
333+ const forcedMetadata = {
334+ ...freshFallbackMetadata ,
335+ ref : canonicalRef ,
336+ } ;
337+ const forcedTrust = {
338+ ref : canonicalRef ,
339+ provenance : `${ "e" . repeat ( 64 ) } .${ "f" . repeat ( 64 ) } ` ,
340+ } ;
341+ const {
342+ bindLocalAgentBaseImageHandoffToResolution,
343+ ensureAgentBaseImage,
344+ pinAgentSandboxBaseImageRef,
345+ } = mockBaseImagePreflight ( sourceRef ) ;
346+ ensureAgentBaseImage
347+ . mockReturnValueOnce ( {
348+ imageTag : sourceRef ,
349+ built : false ,
350+ resolutionMetadata : freshFallbackMetadata ,
351+ } )
352+ . mockReturnValueOnce ( {
353+ imageTag : canonicalRef ,
354+ built : true ,
355+ resolutionMetadata : forcedMetadata ,
356+ trustedLocalOverride : forcedTrust ,
357+ } ) ;
358+ pinAgentSandboxBaseImageRef . mockReturnValue ( canonicalRef ) ;
359+
360+ const result = ensureRebuildAgentBaseImage ( "hermes" , makeBail ( ) , {
361+ resolutionHint : persistedHint ,
362+ } ) ;
363+
364+ expect ( bindLocalAgentBaseImageHandoffToResolution ) . not . toHaveBeenCalled ( ) ;
365+ expect ( ensureAgentBaseImage ) . toHaveBeenCalledTimes ( 2 ) ;
366+ expect ( ensureAgentBaseImage ) . toHaveBeenNthCalledWith ( 2 , expect . anything ( ) , {
367+ forceBaseImageRebuild : true ,
368+ } ) ;
369+ expect ( result ) . toMatchObject ( {
370+ imageRef : canonicalRef ,
371+ resolutionMetadata : forcedMetadata ,
372+ trustedLocalOverride : forcedTrust ,
373+ } ) ;
374+ } ) ;
375+
376+ it ( "reuses the canonical forced-build metadata on the next offline rebuild" , ( ) => {
377+ const canonicalRef = `nemoclaw-hermes-sandbox-base-local:image-${ "d" . repeat ( 64 ) } ` ;
378+ const resolutionMetadata = {
379+ ref : canonicalRef ,
380+ digest : null ,
381+ source : "local" ,
382+ imageId : `sha256:${ "d" . repeat ( 64 ) } ` ,
383+ } as SandboxBaseImageResolutionMetadata ;
384+ const provenance = `${ "e" . repeat ( 64 ) } .${ "f" . repeat ( 64 ) } ` ;
385+ const {
386+ bindLocalAgentBaseImageHandoffToResolution,
387+ ensureAgentBaseImage,
388+ pinAgentSandboxBaseImageRef,
389+ } = mockBaseImagePreflight ( canonicalRef ) ;
390+ ensureAgentBaseImage . mockReturnValue ( {
391+ imageTag : canonicalRef ,
392+ built : false ,
393+ resolutionMetadata,
394+ reusedResolutionHint : resolutionMetadata ,
395+ } ) ;
396+ pinAgentSandboxBaseImageRef . mockReturnValue ( canonicalRef ) ;
397+ bindLocalAgentBaseImageHandoffToResolution . mockReturnValue ( {
398+ ref : canonicalRef ,
399+ provenance,
400+ } ) ;
401+
402+ const result = ensureRebuildAgentBaseImage ( "hermes" , makeBail ( ) , {
403+ resolutionHint : resolutionMetadata ,
404+ } ) ;
405+
406+ expect ( ensureAgentBaseImage ) . toHaveBeenCalledOnce ( ) ;
407+ expect ( ensureAgentBaseImage ) . toHaveBeenCalledWith ( expect . anything ( ) , {
408+ forceBaseImageRebuild : false ,
409+ resolutionHint : resolutionMetadata ,
410+ } ) ;
411+ expect ( bindLocalAgentBaseImageHandoffToResolution ) . toHaveBeenCalledWith (
412+ expect . anything ( ) ,
413+ canonicalRef ,
414+ canonicalRef ,
415+ resolutionMetadata ,
416+ resolutionMetadata ,
417+ ) ;
418+ expect ( result . trustedLocalOverride ) . toEqual ( { ref : canonicalRef , provenance } ) ;
419+ } ) ;
420+
255421 it ( "disposes a temporary recreate handoff at most once (#7144)" , ( ) => {
256422 const disposeImageRef = vi . fn ( ( ) => true ) ;
257423 const preflight = {
0 commit comments