Gatekeeper F-A: shared durable session-identity primitive (P1-4 / Task 0.5d) - #145
Merged
Conversation
Generalizes F3's per-gate RateLimitGate sessionKeySelector into one shared primitive, so a deployment configures "how do I identify a session across reloads / across workers" ONCE instead of per gate — and provides the identity the future containment track keys ContainmentTarget.Session on (F-A foundation; Phase 0.5 Task 0.5d of the master Gatekeeper plan). - SessionIdentity helper: FromStateBag(key) resolves a durable id from the session StateBag; Combine(...) chains resolvers (first non-empty). A null/empty result ⇒ the gate falls back to object identity for that session (partial rollout degrades gracefully). - GatekeeperOptions.SessionIdentity (Func<AgentSession,string?>?): the shared resolver. - ISessionIdentityAware: UseGatekeeper injects the shared resolver into every gate implementing it, set-once so an explicit per-gate selector always wins. RateLimitGate implements it. - Non-breaking: default null ⇒ pre-F-A object-identity behavior unchanged. 10 tests (FromStateBag resolve/absent/validation, Combine first-non-empty/all-empty/validation, UseGatekeeper injection makes rate-limit survive reload, no-identity resets, explicit-wins). Full net10 8483/0/2, net8 clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01221nyEWUvsPQSR3AmJ3Lro
…ance Adversarial audit found no HIGH; the logic is sound (default path unchanged, throwing resolvers fail-closed via the run-gate Block-on-exception, injection completes before any run, explicit selector wins). Two contract/doc findings hardened: - (MED) UseSessionIdentityDefault is first-write-wins on a now-mutable field: a SINGLE gate instance shared across two UseGatekeeper configs with different SessionIdentity resolvers keeps only the first, silently dropping the second (that config would fall back to object identity). Documented as an explicit contract — one gate instance per configuration (the normal, tested pattern), or an explicit per-gate selector. - (LOW) SessionIdentity.FromStateBag doc suggested "a request header" as an id source — a client-controlled id lets an attacker rotate it to mint a fresh counter and defeat the cap (and grow the per-id table unboundedly). Replaced with a server-attested-id-only caveat. Doc-only (no logic change); 17 F-A/session-gate tests still pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01221nyEWUvsPQSR3AmJ3Lro
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.
F-A foundation — the shared durable session-identity primitive. This is Task 0.5d of the master Gatekeeper plan (the last open item of Phase 0.5) and the deferred P1-4 from the Fable-5 remediation. Non-breaking; additive. Full net10 suite: 8483 pass / 0 fail / 2 skip; net8 clean.
What & why
RateLimitGate(and the future containment track) key per-session state on a durable logical session id so a cap survives a persisted-session reload or a logical session load-balanced across workers — otherwise a freshAgentSessionobject resets the counter, which is a bypass. F3 added a per-gatesessionKeySelectortoRateLimitGate; this generalizes it into one shared primitive configured once.SessionIdentityhelper —FromStateBag(key)resolves a durable id from the sessionStateBag(where a host stashes an auth claim / request header / store key);Combine(...)chains resolvers (first non-empty). A null/empty result ⇒ the gate falls back to object identity for that session (partial rollout degrades gracefully).GatekeeperOptions.SessionIdentity— the shared resolver, configured once.ISessionIdentityAware—UseGatekeeperinjects the shared resolver into every gate implementing it, set-once so an explicit per-gate selector always wins.RateLimitGateimplements it.null⇒ the pre-F-A object-identity behavior is unchanged.This is the identity primitive the Phase-3
ContainmentTarget.Session(id)will key on (built here once, per D6).Tests
10 tests:
FromStateBagresolve/absent/empty-key-throws;Combinefirst-non-empty/all-empty/validation;UseGatekeeperinjection makes the rate-limit survive a reload; no-identity resets (contrast); explicit per-gate selector wins over the injected default.An adversarial audit of this diff (fail-open / thread-safety focus) was run before merge; findings, if any, are addressed in follow-up commits.
🤖 Generated with Claude Code