Close the MCP tool surface and remove the plaintext passphrase cache - #97
Merged
Conversation
The MCP surface is generated from the CLI's command definitions, so every command reaches agents by default. Served as tools, the CLI's safety gates stop meaning anything: --danger and the typed EXPORT confirm become boolean parameters the model sets for itself, the interactive passphrase prompt is answered by AGENTSCORE_PAY_PASSPHRASE, and whatever they guard is returned into the transcript and the model provider's logs. One prompt injection reaches key export or an irreversible transfer. wallet create, wallet show-mnemonic, wallet export, wallet remove, send and unlock are now hidden from MCP clients. All six remain on the CLI, where a human is present and those gates do their job; mcp: false only affects what is served as a tool. Deliberately still exposed after checking what they actually return: init creates wallets but its result carries only chain lists and flags, never the mnemonic, and wallet import takes a secret in rather than handing one out. Spend limits now apply to `send`. They were enforced on the 402 paths only, so a configured ceiling could be walked straight past by using a raw transfer instead. Only the USDC path is checked: a native transfer moves gas tokens this command does not price in dollars, and inventing a conversion would be a worse guess than not claiming one. It is a no-op when nothing is configured, so this changes behavior only for someone who asked for it. The surface test drives a real MCP handshake rather than reading source or CLI internals. Grepping for mcp: false would pass whether or not the flag reached the tool list, and the Cli object is a closure with no command map to walk, so asking the server is the only check that cannot pass vacuously. It pins the exclusions and, alongside them, that pay and balance stay exposed, since those are what the agent fleet uses to pay.
`~/.agentscore/.unlock` held the wallet passphrase in cleartext for up to 8
hours, beside the scrypt+AES keystores it unlocks. File modes (0600 in a 0700
dir) were the only protection, and they stop nothing already running as that
user: malware, a backup, a CI cache, or an LLM agent with filesystem tools. The
`unlock` command that wrote it is gone along with the read path.
An installation upgrading from a version that wrote the file gets it DELETED
rather than ignored. Ignoring it would be the worst of the three options, since
the cleartext secret would still be on disk with nothing left that admits to
using it. The purge runs where the passphrase would have been read, so it
happens on the next command that needs one, and `wallet remove` keeps its
existing cache wipe.
Unattended use is AGENTSCORE_PAY_PASSPHRASE in the environment. That was already
the recommendation on the `unlock` command's own hint text ("it leaves no
on-disk artifact"), and it is what the agent fleet already uses, so the fleet is
unaffected: prompts.ts checks the environment before it ever touched the cache.
Verified by running the fleet's exact sequence non-interactively with only the
env var set, which creates wallets and reads balances and writes no cache file.
The no-TTY error now explains the removal instead of suggesting `unlock`, since
that suggestion would send someone to a command that no longer exists.
Two test files changed for reasons worth noting rather than hiding. The
error-envelope suite used `unlock --for bad-format` as a convenient source of a
bare invalid_input and now uses `assess` with no identity, which raises the same
shape. The cache suite now pins the two properties that matter after removal:
nothing can read or write a passphrase to disk, and a leftover file is deleted.
8 tasks
vvillait88
added a commit
that referenced
this pull request
Aug 28, 2026
…moved (#98) ## Summary Version bump for the AS-006 and AS-011 work merged in #97. ## Type of change - [ ] Bug fix (no breaking change) - [ ] New feature (no breaking change) - [x] Breaking change (existing callers must update) - [ ] Docs, tests, or internal maintenance only Minor rather than patch because 0.x signals breaking with the minor. The `unlock` command is removed and six commands no longer appear over MCP. ## Public API **CLI:** `unlock` is gone. Unattended use is `AGENTSCORE_PAY_PASSPHRASE` in the environment, which that command's own hint text already recommended; interactive users type the passphrase per command. Every other command is unchanged. **MCP:** `wallet_export`, `wallet_show-mnemonic`, `wallet_create`, `wallet_remove`, `send` and `unlock` now return `Unknown tool`. All six still run from a shell. **`send`** gains a spend-limit check, a no-op unless `limits.json` sets one. A leftover `~/.agentscore/.unlock` is deleted on the next command that needs a passphrase, rather than ignored. ## Test plan On the bumped tree: `bun run lint`, `bun run typecheck` and `bun run test` clean, 57 files and 574 tests passing. The behavior is verified in #97, including a live MCP handshake confirming the six tools return `Unknown tool` while `pay` and `balance` stay exposed, and drills that reintroduce each guard's removal and confirm the tests fail. ## Checklist - [x] Tests cover the new behavior, and the suite passes locally - [x] Lint, format, and type checks pass - [x] Docs and README examples updated if the public surface changed - [x] No secrets, credentials, or personal data in the diff or the tests
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes AS-006 and the passphrase half of AS-011 from the Q3 2026 penetration test. They are one chain: the dangerous MCP tools all need the keystore passphrase, and the plaintext cache is what supplies it unattended.
AS-006, the MCP tool surface. It is generated from the CLI's command definitions, so every command reaches agents by default. Served as tools, the CLI's safety gates stop meaning anything:
--dangerand the typed EXPORT confirm become boolean parameters the model sets for itself, and whatever they guard is returned into the transcript and the model provider's logs.wallet create,wallet show-mnemonic,wallet export,wallet remove,sendandunlockare now hidden from MCP clients while remaining on the CLI, where a human is present.Two commands were checked and deliberately left exposed, on what they actually return rather than their names:
initcreates wallets but its result carries only chain lists and flags, never the mnemonic, andwallet importtakes a secret in rather than handing one out.Spend limits now apply to
send(the reporter's step 7). They were enforced on the 402 paths only, so a configured ceiling could be walked past with a raw transfer. USDC only: a native transfer moves gas tokens this command does not price in dollars, and inventing a conversion would be a worse guess than not claiming one.AS-011, the plaintext passphrase cache.
~/.agentscore/.unlockheld the passphrase in cleartext for up to 8 hours beside the keystores it unlocks, with file modes as the only protection, which stop nothing already running as that user. Theunlockcommand and the read path are both gone.An installation upgrading from a version that wrote the file gets it deleted rather than ignored. Ignoring it is the worst of the three options: the secret stays on disk with nothing left that admits to using it.
Type of change
Breaking on two surfaces, both deliberate. The
unlockcommand is removed, and six tools no longer appear over MCP.Public API
CLI:
unlockis gone. Anyone who used it for interactive convenience now either exportsAGENTSCORE_PAY_PASSPHRASEor types the passphrase per command. The no-TTY error explains this rather than suggesting the removed command. All other commands are unchanged, including the six hidden from MCP.MCP:
wallet_export,wallet_show-mnemonic,wallet_create,wallet_remove,sendandunlocknow returnUnknown tool. That is the intent of the finding.sendgains a spend-limit check, a no-op unlesslimits.jsonsets one.Test plan
bun run lint,bun run typecheck,bun run knipclean.bun run test: 57 files, 574 tests passing.The agent fleet is unaffected, verified by running its exact sequence rather than reasoning about it. Our GTM agent shells
agentscore-pay balanceandagentscore-pay pay --chain tempo --max-spend NwithAGENTSCORE_PAY_PASSPHRASEexported into its sandbox. Run non-interactively with only that variable set,initcreates all three wallets andbalancereturns them, and no cache file is written. It never starts an MCP server, andprompts.tschecks the environment before it ever touched the cache, so neither change reaches it.The surface test drives a real MCP handshake. Two earlier versions were unsound and are worth recording: grepping for
mcp: falsewould pass whether or not the flag reached the tool list, and walking theCliobject does not work because incur builds it as a closure with no command map. It carries a positive control, so an empty handshake cannot read as success. Verified live against the reporter's steps:get_tool_detailsreturnsUnknown toolfor all five named tools, andsearch_toolsfor "export" and "unlock" returns empty where it previously returned the wallet tools.Three guards were drilled: un-hiding
wallet exportandsendfails the surface test naming each tool; removing the enforce block fails the limit test; and restoring the cache read leaves the leftover file on disk, which the removal test catches.Two test files changed for reasons worth stating. The error-envelope suite used
unlock --for bad-formatas a convenient bareinvalid_inputand now usesassesswith no identity, which raises the same shape.tests/unlock-cmd.test.tsis deleted with the command it covered.Checklist