Skip to content

Gatekeeper F-A: shared durable session-identity primitive (P1-4 / Task 0.5d) - #145

Merged
joslat merged 2 commits into
mainfrom
feat/gatekeeper-session-identity
Jul 25, 2026
Merged

Gatekeeper F-A: shared durable session-identity primitive (P1-4 / Task 0.5d)#145
joslat merged 2 commits into
mainfrom
feat/gatekeeper-session-identity

Conversation

@joslat

@joslat joslat commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

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 fresh AgentSession object resets the counter, which is a bypass. F3 added a per-gate sessionKeySelector to RateLimitGate; this generalizes it into one shared primitive configured once.

  • SessionIdentity helperFromStateBag(key) resolves a durable id from the session StateBag (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.
  • ISessionIdentityAwareUseGatekeeper 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 ⇒ 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: FromStateBag resolve/absent/empty-key-throws; Combine first-non-empty/all-empty/validation; UseGatekeeper injection 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

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
Copilot AI review requested due to automatic review settings July 25, 2026 10:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…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
Copilot AI review requested due to automatic review settings July 25, 2026 10:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@joslat
joslat merged commit 4062c72 into main Jul 25, 2026
15 checks passed
@joslat
joslat deleted the feat/gatekeeper-session-identity branch July 25, 2026 10:53
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.

2 participants