2026-07-27 12:54:54 +08:00
/**
2026-07-28 21:31:17 +08:00
* Assembled-app regression: a parent-only read-only override is seeded into
* its child log and confines a real write under a wider deployment default.
2026-07-27 12:54:54 +08:00
*/
import { readFile , readdir , writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { Context } from 'cordis'
import { normalizeSessionLog , scrubRequestHeaders , type NormalizeContext } from '@deepseek-ai/dsh-acp-snapshot'
import { LOADER_SMOKE_TEST_TIMEOUT_MS , runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
2026-07-28 21:11:20 +08:00
import { createUserMessage } from '@deepseek-ai/dsh-llm'
2026-07-27 12:54:54 +08:00
import SessionStore , { SESSION_FORMAT_VERSION , SessionId , type SessionEvent , type SessionHeader } from '@deepseek-ai/dsh-session'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import { describe , expect , it } from 'vitest'
const fixtureDir = fileURLToPath ( new URL ( './subagent-inheritance-snapshots/parent-override' , import . meta . url ) )
const replayOverride = join ( fixtureDir , 'replay.override.json' )
const childReplay = join ( fixtureDir , 'child.replay.jsonl' )
const parentExpected = join ( fixtureDir , 'parent.expected.jsonl' )
const childExpected = join ( fixtureDir , 'child.expected.jsonl' )
const configPath = fileURLToPath ( new URL ( '../subagent-inheritance.cordis.snapshot.yml' , import . meta . url ) )
const binScript = fileURLToPath ( new URL ( '../../../packages/examples/cli-demo/src/bin.ts' , import . meta . url ) )
const tsconfigPath = fileURLToPath ( new URL ( '../../../tsconfig.json' , import . meta . url ) )
const sessionId = SessionId ( 'subagent-inheritance-parent' )
const refreshing = process . env . DSH_SNAPSHOT === 'refresh'
const task = 'Delegate the write probe to a subagent.'
2026-07-28 21:31:17 +08:00
/** Seed a completed parent turn with the only read-only fact in the app. */
2026-07-27 12:54:54 +08:00
async function seedReadOnlyParent ( root : string , cwd : string ) : Promise < void > {
const ctx = new Context ( )
await ctx . plugin ( SessionStore )
await ctx . plugin ( SessionPersistenceJsonl , { root , compression : 'none' } )
const meta : SessionHeader = {
version : SESSION_FORMAT_VERSION ,
id : sessionId ,
createdAt : 1 ,
cwd ,
delegationDepth : 0 ,
}
const events : SessionEvent [ ] = [
2026-07-30 13:49:57 +08:00
{ type : 'turn/start' , seq : 0 , time : 10 , data : { turn : 1 } } ,
2026-07-28 21:11:20 +08:00
{ type : 'user/message' , seq : 1 , time : 11 , data : createUserMessage ( { content : [ { type : 'text' , text : 'Tighten this session to read-only.' } ] , source : { kind : 'user' } } ) , surfaceOp : 'append' } ,
2026-07-27 12:54:54 +08:00
{ type : 'sandbox/mode' , seq : 2 , time : 12 , data : { mode : 'read-only' } } ,
2026-08-04 14:09:52 +08:00
{ type : 'turn/end' , seq : 3 , time : 13 , data : { turn : 1 , reason : { kind : 'completed' } } } ,
2026-07-27 12:54:54 +08:00
]
try {
await ctx . sessionPersistence . create ( meta )
await ctx . sessionPersistence . append ( sessionId , events )
} finally {
await ctx . fiber . dispose ( )
}
}
describe ( 'parent-only override inheritance snapshot' , ( ) = > {
it ( 'confines a delegated child through the assembled headless app' , async ( ) = > {
let cwd = ''
const result = await runLoaderSmoke ( {
label : 'subagent inheritance headless stream-json snapshot' ,
tempDirPrefix : 'dsh-subagent-inherit-' ,
binScript ,
configPath ,
binArgs : [ '--config' , configPath , '--output-format' , 'stream-json' , task ] ,
tsconfigPath ,
env : {
// The primary fixture path must exist for llm-replay's config guard;
// the override sidecar fully replaces the derived parent script.
DSH_SNAPSHOT_FILE : replayOverride ,
DSH_SNAPSHOT_OVERRIDE : replayOverride ,
DSH_SNAPSHOT_CHILD_FILES : childReplay ,
} ,
prepare : async ( runCwd ) = > {
cwd = runCwd
await seedReadOnlyParent ( join ( runCwd , '.sessions' ) , runCwd )
} ,
inspect : async ( runCwd ) = > {
// THE physical fact: the child's write never reached the disk. Under
// the deployment default (workspace-write) alone it would succeed.
await expect ( readFile ( join ( runCwd , 'inherited.txt' ) , 'utf8' ) ) . rejects . toMatchObject ( { code : 'ENOENT' } )
// Collect both persisted logs (parent resumed turn + child run).
const sessionsDir = join ( runCwd , '.sessions' )
const files = ( await readdir ( sessionsDir , { recursive : true } ) ) . filter ( file = > file . endsWith ( '.jsonl' ) )
const logs = await Promise . all ( files . map ( async file = > readFile ( join ( sessionsDir , file ) , 'utf8' ) ) )
const headerOf = ( content : string ) : Record < string , unknown > = >
JSON . parse ( content . split ( '\n' ) [ 0 ] ? ? '{}' ) as Record < string , unknown >
const parent = logs . find ( content = > content . includes ( '"subagent-inheritance-parent"' ) )
const child = logs . find ( content = > typeof headerOf ( content ) . parentSession === 'string' )
if ( parent === undefined || child === undefined ) throw new Error ( 'missing persisted parent or child log' )
2026-07-28 21:31:17 +08:00
const childRecords = child . trimEnd ( ) . split ( '\n' ) . map (
line = > JSON . parse ( line ) as Record < string , unknown > ,
)
expect ( childRecords [ 1 ] ) . toMatchObject ( {
type : 'sandbox/mode' ,
seq : 0 ,
data : { mode : 'read-only' , source : 'delegation' } ,
} )
2026-07-27 12:54:54 +08:00
2026-07-30 22:15:34 +08:00
const runtimeContexts = ( content : string ) : string [ ] = > content . trimEnd ( ) . split ( '\n' ) . flatMap ( ( line ) = > {
const record = JSON . parse ( line ) as {
type ? : string
data ? : { source ? : { kind? : string ; plugin? : string } ; content? : Array < { type ? : string ; text? : unknown } > }
}
if ( record . type !== 'user/message'
|| record . data ? . source ? . kind !== 'plugin'
|| record . data . source . plugin !== '@deepseek-ai/dsh-system-prompt' ) return [ ]
return record . data . content ? . flatMap ( block = > block . type === 'text' && typeof block . text === 'string' ? [ block . text ] : [ ] ) ? ? [ ]
2026-07-30 18:51:29 +08:00
} )
2026-07-30 22:15:34 +08:00
const policyContexts = [ . . . runtimeContexts ( parent ) , . . . runtimeContexts ( child ) ]
expect ( policyContexts ) . toHaveLength ( 2 )
for ( const context of policyContexts ) {
2026-07-31 13:28:02 +08:00
expect ( context ) . toContain ( 'Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode.' )
expect ( context ) . toContain ( 'Do not refuse a required modification from this policy alone' )
expect ( context ) . not . toContain ( 'write and edit tools' )
2026-07-30 22:15:34 +08:00
expect ( context ) . not . toContain ( 'one-shot bash commands' )
expect ( context ) . not . toContain ( 'terminal sessions' )
2026-07-30 18:51:29 +08:00
}
2026-07-27 12:54:54 +08:00
const context : NormalizeContext = { sessionIds : [ sessionId , String ( headerOf ( child ) . id ) ] , cwd }
const normalizedParent = scrubRequestHeaders ( normalizeSessionLog ( parent , context ) )
const normalizedChild = scrubRequestHeaders ( normalizeSessionLog ( child , context ) )
if ( refreshing ) {
await writeFile ( parentExpected , normalizedParent )
await writeFile ( childExpected , normalizedChild )
}
expect ( normalizedParent ) . toBe ( await readFile ( parentExpected , 'utf8' ) )
expect ( normalizedChild ) . toBe ( await readFile ( childExpected , 'utf8' ) )
// The child's real write was denied by the real fence.
expect ( normalizedChild ) . toContain ( 'file access denied under read-only mode' )
} ,
} )
expect ( result . stderr ) . toBe ( '' )
const records = result . stdout . trimEnd ( ) . split ( '\n' ) . map ( line = > JSON . parse ( line ) as Record < string , unknown > )
expect ( records . at ( - 1 ) ) . toMatchObject ( {
type : 'result' ,
sessionId ,
2026-07-31 22:00:39 +08:00
output : 'The delegated child was denied by the sandbox. PARENT_DONE' ,
2026-07-27 12:54:54 +08:00
} )
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
} )