subagent: carry inherited policy overrides in the child session header
Review fix (ds-review-bot critical #2 on #623): the first-turn event stamp had a durability hole no turn anchoring can close — an idle SessionStart- style injection persists a complete one-shot turn before any prompt turn opens, so a crash in that window left a resumable-looking child with no inherited policy, falling back to a possibly wider deployment default. The captured overrides now ride the child's creation meta into its immutable SessionHeader (sandboxMode/approvalPolicy, neutral strings at the session boundary — the delegationDepth precedent), durable from the moment the session exists: no listener ordering can starve the baseline and no crash window can lose it. overrideOf(session) on both policy services resolves fold(events past header.seedLength) ?? header baseline, validating against the closed vocabulary on read; stampOverride and the prompt-submit listener machinery are deleted. The header field rides both persistence backends (JSONL header line; SQLite sessions columns, SCHEMA_VERSION 11 — pre-release, no migration). pty-local reads through overrideOf so PTY spawns see the baseline too. Red-first: header-durability-before-any-turn test (the injection crash window shape), baseline/seed-boundary/closed-vocabulary contract tests in both service suites; the real-wall suite (race, veto, fork stale-seed, grandchild) re-anchored on header assertions and green. The Agent Note's Alternatives now records the superseded event-stamping iteration with the review evidence; bilingual docs updated.
This commit is contained in:
@@ -20,6 +20,9 @@ function fakeAgent(seed: Array<{ type: string }> = [{ type: 'turn/start' }, { ty
|
||||
const agent = {
|
||||
session: {
|
||||
events: seed,
|
||||
// The typed Session contract the service folds over includes the header
|
||||
// (seed boundary + inherited baselines); the stub carries a bare one.
|
||||
header: { version: 0, id: 'fake-session', createdAt: 0 },
|
||||
append: (type: string, data: Record<string, unknown>) => {
|
||||
appended.push({ type, data })
|
||||
return { type, data } as unknown as SessionEvent
|
||||
@@ -577,14 +580,24 @@ describe('approval policy (the approval/policy fold)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('delegation inheritance (overrideOf + stampOverride)', () => {
|
||||
const policyEvents = (session: Session) => session.events.filter(e => e.type === 'approval/policy')
|
||||
|
||||
describe('delegation inheritance (overrideOf over the header baseline)', () => {
|
||||
function bareSession(id: string): Session {
|
||||
return new Session(SessionId(id))
|
||||
}
|
||||
|
||||
it('overrideOf folds to the LAST override and never falls back to the configured default', async () => {
|
||||
/** A session whose header carries the delegation-inheritance baseline. */
|
||||
function inheritedSession(id: string, meta: { approvalPolicy?: string; seedLength?: number } = {}): Session {
|
||||
const sessionId = SessionId(id)
|
||||
return new Session(sessionId, undefined, {
|
||||
version: 0,
|
||||
id: sessionId,
|
||||
createdAt: 0,
|
||||
...meta.approvalPolicy === undefined ? {} : { approvalPolicy: meta.approvalPolicy },
|
||||
...meta.seedLength === undefined ? {} : { seedLength: meta.seedLength },
|
||||
})
|
||||
}
|
||||
|
||||
it('overrideOf folds the session log and never falls back to the configured default', async () => {
|
||||
const ctx = await mounted()
|
||||
const parent = bareSession('sess-appr-inherit-parent')
|
||||
setApprovalPolicy(parent, 'never')
|
||||
@@ -593,24 +606,34 @@ describe('delegation inheritance (overrideOf + stampOverride)', () => {
|
||||
expect(ctx.approval.overrideOf(bareSession('sess-appr-unswitched'))).toBeUndefined()
|
||||
})
|
||||
|
||||
it('stampOverride appends the captured policy through the canonical write path', async () => {
|
||||
it('overrideOf reads the header baseline when the log has no own switch, and effectivePolicy follows', async () => {
|
||||
const ctx = await mounted()
|
||||
const child = bareSession('sess-appr-inherit-child')
|
||||
const child = inheritedSession('sess-appr-baseline', { approvalPolicy: 'never' })
|
||||
|
||||
ctx.approval.stampOverride(child, 'never')
|
||||
|
||||
const stamped = policyEvents(child)
|
||||
expect(stamped).toHaveLength(1)
|
||||
expect(stamped[0]?.data).toEqual({ policy: 'never' })
|
||||
expect(ctx.approval.overrideOf(child)).toBe('never')
|
||||
// The request path consumes the same chain: an inherited 'never' rejects
|
||||
// deterministically before any answerer could run.
|
||||
child.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
const agent = { session: child } as unknown as Agent
|
||||
await expect(ctx.approval.request({ agent, toolName: 'echo' })).resolves.toBe('rejected')
|
||||
})
|
||||
|
||||
it('stampOverride skips a child already folding to the policy (fork-seed dedup)', async () => {
|
||||
it('a seed-carried stale switch loses to the baseline; an OWN later switch wins over it', async () => {
|
||||
const ctx = await mounted()
|
||||
const child = bareSession('sess-appr-dedup-child')
|
||||
setApprovalPolicy(child, 'never')
|
||||
const child = inheritedSession('sess-appr-slice', { approvalPolicy: 'never', seedLength: 1 })
|
||||
// Event 0 sits inside the seed boundary — stale parent history, subsumed
|
||||
// by the delegation-time baseline.
|
||||
setApprovalPolicy(child, 'ask')
|
||||
expect(ctx.approval.overrideOf(child)).toBe('never')
|
||||
// Event 1 is the child's OWN switch — it outranks the baseline.
|
||||
setApprovalPolicy(child, 'ask')
|
||||
expect(ctx.approval.overrideOf(child)).toBe('ask')
|
||||
})
|
||||
|
||||
ctx.approval.stampOverride(child, 'never')
|
||||
it('rejects a header baseline outside the closed policy vocabulary (durable boundary)', async () => {
|
||||
const ctx = await mounted()
|
||||
const child = inheritedSession('sess-appr-invalid', { approvalPolicy: 'always' })
|
||||
|
||||
expect(policyEvents(child)).toHaveLength(1)
|
||||
expect(() => ctx.approval.overrideOf(child)).toThrow(/approvalPolicy/)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user