Skip to content

feat(#575): let a deployment switch the audience floor on - #734

Merged
Weegy merged 1 commit into
mainfrom
feat/575-grant-store
Aug 18, 2026
Merged

feat(#575): let a deployment switch the audience floor on#734
Weegy merged 1 commit into
mainfrom
feat/575-grant-store

Conversation

@Weegy

@Weegy Weegy commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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 audienceGrants to 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

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 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 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)". 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. Refusing is the safer wrong answer.

Blast radius

Surface Effect
Deployments that pass no audienceGrants none
Deployments that do the three guards begin enforcing the intersection
Published plugin contract none
Database none — grants come from the supplied store (InMemoryGrantStore ships; a persistent one is separate)

Verification

The #573 ratchet caught 10 new type errors in the new test on the first run, and they were fixed rather than 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. Worth recording because the error message blames the assignment, not the import path.

Where #575 stands

Piece State
ScopeId (phase 1) merged
Floor + grants (phase 2) merged #729
Guard 1 — egress merged #731
Guard 2 — context recall merged #732
Guard 3 — handle resolution merged #733
Switching it on this PR
Persistent (Postgres) grant store still to come — needs an admin surface to manage grants
Handle bound at minting needs channel-plugin storage

Refs #575


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

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
@Weegy
Weegy enabled auto-merge (squash) August 18, 2026 10:18
@Weegy
Weegy merged commit 4f1e164 into main Aug 18, 2026
9 checks passed
@Weegy
Weegy deleted the feat/575-grant-store branch August 19, 2026 06:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant