Skip to content

MCP 2b-1: session-to-agent binding + dual-key rotation#28

Merged
chaodu-agent merged 1 commit into
mainfrom
mcp-session-binding
Jul 13, 2026
Merged

MCP 2b-1: session-to-agent binding + dual-key rotation#28
chaodu-agent merged 1 commit into
mainfrom
mcp-session-binding

Conversation

@chaodu-agent

Copy link
Copy Markdown
Contributor

Summary

First slice of #17's [2b] gate items (per the delivery plan: 2b ships as a stacked sequence — this is the smallest, purely-local piece).

Changes

  • Session-to-agent binding: the session pin is now SessionPin { identity_id, agent_id }. A session ID presented by a different agent — even one holding a valid key of its own — gets 403 session not owned by this agent, audit-logged as a binding violation. Mode mismatches (a Phase 1 no-agent session presented in agent mode, or vice versa) are also rejected. The response does not disclose whether the session exists.
  • Dual-key rotation: [[mcp.agents]] accepts keys = [...] alongside (or instead of) key = ...; all entries are secret-ref-resolved and simultaneously valid, enabling add-new → roll-over → remove-old rotation with zero downtime. An agent with no keys at all fails config load loudly.
  • Pin audit line includes the owning agent: MCP session pinned to identity zz [agent=kiro-zz] [session=…].

Why binding matters even with per-request policy checks

Tool-allowlist enforcement is per-request against the presenting agent's policy, so there was no privilege escalation — but without binding, agent B could ride agent A's session (consuming its pinned identity/budget and polluting audit attribution). Now the session owner is part of the authz decision.

Tested

45 tests (3 new): cross-agent session rejection (valid key, wrong session → 403, upstream never sees it; rightful owner unaffected), mode-mismatch rejection, dual-key rotation (both keys valid, resolving to the same policy). Clippy -D warnings clean.

Remaining 2b slices

2b-2 write classification + repo allowlists → 2b-3 GitHub App backend (incl. session-credential lifetime) → 2b-4 fail-closed audit → 2b-5 enable writes.

First slice of #17's [2b] items:

- SessionPin { identity_id, agent_id } replaces the plain identity string
  in the session cache. A session ID presented by a different agent (or by
  a different mode: agent vs network-trust) is rejected with 403 — session
  IDs are not portable between agents.
- Dual keys: [[mcp.agents]] accepts keys = [...] alongside key = ... (both
  merged at load) so two keys can be simultaneously valid for zero-downtime
  rotation. Config load fails loudly when an agent has no key at all.
- Pin log line now includes the owning agent.

Closes the '[2b] Session-to-agent binding' gate item (the session-cannot-
outlive-credential half lands with the App backend slice) and the dual-key
rotation open question from 2a.

45 tests (3 new: cross-agent session rejection, mode-mismatch rejection,
rotation accepting both keys with identical policy).
@chaodu-agent chaodu-agent merged commit e127c85 into main Jul 13, 2026
1 check passed
@chaodu-agent chaodu-agent deleted the mcp-session-binding branch July 13, 2026 04:06
chaodu-agent added a commit that referenced this pull request Jul 13, 2026
chaodu-agent added a commit that referenced this pull request Jul 13, 2026
chaodu-agent added a commit that referenced this pull request Jul 13, 2026
* 2b-1: session-to-agent binding + dual-key rotation

First slice of #17's [2b] items:

- SessionPin { identity_id, agent_id } replaces the plain identity string
  in the session cache. A session ID presented by a different agent (or by
  a different mode: agent vs network-trust) is rejected with 403 — session
  IDs are not portable between agents.
- Dual keys: [[mcp.agents]] accepts keys = [...] alongside key = ... (both
  merged at load) so two keys can be simultaneously valid for zero-downtime
  rotation. Config load fails loudly when an agent has no key at all.
- Pin log line now includes the owning agent.

Closes the '[2b] Session-to-agent binding' gate item (the session-cannot-
outlive-credential half lands with the App backend slice) and the dual-key
rotation open question from 2a.

45 tests (3 new: cross-agent session rejection, mode-mismatch rejection,
rotation accepting both keys with identical policy).

* 2b-2: write classification + per-agent repo allowlists

Second slice of #17's [2b] items (stacked on 2b-1):

- src/policy.rs: rule-based tool classification — Read iff get_*/list_*/
  search_*/*_read (matches the observed /readonly surface), everything
  else including unknown names is Write (conservative: misclassifying a
  read is an inconvenience, misclassifying a write would be a security
  bug). Repo resolution from tool arguments (owner+repo) and allowlist
  matching (owner/repo exact, owner/* wildcard, case-insensitive,
  malformed entries fail closed).
- Enforcement for authenticated agents on tools/call, in order:
  tool allowlist -> write classification (ALL writes 403 until 2b-5's
  enable switch, even if allowlisted — closes the operator-typo gap) ->
  repo allowlist with deny-if-unresolvable.
- repos = [] means unrestricted (additional constraint, keeps 2a configs
  working). Phase 1 no-agent mode is untouched (readonly upstream is its
  write barrier).
- Audit lines now include the resolved repo: MCP tools/call issue_read
  repo=openabdev/ghpool [agent=... via ...]

59 tests (14 new: 7 policy unit, 7 integration incl. allowlisted-write
blocked, wildcard match, deny-unresolvable, 2a backward compat).

* Merge main (squash-merged #28)

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
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