Skip to content

refactor(mcp): credential-migration tools are listed only for the keys the backend admits (#3435) - #3484

Draft
vybe wants to merge 11 commits into
devfrom
feature/3435-credential-mcp-tools
Draft

vybe wants to merge 11 commits into
devfrom
feature/3435-credential-mcp-tools

Conversation

@vybe

@vybe vybe commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #3435. One of five independent stale-code PRs off dev. Draft until the live exercise below is recorded.

What

The issue was filed as "two credential tools fail on every call". The diagnosis found no defect: the requests are correct, and agent-scoped keys are refused by design (import and export are owner-or-admin and human-only; the encryption-key route is admin-only). This PR makes the catalog honest about that.

  • Visibility — export_credentials, import_credentials and get_credential_encryption_key (src/mcp-server/src/tools/agents.ts) each gain a per-tool canAccess allow-list of user and system sessions. An agent session no longer lists them, and calling one by name answers "not found". The previous group default listed them for user, agent and system, so this can only remove a tool from a session.
  • Wording — each description now says who may call the tool (a user-scoped or system-scoped key; admin for the key tool). Lengths 613 / 660 / 564 characters, under the 2,048 cap.
  • Refusals — a backend 403 is returned as a typed denial (human_only / admin_only / not_authorized) and recorded as denied in the audit log. A 403 without a JSON detail returns a fixed sentence; the response body is never copied. 400, 404 and 503 are thrown exactly as before.
  • Guards — a visibility test over the real transport, a shrink-only baseline of the nine other admin-only tools still listed for agents (a new one cannot be added undecided), and tests/unit/test_3435_credential_tool_gates.py, which pins the three backend routes' existing gates by explicit principal.
  • Docs — the user-docs MCP list, the FAQ migration entry (including setting the key on the target instance), the credential-injection feature flow and the MCP-server architecture row.

No backend change: git diff origin/dev...HEAD -- src/backend docker is empty. Nobody gains access to anything.

Rulings carried (operator, recorded on the issue 2026-10-09)

  • Keep all three tools; fix visibility and wording rather than remove the key tool.
  • Allow-list is user + system, so what is advertised equals what the backend admits.
  • Only 403 is mapped to the typed envelope.
  • No backend access narrowing here, and the nine sibling admin-only tools are not converted here — both are follow-ups.

Review + security

/review (reading review, report-only): MERGEABLE, 0 critical. Hard limits verified: the access.ts rows keep their policy kinds (the owner strings are labels and enforce nothing), the allow-list is a filter on the registered set, a future scope defaults to hidden, and the system key is admitted by all three backend gates. /cso --diff: no findings; the attack surface shrinks. Fixed after review: the non-JSON 403 fallback no longer echoes the response body (test red first), "user-scoped or system-scoped" wording, and a list-formatting fix in the user doc.

Tests

  • MCP server: npm test 836 / 836; the two changed test files 36 / 36.
  • Backend pins, shuffled (seed 12345): test_3435_credential_tool_gates.py 25 passed, plus the auth-wiring, admin-gate and human-only route suites — 150 passed in six files.
  • Mutation checks: dropping canAccess from one tool, hiding a baselined tool without shrinking the baseline, advertising an undecided admin-only tool, removing the audit stamp, and removing the agent-principal rejection from the import handler each turn a test red.
  • git merge-tree against dev (21b4a11): clean.
  • Not run here: the full unit island (CI) and anything needing a live stack.

Before merge

Handoffs

🤖 Generated with Claude Code

Trinity Agent (trinity) and others added 8 commits October 9, 2026 13:30
…ackend admits (#3435)

export_credentials, import_credentials and get_credential_encryption_key
carry a per-tool canAccess allow-list {user, system} (fails closed, #848),
descriptions that name who may call each, and a backend 403 returned as a
typed {success:false, human_only|admin_only|not_authorized} envelope through
accessDenied so the audit row reads denied (#2807). 400/404/503 stay thrown.
No backend change. Mutations verified: deny-check allow-list and an
unstamped envelope each turn agents.credentials.test.ts red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, with a shrink-only admin-only guard (#3435)

Boots the real server in key mode: a user key lists the three credential
tools, an agent key neither lists nor can call them (not found). Every
other ADMIN_ONLY row is either hidden from agent sessions or in a
shrink-only baseline of nine (follow-up per ruling TD-3). Dropping one
tool's canAccess turns it red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s by principal (#3435)

A pin of current behaviour, no backend change: import/export admit an
owner or admin human and refuse every agent key with the exact human-only
403 the MCP mapper keys on (uniform 404 for a non-owner); the key route
admits an admin human only (403 non-admin, agent, ops scope; 503 when the
key is unset). Explicit models.User principals. Mutation: removing
reject_agent_principal from the import handler turns 2 cases red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
User-docs MCP list and FAQ migration entry name the caller class per tool
(the FAQ gains the target-side step: set CREDENTIAL_ENCRYPTION_KEY in .env
and restart before importing). The feature flow's stale export/import
handler snippets now show the real dependency chain, and its MCP section
and the mcp-server architecture row describe the per-tool allow-list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntence, never the body (I1) (#3435)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…scoped key the allow-list admits (I2) (#3435)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plitting it (I3) (#3435)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ote (I3) (#3435)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ads (#3435)

test_inject_assigned_credentials.py replaces
sys.modules["services.credential_encryption"] at collection time, and the
export/import handlers resolve get_credential_encryption_service through a
function-local import at request time. In a shared xdist worker the stub this
file installed on its import-time alias was never read, the real service ran,
and every 200-path case got a 503 ("Failed to connect to agent").

The fixture now patches the module currently in sys.modules, overrides the
exact get_current_user object the router's Depends() holds, and stubs the db
the ownership dependency closes over. Same principals, real router and
dependencies, same 25 cases; no backend change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vybe

vybe commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

CI note: the first run's regression diff reported 10 new failures, all in the new test_3435_credential_tool_gates.py (the cases that reach the service). Cause: tests/unit/test_inject_assigned_credentials.py replaces services.credential_encryption in sys.modules at collection and does not restore it; the export and import routes import that module per request, so the new test's stub sat on a stale copy and the real service returned 503. Fixed in c547fb4e (test file only): the fixture patches the module objects the running route reads. The reproducing pairing passes on 20 shuffle seeds and every check on c547fb4e is green. Handoff: the sibling file should restore the module in its teardown — not changed here.

trinity-ability and others added 2 commits October 10, 2026 09:45
…l-mcp-tools

# Conflicts:
#	tests/registry.json
…ever by replacing the target's key (#3435)

The migration entry told operators to set the source instance's key as
CREDENTIAL_ENCRYPTION_KEY on the target. On a target that already holds
encrypted secrets that strands every one of them. The decrypt path already
falls back to CREDENTIAL_ENCRYPTION_KEY_SECONDARY, so that is the safe slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

@github-actions

Copy link
Copy Markdown

⚠️ Live-instance suite skipped — merge conflict against dev.

Resolve by merging dev locally and pushing the result; the next nightly re-tests.

This branch has not been deployed

No deployments
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.

2 participants