Skip to content

feat(wallet-cli): Add wallet CLI with daemon support#8446

Open
rekmarks wants to merge 21 commits intofeat/wallet-libraryfrom
rekm/wallet-cli
Open

feat(wallet-cli): Add wallet CLI with daemon support#8446
rekmarks wants to merge 21 commits intofeat/wallet-libraryfrom
rekm/wallet-cli

Conversation

@rekmarks
Copy link
Copy Markdown
Member

@rekmarks rekmarks commented Apr 14, 2026

Introduces an oclif CLI for @metamask/wallet. It is more or less a port of the Ocap Kernel equivalent.

Usage

First, cd packages/wallet-cli.

Then, to start the daemon:

yarn mm daemon start \
  --infura-project-id <YOUR_PROJECT_ID> \
  --password testpass \
  --srp 'test test test test test test test test test test test ball'

Try calling an available controller action:

yarn mm daemon call AccountsController:listAccounts

Actions params must be specified as a stringified JSON array:

yarn mm daemon call \
    KeyringController:signPersonalMessage \
    '[{"data": "0x48656c6c6f2c20776f726c6421", "from": "0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf"}]'

When done, stop the daemon and clean up:

yarn mm daemon purge --force

Summary

  • Create wallet-cli package scaffolded with oclif (mm binary)
  • Implement daemon infrastructure for running a @metamask/wallet instance as a detached background process, communicating over JSON-RPC via Unix domain sockets
  • Add daemon commands: start, stop, status, purge, call
  • call command dispatches arbitrary messenger actions to the running daemon (e.g. mm daemon call AccountsController:listAccounts)
  • Unit tests for all daemon modules (108 tests, 100% coverage on daemon code)

Note

High Risk
Introduces a new local daemon that can dispatch arbitrary wallet messenger actions over a socket and requires handling secrets (SRP/password) and process lifecycle correctly, which is security- and reliability-sensitive despite being new code.

Overview
Introduces a new @metamask/wallet-cli package (Oclif mm binary) that can start @metamask/wallet as a detached background daemon and communicate with it over newline-delimited JSON-RPC on a Unix domain socket.

Adds daemon management and interaction commands (start, stop, status, purge, call), including a call pathway that forwards arbitrary messenger actions to the running wallet instance, plus supporting socket server/client/spawn/cleanup utilities with extensive unit tests.

Monorepo wiring is updated to include the new workspace in README.md, CODEOWNERS/teams.json, root tsconfig references, ESLint overrides, and Yarn workspace validation; @metamask/wallet now exports importSecretRecoveryPhrase for CLI consumption.

Reviewed by Cursor Bugbot for commit 8e1bd9d. Bugbot is set up for automated code reviews on this repo. Configure here.

@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 14, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​metamask/​auto-changelog@​3.4.49910010093100
Updated@​inquirer/​confirm@​6.0.7 ⏵ 6.0.11100 +11009894 -2100
Added@​oclif/​core@​4.10.59810010096100

View full report

@socket-security

This comment was marked as resolved.

Comment thread packages/wallet-cli/src/daemon/stop-daemon.ts Outdated
Comment thread packages/wallet-cli/src/daemon/utils.ts
Comment thread packages/wallet-cli/src/daemon/utils.ts
Comment thread packages/wallet-cli/src/daemon/rpc-socket-server.ts
Comment thread packages/wallet-cli/src/daemon/rpc-socket-server.ts
Comment thread packages/wallet-cli/src/index.ts Outdated
Comment thread packages/wallet-cli/src/commands/daemon/start.ts
Comment thread packages/wallet-cli/src/daemon/daemon-spawn.ts
Comment thread packages/wallet-cli/src/daemon/daemon-entry.ts
@rekmarks rekmarks requested a review from a team as a code owner April 14, 2026 05:34
Comment thread packages/wallet-cli/src/daemon/daemon-spawn.ts
Comment thread packages/wallet-cli/src/daemon/daemon-entry.ts Outdated
Comment thread packages/wallet-cli/src/daemon/rpc-socket-server.ts
Comment thread packages/wallet-cli/typedoc.json Outdated
Comment thread packages/wallet-cli/src/daemon/stop-daemon.ts
Comment thread packages/wallet-cli/src/daemon/daemon-entry.ts
Base automatically changed from rekm/wallet-library-tweaks to feat/wallet-library April 14, 2026 10:50
Comment thread packages/wallet-cli/src/daemon/rpc-socket-server.ts Outdated
@rekmarks
Copy link
Copy Markdown
Member Author

@SocketSecurity ignore npm/jake@10.9.4

Transitive via @oclif/core. Usage not suspicious.

Comment thread packages/wallet-cli/src/daemon/daemon-entry.ts
@rekmarks rekmarks force-pushed the feat/wallet-library branch 2 times, most recently from 623f9af to 3fb60ee Compare April 16, 2026 21:13
rekmarks and others added 10 commits April 17, 2026 11:42
…rocess

Adds a daemon that runs a Wallet instance as a detached background
process, communicating over JSON-RPC via Unix domain sockets. This
mirrors the architecture of kernel-cli's daemon.

Infrastructure (src/daemon/):
- socket-line: newline-delimited socket I/O
- rpc-socket-server: generic Unix socket JSON-RPC server
- daemon-client: one-shot JSON-RPC client with retry
- daemon-entry: standalone entry point for the spawned process
- daemon-spawn: spawns daemon-entry as detached child
- stop-daemon: shared stop logic with escalation
- wallet-factory: creates configured Wallet from config
- utils, paths, types: process utilities and path resolution

Commands (src/commands/daemon/):
- start: start daemon (--infura-project-id or INFURA_PROJECT_ID env)
- stop: stop daemon (RPC shutdown -> SIGTERM -> SIGKILL)
- status: check daemon status
- purge: stop daemon and delete all state files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9 test files covering all daemon infrastructure: paths, socket-line,
utils, rpc-socket-server, daemon-client, stop-daemon, wallet-factory,
daemon-entry, and daemon-spawn. 96 tests total.

Config changes:
- jest.config.js: exclude commands/ from coverage (not yet tested)
- eslint.config.mjs: disable n/no-process-env and n/no-sync for
  wallet-cli test files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire sendSignal into stopDaemon so EPERM errors from process.kill are
not silently treated as successful stops. Remove unused withTimeout
utility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Thread wallet password and secret recovery phrase through the daemon
startup chain so the wallet is initialized with an imported SRP.

- Export importSecretRecoveryPhrase from @metamask/wallet
- Add --password and --srp required flags to `daemon start`
- Pass MM_WALLET_PASSWORD / MM_WALLET_SRP env vars to spawned daemon
- Make createWallet async; call importSecretRecoveryPhrase after init

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a `daemon call` CLI command that forwards arbitrary messenger
action calls to the running wallet daemon over JSON-RPC. The daemon
registers a `call` RPC handler that invokes `wallet.messenger.call()`
with the provided action name and arguments.

Usage:
  wallet-cli daemon call AccountsController:listAccounts
  wallet-cli daemon call NetworkController:getState --timeout 10000

Also fixes lint errors in wallet-factory.ts (missing return types).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove placeholder index.ts and library exports (CLI-only package)
- Add yarn constraints exception for wallet-cli exports
- Document intentional `as any` messenger dispatch in daemon-entry
- Remove duplicate socketPath param from ensureDaemon
- Remove unused logPath from DaemonSpawnConfig
- Add 30s server-side socket read timeout in rpc-socket-server
- Handle sendCommand throwing in status command
- Have purge remove entire data directory
- Tighten isRpcError to require both code and message
- Fix waitFor to return false on timeout instead of re-checking
- Clarify multi-request rejection test assertions
- Document password/srp CLI flags as testing-only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rekmarks and others added 11 commits April 17, 2026 11:43
Destructuring null params (when a request omits the params field) would
throw a confusing TypeError. Add a runtime check that params is a
non-empty array before destructuring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… typedoc and JSDoc

- Await rm calls in shutdown finally block via Promise.all and in error
  cleanup path so callers know cleanup is complete
- Use rpcErrors.parse() (-32700) for JSON.parse failures per JSON-RPC spec
- Update typedoc.json entry points after index.ts removal
- Move CONNECTION_TIMEOUT_MS above JSDoc so doc attaches to the function

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reject params where the first element is not a string, preventing
confusing downstream errors from messenger.call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Log errors in catch blocks instead of silently swallowing them across
  daemon-entry, stop-daemon, and rpc-socket-server
- Make shutdown cleanup independent: handle.close(), wallet.destroy(),
  and file removal each run in their own try/catch so one failure
  doesn't skip the others
- Add default 30s timeout to sendCommand to prevent indefinite hangs
- Use JsonRpcResponse from @metamask/utils as handleRequest return type
- Add shared DaemonStatusInfo type for the getStatus RPC contract
- Change RpcHandler to allow void return, document null params
- Filter expected socket errors (EPIPE/ECONNRESET), log unexpected ones
- Only suppress ENOENT in socket unlink, re-throw other errors
- Clean up socket file (not just PID file) when stopDaemon succeeds
- Add child.on('error') handler in daemon-spawn for spawn failures
- Switch makeLogger from sync appendFileSync to async appendFile
- Remove socketPath from DaemonSpawnConfig, derive from dataDir
- Include error details in status command catch block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the required --force flag with an interactive y/N confirmation
prompt using @inquirer/confirm. The --force flag now skips the prompt
instead of being mandatory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8e1bd9d. Configure here.

}

const handler = handlers[method];
if (!handler) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handler lookup vulnerable to prototype method invocation

Low Severity

The handlers[method] lookup on a plain object doesn't guard against Object.prototype properties. If a client sends a request with method set to "constructor", "toString", "hasOwnProperty", etc., the lookup returns a truthy built-in function instead of undefined, bypassing the "Method not found" check. The server then invokes that prototype function as if it were an RPC handler, returning an unexpected result instead of a -32601 error.

Using Object.hasOwn(handlers, method) (or creating handlers with Object.create(null)) before accessing the handler would ensure only explicitly registered methods are dispatched.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8e1bd9d. Configure here.

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