Skip to main content

Gateway — Per-Agent Binding & Delta Map (WIP)

Audience: an engineer who read the ADR-0022 design thread and wants to know where per-agent configuration is anchored and which of the "proposed / new" items actually shipped.

Scope of THIS doc: the narrow angle — where does "per-agent" get configured, plus the delta map from the design conversation to current main. It does not re-explain the two-hop auth model, the edge PEP, egress guard, or onboarding — those live in the full picture: Governed MCP & the Team Gateway. Read that first if you want the end-to-end system.

Status legend (same as the sibling doc): 🟢 LIVE · 🟡 STUBBED · 🔵 DEV-ONLY · 🟠 PENDING/PARKED.


Changelog since 2026-07-06 (imported from Knowledge-Base Google Doc)

The original tab was a design conversation captured while ADR-0022 was still awaiting-approval — it framed credential_mode, the deny-floor override, and agent-scoped policy as proposed decisions. Since then the ADR was approved (founder ruling 2026-07-10) and the whole model shipped. This doc replaces the "proposed / next step: dispatch architect" framing with verified status.

  • Resolved: credential_mode on the binding, require_per_agent fail-closed, agent-level guardrail scope, deny-floor invariant, and per-agent narrow-only tool selection are all merged and LIVE (verified below).
  • Superseded: the "I'm dispatching the architect / PM, awaiting your sign-off" narrative — ADR-0022 (#1732) and PRD #1681 v1.1 are approved; the implementation issues (#1739/#1740/#1742/#1744/#1736) merged.
  • Corrected: the delta-map "New" rows are no longer new.

Where does "per-agent" get configured? → At the binding, not at registration

This was the base's open question. Code confirms the design answer.

Registration declares a fact about the upstream (what auth it supports). The choice of shared vs individual credentials is a per-team decision, so it lives on the binding rowmcp_server_units, i.e. (server × team):

  • mcp_server_units.credential_modeteam_shared (default) | per_agent (migration 143; internal/mcpserver/binding.go).
  • mcp_server_units.require_per_agent — the fail-closed switch.

Same server bound to two teams can run different modes — exactly the "some teams per-agent, some per-team" requirement, with no server-level either/or. The mode is editable after the fact via UpdateMCPServerBinding (SetCredentialMode / SetRequirePerAgent, ADR-0022 W2 #1739) — it is not frozen at registration. 🟢 LIVE

Nuance vs the base: RegisterMCPServer does accept an initial credential_mode (it creates the first binding), but the secret itself no longer travels in the register form — the ref is left empty for a separate SetMCPServerCredential write (service.go: "secret leaves the ref empty…"). So the base's "kill the register-time token" is done at the backend; the org-wide _default slot survives only as the final backward-compat fallback.

Credential resolution (most-specific-wins) — verified

Creds live in the vault keyed by the team_id column at three grains (internal/mcpserver/credential/scope.go):

GrainVault team_idWritten by
org_defaultlegacy slot (both scope ids absent)
team<unit_id>scoped SetMCPServerCredential
agent_agent_<agent_id>scoped write; deleted on agent offboard (AgentScopeTeamID)

Resolver precedence (credential/resolve.go, CredentialSlots):

  • require_per_agent = true[_agent_<id>] only; empty agent_idempty slice = hard denial (never a silent org-key fallback). 🟢
  • credential_mode = per_agent[_agent_<id>?, unit?, _default]. 🟢
  • credential_mode = team_shared (default) → [unit?, _default]. 🟢

So: per-server/binding by default, optionally per-team, optionally per-agent — and per-agent can be made mandatory (fail-closed). The passthrough ban is untouched: resolution only selects which gateway-owned key is dereferenced, never the client's token.


Delta map — design conversation → shipped code

The base tagged four items New/proposed. All four are now LIVE.

PieceBase statusVerified current status
Auth-mode-only registration (secret out of register form)"UI change (#530), backend RPC already separate"🟢 backend LIVE — secret written via scoped SetMCPServerCredential; register leaves ref empty. UI rework tracked in #530 🟡
Team bindings + approvalExists🟢 LIVE (binding.go, UpdateMCPServerBinding / decideBinding)
Org→team guardrail cascadeExists🟢 LIVE (mcp_tool_policies, unit_id NULL = org-wide)
Agent-level guardrail scopeNew (schema + resolver)🟢 LIVE — migration 142 adds nullable agent_id to mcp_tool_policies; resolveQuery gives agent ▸ team ▸ org precedence
Per-team credentialsVault ready; write-RPC + UI new🟢 backend LIVE — three-grain scoped write (credential/scope.go, #1740)
Per-agent credentials + fail-closed modeNew (keying + credential_mode + lifecycle)🟢 LIVE_agent_<id> keying + credential_mode + require_per_agent + offboard cascade (#1742/#1740)
Team-page gateway section (badge, matrix, guardrail editor)Frontend new; reads existing tables🟢 backend/tables LIVE; SPA team-gateway/*.jsx present
Auto-derived team endpointmig-131 reusable; auto-derivation new🟢 LIVE — /mcp/team/{unit} is the PEP

The three governance decisions the base flagged as "proposed" — now resolved

  1. credential_mode placement → on the binding. ✅ Shipped exactly as proposed: it is a column on mcp_server_units, per (server, team), editable per-team. Registration only declares the upstream auth fact.

  2. Deny-floor override semantics → un-softenable. ✅ Preserved. Migration 142's comment is explicit: within a severity, agent ▸ team ▸ org, but (action = 'deny') DESC stays the first sort key — an agent-scoped escalate can never soften an org/team deny. A child scope may only tighten. This upholds the T5 cascade-override invariant (migration 132).

  3. Agent-policy scoping mechanism → agent_id column (option a). ✅ Founder ruling 2026-07-10. agent_id is TEXT with no FK (deliberately — mirrors member_api_keys.scoped_agent_id; the runtime agent id is an opaque string and the referent is ambiguous under the #969 schema-lie). Tenant isolation is preserved by org_id + FORCE RLS; the write RPC additionally validates AgentInOrg.

Proposed decision 4 (per-agent MCP selection at creation) — also shipped: AgentService.SetAgentTools repurposes agent_configurations.tools as a narrow-only allowlist over the team's approved set (empty = inherit-all; the wizard can only deselect). Effective set = team set ∩ allowlist ∩ agent guardrails. Grants stay team-level (#1744, ADR-0022 W3/B8). 🟢 LIVE — enforced at the edge as EdgeAgentAllowlist (gate 3.4 in the sibling doc).


What's still WIP / not settled here

  • Prod enablement is parkedMCP_GATEWAY_ENABLED defaults false and there is no prod platform signing key (#1864). Everything above is exercisable on beta-dev, dark for prod. See the sibling doc §8. 🟠
  • #530 register-form UI rework — the backend split is done; the client SPA register form change is the remaining slice. 🟡
  • visibility column honored by the MCP resolver — flagged in the ADR housekeeping; confirm before relying on mcp_server_units.visibility for entitlement decisions. (Unverified in this pass — check the resolver.)

Source map (this angle only)

  • Binding + credential_mode / require_per_agent: internal/mcpserver/binding.go, internal/mcpserver/service.go
  • Three-grain credential write + IDOR/clearance: internal/mcpserver/credential/scope.go
  • Most-specific-wins resolution + fail-closed: internal/mcpserver/credential/resolve.go
  • Agent-scoped tool policy: internal/context/store/migrations/142_agent_scoped_tool_policies.up.sql, internal/guardrails/store.go
  • Per-agent narrow-only allowlist: internal/agent/service.go (SetAgentTools)

Full system, auth hops, egress, onboarding: Governed MCP & the Team Gateway. Related: ADR-0021/0022 (gateway + trust root), ADR-0024 (governance realignment), PRD #1681. Trust-root blocker: #1864.


Reflects upsquad-core@9858af5f (2026-07-24). If ADR-0022 items change or #1864 resolves, re-verify the delta map against internal/mcpserver/credential/.