Skip to content

POST /api/credentials/update has no callers but is named as the sanctioned MCP-config path #2008

Description

@AndriiPasternak31

Summary

POST /api/credentials/update (docker/base-image/agent_server/routers/credentials.py:74) has no callers anywhere — not the backend, not the MCP server, not startup.sh, not the frontend, not the enterprise submodule. Its own docstring says otherwise:

"This endpoint is called by the Trinity backend when credentials are updated."

Two other places treat it as live:

So the documented escape hatch for configuring MCP servers does not exist, and it is the only implementation of the .mcp.json.template contract the agent guide promises (#2007).

Verification

The backend's agent-facing credential calls are /api/credentials/status (routers/credentials.py:72), /inject (:345), /read + /list (credential_encryption.py:308,329,346), /reload-token (subscription_auto_switch.py:361). startup.sh:541 calls only /api/internal/decrypt-and-inject. Every agent URL in the backend is a literal f-string with a fixed path; no dynamic construction reaches it. Searched src/, docker/, scripts/, config/, the .claude submodule and the populated src/backend/enterprise submodule.

Why it matters if anyone re-wires it

The renderer is a whole-text str.replace over .mcp.json.template, so unlike the backend engine it does substitute into command, and nothing on that path runs validate_mcp_config. Simulated verbatim from its source:

plain ${VAR} in command -> {"command": "/bin/sh -c whatever", "args": []}

That is the RCE-by-config class #590 closed on the inject path — a credential value becoming the executed command. It is inert only because nothing calls the endpoint, and the security comment above actively invites someone to call it.

Note the endpoint is not unauthenticated: #1159 puts the agent server behind X-Trinity-Agent-Token, so only the backend (or the agent itself, which holds its own token) can reach it. That bounds the exposure; it does not make the substitution safe.

The decision

Pick one, don't leave it:

  • Delete the endpoint and correct files.py:181-186 + architecture.md:331 to name the path that actually exists (validated POST /api/agents/{name}/credentials/inject); or
  • Wire it deliberately as the implementation of the .mcp.json.template contract — in which case it must run validate_mcp_config on its output and must not substitute into command.

The second option overlaps with the sibling "github: templates never render .mcp.json.template" issue; if that one is fixed by rendering at creation in the backend, this endpoint should be deleted rather than revived.

Acceptance criteria

  • The endpoint is either removed, or reachable and validated
  • files.py:181-186 describes a path that exists
  • docs/memory/architecture.md:331 matches reality
  • If retained: output passes validate_mcp_config, and ${...} in command is refused rather than substituted
  • A test that fails if a ${VAR} can reach command through this path

Related

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions