policy: scope seed-boundary slicing to delegation children

Review fix (ds-review-bot on #623): the unconditional slice regressed the
public SessionStore.fork path — a generic fork child gets seedLength but no
policy baseline, so slicing discarded its seed-carried sandbox/approval
switches and silently widened it to the deployment defaults (a forked
read-only/never parent produced a workspace-write/ask child).

overrideOf now branches on baseline presence: with a header baseline (a
delegation child) the fold covers only own post-seed switches — the
baseline captured from the parent's FULL log subsumes seed history; without
one, the whole log — seeded switches ARE the replayed inherited truth. The
permission preset fold scopes the same way. Red-first: generic-fork
seed-carried override tests in both policy suites.
This commit is contained in:
kingwl
2026-07-26 23:06:15 +08:00
parent b02d3e4c03
commit 99f5fab7bc
17 changed files with 78 additions and 41 deletions

View File

@@ -56,13 +56,17 @@ export function effectiveSandboxMode(events: readonly SessionEvent[]): SandboxMo
/**
* The session's complete sandbox-mode OVERRIDE chain — the one home every
* consumer (the policy service, the permission presets) resolves through:
* the fold of the session's OWN switches (events past the seed boundary — a
* fork seed's stale parent switch is subsumed by the delegation baseline
* captured after it), else the header's inherited baseline. Never the
* deployment default. The durable baseline is validated UNCONDITIONALLY — a
* corrupt or foreign header must fail loud on every read, not only when no
* own switch happens to shadow it.
* consumer (the policy service, the permission presets) resolves through.
* With a header baseline (a delegation child), the fold covers only the
* session's OWN switches past the seed boundary — the baseline was captured
* from the parent's FULL log at delegation, so any seed-carried switch is
* already subsumed by it, stale or not. Without a baseline (a top-level
* session, or a generic `SessionStore.fork` child that captured no policy
* meta), the fold covers the whole log: seeded switches ARE the replayed
* inherited truth, and slicing them away would silently widen the child to
* the deployment default. Never the deployment default itself. The durable
* baseline is validated UNCONDITIONALLY — a corrupt or foreign header must
* fail loud on every read, not only when no own switch happens to shadow it.
* @param session - the session whose override chain to resolve.
* @returns the effective override, or `undefined` for a session following
* the deployment default.
@@ -70,11 +74,12 @@ export function effectiveSandboxMode(events: readonly SessionEvent[]): SandboxMo
*/
export function sandboxOverrideOf(session: Session): SandboxMode | undefined {
const baseline = session.header.sandboxMode
if (baseline !== undefined && !SANDBOX_MODES.includes(baseline as SandboxMode)) {
if (baseline === undefined) return effectiveSandboxMode(session.events)
if (!SANDBOX_MODES.includes(baseline as SandboxMode)) {
throw new Error(`session header sandboxMode "${baseline}" is outside the closed mode vocabulary`)
}
const own = effectiveSandboxMode(session.events.slice(session.header.seedLength ?? 0))
return own ?? baseline as SandboxMode | undefined
return own ?? baseline as SandboxMode
}
/**