feat(#575): let a deployment switch the audience floor on - #734
Merged
Conversation
The piece that makes the rest reachable. The floor (#729), the grants (#729) and all three guards (#731, #732, #733) are merged but INERT: nothing installs an audience source, so every guard short-circuits. Passing `audienceGrants` to the orchestrator now builds a provider per turn and enforcement begins. An explicit opt-in rather than a default, and deliberately so: the floor fails closed, so a deployment that has not yet decided who may do what would find its rooms bounded by an empty grant table. Omit the option and behaviour is unchanged — which the full suite demonstrates, since none of its 6662 tests passes one. The chain, per evaluation: roster (ChatParticipantsProvider) -> Principal per participant (#333 phase 1, via the same knowledge-graph join `resolveTurnOwnerIdentity` uses) -> roles (#333 phase 2) -> capabilities (#575 grants) -> the intersection (#575 floor) This module adds NO policy of its own. Every failure mode along that chain was already made explicit by the layer that owns it — an unreadable role source yields no capability set, an unplaceable participant yields `unresolved`, an empty roster yields `unknown` — and each of them closes the room with a reason. It only wires. IT DELIBERATELY DOES NOT CACHE. The egress guard re-evaluates per tool call precisely so a participant who joins mid-turn narrows the floor before the next call fires. Memoizing the roster for a turn would make that re-evaluation theatre — the guard would keep re-asking and keep getting the turn's opening answer. Caching is not forbidden, it is somebody else's job: `ChatParticipantsProvider` already documents its accessor as "expected to be cheap (cached by the implementer)", and a channel adapter knows when its roster goes stale. This module does not. A turn with no `channelIdentity` resolves to no principals rather than defaulting to a plausible channel kind. A wrong kind resolves against a DIFFERENT identity cluster, which would hand the room somebody else's grants — worse than refusing. Mutation-checked: memoizing the provider kills both no-cache tests, including "a mid-turn joiner narrows the floor on the NEXT evaluation". The #573 ratchet caught 10 new type errors in the new test on the first run and they were FIXED, not baselined: the test imported the SDK through the source barrel while the provider resolves it through `dist`, and TypeScript treats the two as nominally different classes. Full suite 6662 tests / 0 fail / 0 cancelled. Typecheck, lint, the #470 decoupling ratchet (3294) and the #573 ratchet (406/406, unchanged) green. Refs #575
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The piece that makes the rest reachable.
The floor and grants (#729) and all three guards (#731, #732, #733) are merged but inert — nothing installs an audience source, so every guard short-circuits. Passing
audienceGrantsto the orchestrator now builds a provider per turn and enforcement begins.Why an opt-in and not a default
The floor fails closed by design. A deployment that has not yet decided who may do what would otherwise find its rooms bounded by an empty grant table — every tool refused, no context recalled, no attachment readable. So it is switched on deliberately, or not at all.
Omit the option and behaviour is unchanged, which the suite demonstrates rather than asserts: none of its 6662 tests passes one.
The chain, per evaluation
This module adds no policy of its own. Every failure along that chain was already made explicit by the layer that owns it — an unreadable role source yields no capability set, an unplaceable participant yields
unresolved, an empty roster yieldsunknown— and each closes the room with a reason. It only wires.It deliberately does not cache
The egress guard re-evaluates per tool call precisely so a participant who joins mid-turn narrows the floor before the next call fires. Memoizing the roster for a turn would make that re-evaluation theatre — the guard would keep re-asking and keep getting the turn's opening answer.
Caching is not forbidden, it is somebody else's job:
ChatParticipantsProvideralready documents its accessor as "expected to be cheap (cached by the implementer)". A channel adapter knows when its roster goes stale; this module does not.A turn with no
channelIdentityresolves to no principals rather than defaulting to a plausible channel kind — a wrong kind resolves against a different identity cluster, which would hand the room somebody else's grants. Refusing is the safer wrong answer.Blast radius
audienceGrantsInMemoryGrantStoreships; a persistent one is separate)Verification
npm run typecheck✅ ·npm run lint✅ · Epic: Integrated dev platform — isolated per-job runners that clone, develop, test, and ship PRs #470 ratchet 3294 ✅ ·npm run typechecknever typechecksmiddleware/test/#573 ratchet 406/406 ✅ (unchanged — see below)Where #575 stands
ScopeId(phase 1)Refs #575
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.