@@ -4,6 +4,7 @@ import { queueTableRows, resetDbChainMock } from '@sim/testing'
44import { createPersonalApiKeyPrincipal } from '@sim/testing/factories/principal.factory'
55import { encryptionMock , encryptionMockFns } from '@sim/testing/mocks/encryption.mock'
66import { fileParsersMock , fileParsersMockFns } from '@sim/testing/mocks/file-parsers.mock'
7+ import { getMockLogger } from '@sim/testing/mocks/logger.mock'
78import {
89 mothershipWorkspaceTargetMock ,
910 mothershipWorkspaceTargetMockFns ,
@@ -53,6 +54,7 @@ vi.mock('@/lib/mothership/agent-cli/scoped-transport', () => ({
5354import { createFileReadTransport } from '@/lib/mothership/agent-cli/file-read-transport'
5455import { inspectToolResultForCopilot } from '@/lib/mothership/request/tools/resolved-secret-result'
5556import { executeSimCli } from '@/lib/mothership/tools/handlers/sim-cli'
57+ import { reportWorkspaceFileDelivery } from '@/lib/workspace-files/application/file-delivery-observer'
5658import { readWorkspaceFileArtifact } from '@/lib/workspace-files/application/read-workspace-file-artifact'
5759import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
5860
@@ -98,17 +100,48 @@ function registry() {
98100 return new ResolvedSecretTraceRegistry ( [ ] , { userId : 'reader' , workspaceId : 'workspace' } )
99101}
100102
101- function classify ( status : 'exact' | 'unknown' , fileRevision = revision ) {
103+ type SidecarStatus = 'exact' | 'unknown'
104+
105+ function sidecarRow ( status : SidecarStatus , fileRevision = revision , entries : unknown [ ] = [ ] ) {
106+ return {
107+ fileContentUpdatedAt : fileRevision ,
108+ secretProvenanceVersion : 1 ,
109+ provenanceContentUpdatedAt : fileRevision ,
110+ status,
111+ entries,
112+ }
113+ }
114+
115+ /** Queues one sidecar per status, in the order the reads consume them. */
116+ function classify ( ...statuses : [ SidecarStatus , ...SidecarStatus [ ] ] ) {
102117 resetDbChainMock ( )
103- queueTableRows ( workspaceFiles , [
118+ for ( const status of statuses ) queueTableRows ( workspaceFiles , [ sidecarRow ( status ) ] )
119+ }
120+
121+ type CliInvocation = Parameters < typeof executeSimCli > [ 0 ] [ 'request' ] [ 'invocation' ]
122+
123+ const readFile : CliInvocation = {
124+ kind : 'augmentation' ,
125+ name : 'files read' ,
126+ positionals : [ 'file' ] ,
127+ flags : { } ,
128+ }
129+
130+ function grepFiles ( pattern : string ) : CliInvocation {
131+ return { kind : 'augmentation' , name : 'grep' , positionals : [ pattern ] , flags : { scope : 'files' } }
132+ }
133+
134+ function runCli ( invocation : CliInvocation , trace = registry ( ) ) {
135+ return executeSimCli (
136+ { request : { invocation } } ,
104137 {
105- fileContentUpdatedAt : fileRevision ,
106- secretProvenanceVersion : 1 ,
107- provenanceContentUpdatedAt : fileRevision ,
108- status ,
109- entries : [ ] ,
110- } ,
111- ] )
138+ userId : 'reader' ,
139+ workspaceId : 'workspace' ,
140+ workflowId : '' ,
141+ chatId : 'chat' ,
142+ resolvedSecretTraceRegistry : trace ,
143+ }
144+ )
112145}
113146
114147function readTransport ( trace = registry ( ) ) {
@@ -278,14 +311,53 @@ describe('file provenance at the actual CLI and model-result boundary', () => {
278311 expect ( mocks . stream ) . toHaveBeenCalledTimes ( 1 )
279312 } )
280313
281- it ( 'withholds extracted text whose canonical revision changed' , async ( ) => {
282- classify ( 'exact' , new Date ( revision . getTime ( ) + 1 ) )
314+ it ( 'refuses a forwarded response naming its unknown file, without blanking the invocation' , async ( ) => {
315+ const refusals = getMockLogger ( 'DurableSecretProvenancePersistence' ) . error
316+ refusals . mockClear ( )
317+ const trace = registry ( )
318+ const transport = createFileReadTransport ( {
319+ endpoint : 'https://sim.test' ,
320+ userId : 'reader' ,
321+ registry : trace ,
322+ invocation : { userId : 'reader' , workspaceId : 'workspace' , chatId : 'chat' } ,
323+ transport : async ( ) => {
324+ await reportWorkspaceFileDelivery ( { status : 'exact' , entries : [ ] } , 'clean' )
325+ await reportWorkspaceFileDelivery ( { status : 'unknown' } , 'tainted' )
326+ return new Response ( content )
327+ } ,
328+ } )
329+ const response = await transport (
330+ new Request ( 'https://sim.test/api/v2/files/bulk-download?workspaceId=workspace' )
331+ )
332+ expect ( response . status ) . toBe ( 403 )
333+ expect ( await response . text ( ) ) . not . toContain ( content )
334+ expect ( trace . isPermanentlyIncomplete ( ) ) . toBe ( false )
335+ expect ( refusals ) . toHaveBeenCalledTimes ( 1 )
336+ expect ( refusals ) . toHaveBeenCalledWith (
337+ 'Refusing unavailable durable secret provenance' ,
338+ expect . objectContaining ( { resourceId : 'tainted' } )
339+ )
340+ } )
341+
342+ it ( 'refuses extracted text whose canonical revision changed before loading its bytes' , async ( ) => {
343+ resetDbChainMock ( )
344+ queueTableRows ( workspaceFiles , [ sidecarRow ( 'exact' , new Date ( revision . getTime ( ) + 1 ) ) ] )
283345 const trace = registry ( )
284346 const response = await readTransport ( trace ) ( fileRequest ( '/text' ) )
347+ expect ( response . status ) . toBe ( 403 )
348+ expect ( await response . text ( ) ) . not . toContain ( content )
349+ expect ( mocks . buffer ) . not . toHaveBeenCalled ( )
350+ expect ( trace . isPermanentlyIncomplete ( ) ) . toBe ( false )
351+ } )
352+
353+ it ( 'still withholds a delivered text read once the registry is incomplete for another reason' , async ( ) => {
354+ classify ( 'exact' )
355+ const trace = registry ( )
356+ trace . markIncomplete ( 'provenance-capacity-exceeded' )
357+ const response = await readTransport ( trace ) ( fileRequest ( '/text' ) )
285358 expect ( response . status ) . toBe ( 200 )
286359 const output = await response . text ( )
287360 expect ( output ) . toContain ( content )
288- expect ( trace . isPermanentlyIncomplete ( ) ) . toBe ( true )
289361 expect (
290362 JSON . stringify (
291363 inspectToolResultForCopilot ( { success : true , output } , trace , 'sim_cli' ) . result
@@ -366,62 +438,83 @@ describe('file provenance at the actual CLI and model-result boundary', () => {
366438 classify ( 'unknown' )
367439 const grep = async ( ) => {
368440 const trace = registry ( )
369- const result = await executeSimCli (
370- {
371- request : {
372- invocation : {
373- kind : 'augmentation' ,
374- name : 'grep' ,
375- positionals : [ content ] ,
376- flags : { scope : 'files' } ,
377- } ,
378- } ,
379- } ,
380- {
381- userId : 'reader' ,
382- workspaceId : 'workspace' ,
383- workflowId : '' ,
384- chatId : 'chat' ,
385- resolvedSecretTraceRegistry : trace ,
386- }
441+ const result = await runCli ( grepFiles ( content ) , trace )
442+ expect ( result . error ) . toContain ( 'Search incomplete' )
443+ expect ( JSON . stringify ( result . output ) ) . toContain (
444+ 'files/private.txt (file): could not read text'
387445 )
388- expect ( result . success ) . toBe ( true )
389- expect ( JSON . stringify ( result . output ) ) . toContain ( content )
390- expect (
391- JSON . stringify ( inspectToolResultForCopilot ( result , trace , 'sim_cli' ) . result )
392- ) . not . toContain ( content )
446+ expect ( JSON . stringify ( result . output ) ) . toContain ( 'No matches' )
447+ expect ( trace . isPermanentlyIncomplete ( ) ) . toBe ( false )
393448 }
394449 if ( mode === 'parallel' ) await Promise . all ( [ grep ( ) , grep ( ) ] )
395450 else {
396451 await grep ( )
397452 await grep ( )
398453 }
399- expect ( mocks . buffer ) . toHaveBeenCalledTimes ( 2 )
454+ expect ( mocks . buffer ) . not . toHaveBeenCalled ( )
400455 }
401456 )
402457
403- it ( 'carries unified text reads through the actual CLI transport and secret projection' , async ( ) => {
404- classify ( 'unknown' )
458+ it ( 'returns clean grep matches while refusing only the file whose provenance is unknown' , async ( ) => {
459+ const tainted = { ...file , id : 'tainted' , key : 'workspace/workspace/tainted-key' }
460+ const taintedText = 'TAINTED_FILE_CANARY_FOR_LOCAL_TEST'
461+ const byId = ( id : string ) => ( id === tainted . id ? tainted : file )
462+ mocks . file . mockImplementation ( async ( _workspaceId : string , id : string ) => byId ( id ) )
463+ mocks . reference . mockImplementation ( async ( _workspaceId : string , id : string ) => byId ( id ) )
464+ mocks . context . mockImplementation ( async ( id : string ) => ( {
465+ workspaceId : 'workspace' ,
466+ fileId : id ,
467+ workspaceOrganizationId : null ,
468+ allowPersonalApiKeys : true ,
469+ billedAccountUserId : 'owner' ,
470+ } ) )
471+ mocks . buffer . mockImplementation ( async ( record : { id : string } ) =>
472+ Buffer . from ( record . id === tainted . id ? taintedText : content )
473+ )
474+ vi . stubGlobal ( 'fetch' , async ( ) =>
475+ Response . json ( {
476+ data : [ file , tainted ] . map ( ( record ) => ( { ...record , updatedAt : revision . toISOString ( ) } ) ) ,
477+ nextCursor : null ,
478+ } )
479+ )
480+ classify ( 'exact' , 'unknown' )
481+ const refusals = getMockLogger ( 'DurableSecretProvenancePersistence' ) . error
482+ refusals . mockClear ( )
405483 const trace = registry ( )
406- const result = await executeSimCli (
407- {
408- request : {
409- invocation : {
410- kind : 'augmentation' ,
411- name : 'files read' ,
412- positionals : [ 'file' ] ,
413- flags : { } ,
414- } ,
415- } ,
416- } ,
417- {
418- userId : 'reader' ,
419- workspaceId : 'workspace' ,
420- workflowId : '' ,
421- chatId : 'chat' ,
422- resolvedSecretTraceRegistry : trace ,
423- }
484+ const result = await runCli ( grepFiles ( 'CANARY_FOR_LOCAL_TEST' ) , trace )
485+ const projected = JSON . stringify ( inspectToolResultForCopilot ( result , trace , 'sim_cli' ) . result )
486+ expect ( trace . isPermanentlyIncomplete ( ) ) . toBe ( false )
487+ expect ( projected ) . not . toContain ( taintedText )
488+ expect ( projected ) . toContain ( content )
489+ expect ( mocks . buffer ) . toHaveBeenCalledTimes ( 1 )
490+ expect ( refusals ) . toHaveBeenCalledTimes ( 1 )
491+ expect ( refusals ) . toHaveBeenCalledWith (
492+ 'Refusing unavailable durable secret provenance' ,
493+ expect . objectContaining ( {
494+ cause : 'workspace-file-provenance-unavailable' ,
495+ resourceId : tainted . id ,
496+ } )
424497 )
498+ } )
499+
500+ it ( 'refuses an unknown file through the actual CLI text read without loading its bytes' , async ( ) => {
501+ classify ( 'unknown' )
502+ const result = await runCli ( readFile )
503+ expect ( result . success ) . toBe ( false )
504+ expect ( JSON . stringify ( result . output ) ) . not . toContain ( content )
505+ expect ( JSON . stringify ( result ) ) . toContain ( 'secret provenance is unavailable' )
506+ expect ( mocks . buffer ) . not . toHaveBeenCalled ( )
507+ } )
508+
509+ it ( 'carries unified text reads through the actual CLI transport and secret projection' , async ( ) => {
510+ resetDbChainMock ( )
511+ queueTableRows ( workspaceFiles , [
512+ sidecarRow ( 'exact' , revision , [
513+ { name : 'FILE_SECRET' , encryptedValue : 'fixture-ciphertext' , sourceUserId : 'reader' } ,
514+ ] ) ,
515+ ] )
516+ const trace = registry ( )
517+ const result = await runCli ( readFile , trace )
425518 expect ( result . success ) . toBe ( true )
426519 expect ( JSON . stringify ( result . output ) ) . toContain ( content )
427520 expect ( JSON . stringify ( result . output ) ) . toContain ( 'representation' )
0 commit comments