Skip to content

Close the MCP tool surface and remove the plaintext passphrase cache - #97

Merged
vvillait88 merged 3 commits into
mainfrom
fix/as-006-mcp-tool-surface
Aug 28, 2026
Merged

Close the MCP tool surface and remove the plaintext passphrase cache#97
vvillait88 merged 3 commits into
mainfrom
fix/as-006-mcp-tool-surface

Conversation

@vvillait88

@vvillait88 vvillait88 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

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: --danger and 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, send and unlock are 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: 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 (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/.unlock held 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. The unlock command 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

  • Bug fix (no breaking change)
  • New feature (no breaking change)
  • Breaking change (existing callers must update)
  • Docs, tests, or internal maintenance only

Breaking on two surfaces, both deliberate. The unlock command is removed, and six tools no longer appear over MCP.

Public API

CLI: unlock is gone. Anyone who used it for interactive convenience now either exports AGENTSCORE_PAY_PASSPHRASE or 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, send and unlock now return Unknown tool. That is the intent of the finding.

send gains a spend-limit check, a no-op unless limits.json sets one.

Test plan

bun run lint, bun run typecheck, bun run knip clean. 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 balance and agentscore-pay pay --chain tempo --max-spend N with AGENTSCORE_PAY_PASSPHRASE exported into its sandbox. Run non-interactively with only that variable set, init creates all three wallets and balance returns them, and no cache file is written. It never starts an MCP server, and prompts.ts checks 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: false would pass whether or not the flag reached the tool list, and walking the Cli object 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_details returns Unknown tool for all five named tools, and search_tools for "export" and "unlock" returns empty where it previously returned the wallet tools.

Three guards were drilled: un-hiding wallet export and send fails 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-format as a convenient bare invalid_input and now uses assess with no identity, which raises the same shape. tests/unlock-cmd.test.ts is deleted with the command it covered.

Checklist

  • Tests cover the new behavior, and the suite passes locally
  • Lint, format, and type checks pass
  • Docs and README examples updated if the public surface changed
  • No secrets, credentials, or personal data in the diff or the tests

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.
@vvillait88 vvillait88 changed the title Keep secret-disclosure and fund-moving commands off the MCP tool surface Close the MCP tool surface and remove the plaintext passphrase cache Aug 28, 2026
@vvillait88
vvillait88 merged commit 4a36017 into main Aug 28, 2026
6 checks passed
@vvillait88
vvillait88 deleted the fix/as-006-mcp-tool-surface branch August 28, 2026 01:25
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
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.

1 participant