Skip to content

Audit records cannot attribute a call to a caller under --transport http #412

Description

@plusky

Depends on #411 — this is a property of the record that issue introduces, not a separate mechanism.

Under --transport http one process serves many clients, and nothing in it can say which client made a given call. An audit record can therefore state what happened but not on whose behalf — worth stating as its own limitation rather than discovering it while implementing the record.

What the http transport actually separates

The SessionRegistry mints one isolated McpSession per Mcp-Session-Id (crates/mtui-mcp/src/provider.rs:14-15, :165-170). That is isolation: two clients do not share loaded templates, host connections, or the job table. It is not identity: the session id is transport bookkeeping, assigned at initialize, not an authenticated claim about who is calling.

The docs already say so plainly, docs/src/mcp.md:55-61:

Profiles are not authentication or authorization. They reduce the advertised tool surface; they do not gate callers. HTTP session isolation is likewise not caller authentication. Keep the HTTP transport on its default loopback interface, or place it behind an authenticated boundary you trust to operate the remaining maintenance tools.

This issue does not dispute that posture; it records the consequence for the audit trail.

Why "who" is currently unanswerable from inside the server

Every client of an http mtui-mcp acts with the server's credentials, not its own: the SSH keys the process can reach, gitea_token, slack_token, and whatever the resolved Config carries for the build service and Bugzilla. There is no per-call credential to attribute against, so even a perfectly faithful record of the call cannot name an actor. The strongest true statement available is the session id, which is an opaque handle — useful for grouping a session's calls together, useless for answering "who approved this".

Proposal

  1. Reserve a nullable principal field in the record from day one, always null, documented as reserved. Nothing self-declared by the client — a header, a tool argument, a client name from initialize — may ever be promoted into it; a field that is sometimes a real identity and sometimes a claim the caller typed is worse than an absent one.
  2. Document the limitation next to the existing security boundary: with http, records attribute to a session, not a person.
  3. Revisit when the MCP authorization specification lands and rmcp exposes the authenticated identity. Static shared bearer tokens are not a substitute — they authenticate a deployment, not a caller, which does not make a principal meaningful.

What it unblocks

Real per-caller attribution, and the option of per-caller policy later (this operator may run, that one may only list_*), which is not expressible while every caller is indistinguishable.

Suggested acceptance

Nothing to verify behaviourally beyond the field being present, null, and impossible to populate from client-supplied input — which is worth an explicit test, since the natural "improvement" someone makes later is to fill it in from the client's initialize name.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions