policy: guard adoption baselines, resolve-time validation, and inherited-delta narration

Review fixes (ds-review-bot on #623):

- Persistence adoption compares the immutable policy baselines: onCreated's
  ownerless claim and adoptLivePrefix retain the STORED header, so a
  same-id live session with a conflicting baseline now rejects as a
  collision instead of appending under read-only and resuming under the
  stored danger-full-access.
- resolve() resolves the session override BEFORE applying an explicit
  approved mode: the one-shot grant no longer bypasses the unconditional
  durable-header validation.
- The approval narrator attributes positionally over the session's OWN
  events (past the seed boundary): a fork child whose baseline delta has
  no own override narrates 'inherited from the delegating session' instead
  of misattributing a stale seed-carried switch to the user or the
  operator.

Red-first: baseline-conflict adoption in the shared coordinator contract
(both backends), resolve-with-explicit-mode validation, and the fork-child
narration attribution case.
This commit is contained in:
kingwl
2026-07-26 23:56:41 +08:00
parent 9aaa4a871f
commit 290e1acc45
9 changed files with 105 additions and 15 deletions

View File

@@ -517,6 +517,36 @@ describe('approval policy (the approval/policy fold)', () => {
expect(injected).toEqual(['The approval policy changed from "ask" to "never" (changed by the operator/config).'])
})
it('does not attribute a fork child\'s baseline delta to a stale seed-carried user switch', async () => {
// A fork child: the seed carries the parent's OLD 'ask' switch (event 0,
// inside seedLength) and the last request header told 'ask'; the header
// baseline captured at delegation is 'never'. The delta must not be
// attributed to "the user" — the seed switch is stale parent history, not
// this session's runtime action.
const ctx = new Context()
await ctx.plugin(ApprovalService)
const id = SessionId('sess-narr-fork-baseline')
const session = new Session(id, undefined, {
version: 0,
id,
createdAt: 0,
approvalPolicy: 'never',
seedLength: 2,
})
setApprovalPolicy(session, 'ask')
session.append('request/header', { header: { config: { provider: 'mock', model: 'mock' }, system: `persona\n${ASK_MARKER}` }, reason: 'initial' })
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
const injected: string[] = []
const agent = {
id,
session,
inject: (content: { type: string; text: string }[]) => { injected.push(content[0]?.text ?? '') },
} as unknown as Agent
await preStep(ctx, agent)
expect(injected).toEqual(['The approval policy changed from "ask" to "never" (inherited from the delegating session).'])
})
it('a pinned override survives a default change silently', async () => {
const ctx = new Context()
await ctx.plugin(ApprovalService, { policy: 'never' })