Skip to content

fix(mcp): pair never replaces existing credentials; start unconfigured on keychain files - #730

Merged
p-diogo merged 2 commits into
mainfrom
fix/mcp-creds-guard-degrade
Nov 3, 2026
Merged

p-diogo merged 2 commits into
mainfrom
fix/mcp-creds-guard-degrade

Conversation

@p-diogo

@p-diogo p-diogo commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Risk tier L3 (credentials rail). Needs Pedro's sign-off before merge. The MCP-Hermes parity plan (internal PR 669, items 0.2/0.3) rates pairing and credential handling L3. This PR only adds refusals and a softer startup failure. Key derivation, pairing crypto and bundle parsing are untouched.

What

1. totalreclaw_pair never replaces an existing credentials.json

writePairedCredentials overwrote ~/.totalreclaw/credentials.json without checking. Every TotalReclaw client on the machine shares that file. A Hermes desktop install keeps the phrase in the OS keychain and writes only a marker ("mnemonic": "__keychain__:v1:0x…", keychain_wrapped: true). The MCP server can't read that marker, so it reports "not configured". A generate-mode pairing then silently moved Hermes, and everything else on the machine, to a new account.

  • Refuse. If the server is configured, or the file exists at all, the tool returns already_configured with an explanation and opens no relay session. This applies to both generate and import. A generate pairing always makes a different account, and an import over a file this server can't read can't be checked to be the same account. If the server is configured because of the file, the server is already working, so a same-account import has nothing to do.
  • No race. The new file is created with an exclusive open (wx, 0600). If credentials appear while the user is on the pairing page, nothing is overwritten: the relay gets nack already_configured, and the page shows its existing "Gateway could not finish pairing (already_configured)" error.
  • No replace path. The refusal carries no token and no replace argument is honoured. The agent is told not to move, rename or delete the file.

2. A file the server can't read no longer stops it from starting

A version: 2 file with keychain_wrapped: true (Hermes' bundle keychain form) or an unknown version made main() throw. The host then showed no TotalReclaw tools, and the reason sat in a log file the user never sees.

  • resolveManagedCredential now throws UnsupportedCredentialsError (reason: keychain_wrapped | unknown_version) for those cases. It also recognises the v1 keychain marker, which used to read as "nothing configured" with no explanation.
  • index.ts catches only that class. The server starts unconfigured, and every tool call returns not_configured with reason and a plain message telling the agent not to call totalreclaw_pair. Nothing in the file is used, so this is not a downgrade.
  • Still fatal: a version: 2 bundle that fails parseBundleV1 (§4.7 validation). That points to a corrupt or tampered file, and the parity plan says it stays fatal. The existing credentials.test.ts throw paths are unchanged.
  • The managed-mode startup log line for a plain unconfigured server no longer says to put the phrase in the host config. It now says to ask the assistant to run totalreclaw_pair.

Relation to #716

#716 (draft, L3) ports the full Hermes design: replace=true opens a relay #72 replace-mode session, the user confirms on the page, and the old file is kept. This PR is a strict subset of #716's guard. It reuses #716's names (CredentialsExistError, credentialsPath / isConfigured / openSession options, the already_configured payload, exclusive create) so that #716 can rebase on top and add replace mode. That rebase means taking #716's writeCredentialsFile(…, replace) and adding the replace branch to the guard.

The startup degrade (part 2) is not in #716. The pair-wait/copy fix is separate in #729 (L2), so the rail doesn't hold it up.

Tests

  • New mcp/tests/pair-credentials-guard.test.ts (8 tests):
    • an existing Hermes-marker file is refused in generate and import, no session opened, bytes unchanged
    • a configured server is refused with no file on disk
    • the unusable-file reason is shown in the refusal
    • the refusal holds no token, even when replace / replace_confirmed args are sent
    • first-time pairing still opens a session
    • a file that appears mid-pairing is not overwritten and the relay gets nack already_configured
    • a first pairing writes the file at mode 0600 and acks
  • Mutation check: dropping the existence check and the wx flag fails 5 of the 8.
  • New mcp/tests/credentials-unsupported.test.ts (10 tests): v2 keychain, v1 marker with and without the flag, unknown versions, no file content echoed, an invalid v2 bundle stays a plain fatal Error, the dispatch envelope with and without a reason.
  • All fixtures live in per-test temp dirs and use synthetic data (the public BIP-39 test vector, a made-up marker). Nothing reads the real home directory.
  • cd mcp && npm install --legacy-peer-deps && npm run build && npm test (as CI does; mcp has no lockfile): 47 suites, 821 passed, 5 skipped.
  • node scripts/check-url-binding.cjs: OK. scripts/check-phrase-safety.sh and scripts/check-phrase-safety-dist.sh: clean.

Not verified: the co-located Hermes scenario on a real host (Hermes keychain install, then MCP in Claude Desktop). That is the QA step the parity plan lists for 0.2.

Not changed here, worth a follow-up

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bd3G1BkVMFSXkcdAQ7nFax

…d on keychain files

totalreclaw_pair wrote ~/.totalreclaw/credentials.json without checking.
The file is shared with Hermes, whose desktop keychain marker reads as
"not configured" here, so a generate-mode pairing silently moved every
client on the machine to a new account. The tool now refuses with
already_configured when the server is configured or the file exists
(both modes, no session opened), and creates the file with an exclusive
open so a file that appears mid-pairing is not overwritten (relay gets
nack already_configured). No replace path; that is #716.

A keychain-wrapped file (v2 or the v1 marker) or an unknown version made
the server exit at startup, so the host showed no tools. Those now raise
UnsupportedCredentialsError; the server starts unconfigured and every
tool call returns not_configured with a reason. A v2 bundle that fails
validation stays fatal.

No change to key derivation, pairing crypto or bundle parsing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd3G1BkVMFSXkcdAQ7nFax
@p-diogo
p-diogo marked this pull request as ready for review November 3, 2026 21:41
@p-diogo

p-diogo commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

Approved by Pedro 2026-11-03 (L3 sign-off). Merged main (CHANGELOG conflict with #729 only); MCP suite 48/48 suites, 832 passed locally.

@p-diogo
p-diogo merged commit 181e692 into main Nov 3, 2026
12 checks passed
@p-diogo
p-diogo deleted the fix/mcp-creds-guard-degrade branch November 3, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant