Files
mcp-gateway-nexus/docs/modules/14-agent-profiles.md
drjones 8d3ffef920 docs: initial architecture and design for MCP Nexus
MCP Nexus is a self-hosted control plane for MCP servers — "Kubernetes for
MCP." Agents connect to one endpoint; Nexus discovers services, installs the
right MCP servers, aggregates them behind a namespaced router, secures access
with auth/RBAC, and heals/updates them via a reconcile loop.

This first commit is design-phase only (no runnable code yet):
- README.md            project front door + module map
- docs/ARCHITECTURE.md target design: tenets, two-plane split, reconcile
                       loop, domain types, storage, security, deployment
- docs/ROADMAP.md      phased delivery (foundations -> walking skeleton ->
                       discover+install -> secure -> operate -> extend/scale)
- docs/modules/01-20   one design doc per module, all cross-linked

Backend stack decision: Go (single static binary, embedded SQLite + embedded
React dashboard). Repo initialized in /root with a whitelist .gitignore so
only project files are tracked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 04:38:27 +00:00

9.6 KiB

AI Agent Profiles

Module 14 · Plane: Data · Roadmap phase: 3 Part of MCP Nexus architecture.

Purpose

An AgentProfile (Architecture §7) is the agent-facing binding of identity → roles → visible tool set. Where Authentication proves who is connecting and RBAC is the general role→tool engine, an AgentProfile is the first-class object that ties a specific AI agent — Claude, Cursor, VSCode, OpenWebUI, Codex, Aider, Continue.dev — to the roles it holds and therefore the exact tools it may see and call at the Gateway. It is what makes "one endpoint, many agents" (tenet #1) concrete: every connecting agent resolves to a profile, and the profile decides its worldview.

Responsibilities

  • Own the AgentProfile model: {id, identity, allowed_roles} (Architecture §7), plus display metadata (agent kind, description, owner) and an association to one or more credentials.
  • Identify a connecting agent: map the credential presented at the MCP initialize handshake (an API key, token, or client identifier resolved by Authentication) to exactly one AgentProfile.
  • Resolve the profile to a concrete visible tool set by handing its allowed_roles to the RBAC Enforcer — the profile does not compute tool visibility itself; it binds the identity to roles that RBAC expands.
  • Provide the Router with the profile's resolved AllowedSet so tools/list and tools/call are scoped per agent (enforcement stays in RBAC/Router, per tenet #5).
  • Maintain per-profile operational metadata: last-seen, connection count, and (optionally) per-profile rate-limit / quota hints for Security.
  • Support provisioning: create a profile, mint/attach its agent credential (via Authentication), and assign roles — the operator workflow for onboarding a new agent.

Non-goals

  • Authenticating the credential. Verifying the presented secret is Authentication; the profile consumes the resolved Principal.
  • Computing tool visibility. The allow/deny expansion is RBAC; the profile only supplies the roles.
  • Being a human-user object. Human operators are Users in the Identity store; profiles model agents. Both share the RBAC engine, but a profile is the agent binding specifically (this is how it differs from raw RBAC).
  • Per-tool parameter policy. Fine-grained call constraints, if any, belong to RBAC/Security.
  • Managing agent-side config. Nexus does not push settings into Claude/Cursor; it presents an endpoint and scopes it.

Interfaces

// The agent-facing binding of identity -> roles (Architecture §7).
type AgentProfile struct {
    ID           string   // "profile:claude-primary"
    Identity     string   // stable agent identity key, matches Principal.ID for KindAgent
    Kind         string   // "claude" | "cursor" | "vscode" | "openwebui" | "codex" | "aider" | "continue"
    DisplayName  string
    AllowedRoles []string // role names expanded by RBAC
    CredentialIDs []string // API keys / tokens that resolve to this profile
    CreatedAt    time.Time
    LastSeen     time.Time
}

type ProfileStore interface {
    Upsert(ctx context.Context, p AgentProfile) error
    Get(ctx context.Context, id string) (AgentProfile, error)
    // ByCredential maps an authenticated credential -> its profile (identify step).
    ByCredential(ctx context.Context, credentialID string) (AgentProfile, error)
    List(ctx context.Context) ([]AgentProfile, error)
}

// Resolver ties identify -> roles -> visible tool set via RBAC.
type Resolver interface {
    // Identify: authenticated Principal (an agent) -> its profile.
    Identify(ctx context.Context, p Principal) (AgentProfile, error)
    // VisibleTools: profile's roles expanded by the RBAC Enforcer against the live catalog.
    VisibleTools(ctx context.Context, prof AgentProfile) (AllowedSet, error)
}

HTTP/API surface (control-plane API, behind Auth, RBAC-gated; Admin/operator):

  • GET /api/v1/agents · POST /api/v1/agents — list / create profiles.
  • GET /api/v1/agents/{id} · PUT /api/v1/agents/{id} · DELETE /api/v1/agents/{id}.
  • POST /api/v1/agents/{id}/roles — set the profile's allowed_roles.
  • POST /api/v1/agents/{id}/credentials — mint/attach an agent credential (delegates to Authentication).
  • GET /api/v1/agents/{id}/tools — debug: the resolved visible tool set for this profile.

MCP data-plane integration: on initialize, the Router calls Identify(Principal) to pin the profile to the session; tools/list/tools/call are then scoped by the profile's VisibleTools (enforced in RBAC/Router).

Identify → resolve flow (per agent session):

1. Agent (e.g. Cursor) opens an MCP session and sends `initialize`
     carrying its credential (API key / token on Streamable HTTP).
2. Authentication (Module 06) verifies it → Principal{Kind: agent, ID}.
3. Resolver.Identify(Principal) → ProfileStore.ByCredential → AgentProfile
     - no match → fail closed (empty set) unless a guest profile is set.
4. Resolver.VisibleTools(profile): hand profile.AllowedRoles to the RBAC
     Enforcer, which expands them against the live tool catalog (Module 05)
     → AllowedSet (the agent's worldview).
5. Profile + AllowedSet pinned to the MCP session.
6. tools/list → RBAC.Filter(AllowedSet); tools/call → RBAC.Can(...).
7. last_seen updated; provisioning/role changes audited (Module 19).

Example: profile claude-primary (identity agent:claude-01) holds roles [Developer, Home Automation]; RBAC expands these to github.*, postgres.*, filesystem.*, homeassistant.* — so this agent sees exactly those namespaces and nothing from Networking/Finance/Admin.

Data

  • Reads/writes AgentProfile rows in the Identity store (Architecture §8), alongside Users, Roles, and API keys; a credential→profile index supports the identify step.
  • Reads the RBAC role definitions to expand allowed_roles, and the Dynamic Tool Registry (via RBAC) for the live catalog.
  • Writes profile provisioning and role-binding changes to the Audit store.
  • Module-local: last_seen/connection counters updated on the hot path (cheap, async).

Dependencies

  • Credential verification and the Principal come from Authentication.
  • Role expansion and enforcement come from RBAC; profiles supply the roles.
  • Visibility is scoped against the Dynamic Tool Registry.
  • Enforced at the Gateway/Router on the hot path.
  • Managed through the Web Dashboard (agent onboarding).
  • Provisioning + access audited per Security.

Failure modes & handling

Failure Behavior
Credential resolves to no profile Treated as an unrecognized agent → no roles → empty visible set (fail closed); connection allowed only if a default/guest profile is explicitly configured.
One credential maps to two profiles Rejected at provisioning; the credential→profile index is unique. Ambiguity is a configuration error, not a runtime coin-flip.
Profile references a deleted role Missing role contributes nothing (no phantom access); resolution logs the dangling reference.
Roles changed while agent connected RBAC cache invalidated; next tools/list/tools/call reflects the new set without dropping the session.
Profile deleted while agent connected Session's pinned profile is invalidated; subsequent requests fail closed and the agent must reconnect.
Identity store unavailable Fail closed — unresolved profile means no tools; surfaced via Notifications.

Security notes

Honors Architecture §9 and tenets #1/#5. A profile is the point where an opaque agent connection becomes a scoped, named identity — everything downstream (visibility, invocation, audit) keys off it. Default is fail closed: an agent whose credential maps to no profile, or a profile with no roles, sees nothing. The profile never widens access on its own; it can only reference roles, and RBAC enforces them server-side, so a compromised/misconfigured agent cannot self-elevate. Every profile lifecycle event (create, role change, credential attach/detach) is audited with principal + target. Per-profile rate-limit hints feed the Security baseline so one noisy agent cannot starve others (tenet #1's "many agents" fairness).

Open questions

  • Should a profile map to exactly one credential or many (e.g. Claude Desktop + Claude Code sharing one profile vs. distinct profiles)?
  • Do we ship a curated starter profile pack (Claude, Cursor, VSCode, …) with sensible default roles, or start every profile empty?
  • Is there value in profile-level tool overrides on top of roles, or does that erode the clean identity→roles→tools chain?
  • How do profiles interact with per-agent quotas/budgets (token usage) tracked in Metrics?
  • Guest/anonymous profile: supported at all, or is every agent required to be provisioned first?

Milestone

Delivered in Phase 3 (Secure). Thin slice first: the AgentProfile model, credential→profile Identify, and role expansion via RBAC so a connecting agent resolves to a scoped visible tool set at the Router. Directly powers the Phase 3 exit proof: two agents (distinct profiles, distinct roles) connect and each sees a different tool set — the profile is what binds each agent to its slice.