feat(wallet-cli): Add wallet CLI with daemon support#8446
Draft
rekmarks wants to merge 32 commits into
Draft
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
This comment was marked as resolved.
This comment was marked as resolved.
Base automatically changed from
rekm/wallet-library-tweaks
to
feat/wallet-library
April 14, 2026 10:50
98a9392 to
1c1a4bf
Compare
Member
Author
|
@SocketSecurity ignore npm/jake@10.9.4 Transitive via |
623f9af to
3fb60ee
Compare
45d013e to
6ec54b9
Compare
6687519 to
8e1bd9d
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
d9daf4f to
88c6f09
Compare
…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>
This was referenced May 4, 2026
The `@metamask/wallet` package is meant to be platform-agnostic, so the Node-only `better-sqlite3` persistence layer moves to the CLI together with its tests. `createWallet` now opens a `KeyValueStore` at `<dataDir>/wallet.db`, hydrates state via `loadState`, wires `subscribeToChanges`, and only imports the SRP on first run (detected by absence of a persisted KeyringController vault). On any post-construction failure the wallet is destroyed before the store closes, and first-run failures also remove the on-disk database so a retry can't latch onto an orphaned partial vault. Closes #8682 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves operational footguns surfaced by review of the daemon code:
- `pingDaemon` returns a discriminated `{ status: 'responsive' | 'absent' |
'unreachable' }` rather than a boolean. Callers can now distinguish "no
daemon" from "wedged daemon"; previously both produced silent
duplicate-spawns or wrong cleanup decisions.
- `ensureDaemon` returns `{ state: 'already-running' | 'started' }` and
refuses to spawn when the socket exists but is unreachable. The `daemon
start` command surfaces the "already running" case rather than silently
pretending new flags took effect.
- `daemon-entry` claims the daemon slot atomically: pre-flight refuses to
start when a responsive daemon owns the socket, then writes the PID file
with `flag: 'wx'` (exclusive create) recording `${pid}\n${startTime}`.
Cleanup paths only remove the PID file when its contents still match.
- `stop-daemon` only signals the recorded PID when we have evidence the
socket existed (responsive or unreachable); for absent sockets we treat
the PID file as stale and skip signalling, removing the PID-reuse
footgun. Post-stop cleanup wraps `rm` in best-effort handlers.
- `ensureDaemon` watches `child.on('exit')` so a daemon that crashes during
startup surfaces a real error pointing at the daemon log instead of a
30-second "did not start" timeout.
- `daemon call` catches socket errors and prints a friendly "daemon is not
running" hint for `ENOENT`/`ECONNREFUSED`.
- `daemon purge` deletes a whitelist of daemon-owned files (`pidPath`,
`socketPath`, `logPath`, `dbPath` + `-wal`/`-shm`) rather than rm'ing
the entire oclif `dataDir`.
- `daemon status` reports the unreachable case distinctly and warns when
the local PID file disagrees with the running daemon's reported PID.
- `isProcessAlive` rethrows unknown errors rather than treating them as
"process is gone".
- `sendCommand` verifies the response id matches the request id.
- Reworded a misleading inline comment and a `// TODO: Delete unsafe
flags` that no longer applied. Fixed a README path inaccuracy.
181 wallet-cli tests pass at 100% line/branch/function coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves issues surfaced by a second review of the daemon hardening:
- Collapse `readPidFile` (utils.ts) and `readPidFromFile` (daemon-entry.ts)
into one function in utils.ts that parses the first line. The previous
commit changed the daemon to write `${pid}\n${startTime}\n` but the
shared reader still did `Number(contents)`, which evaluated to `NaN` —
silently disabling `stopDaemon`'s SIGTERM/SIGKILL fallbacks and the
`daemon status` PID-mismatch warning. Added a round-trip test using
the production format.
- `claimDaemonSlot` now also rm's the PID file in the
`existingPid === undefined` branch. A corrupt/truncated PID file from a
crashed run would otherwise permanently block startup with EEXIST on
the exclusive `wx` write.
- `claimDaemonSlot` refuses to clobber when the socket is `unreachable`
AND the recorded PID is still alive — mirrors `ensureDaemon`'s refusal
so direct invocations of `daemon-entry` (or races against parent ping)
can't orphan a sibling daemon either.
- `daemon purge` no longer aborts when `stopDaemon` returns false on a
daemon that isn't responsive — that's the exact state purge exists to
recover from. It still refuses when a daemon IS responsive.
- `packages/wallet/CHANGELOG.md` adds an `Added` entry for the
`importSecretRecoveryPhrase` export introduced earlier on this branch.
- Tightened JSDoc accuracy on `PingResult` (don't tell callers to refuse
destructive action when stopDaemon legitimately signals on unreachable),
on `stopDaemon` and `ensureDaemon` (replace "socket file exists" with
"ping yielded non-ENOENT"), and on `createWallet` (both `password` and
`srp` are unused on subsequent runs, not only the SRP).
- New tests: EEXIST race, unreachable+alive refusal, corrupt-PID-file
recovery, PID-file format round-trip.
186 wallet-cli tests pass at 100% coverage; 9 wallet tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 3 of review-driven fixes. Closes operational/UX gaps surfaced by the branch-wide review of `6eae8aa17` and tightens the CLI surface. Daemon IPC - `PingResult` adds a `reason: 'refused' | 'timeout' | 'permission' | 'protocol' | 'other'` discriminator so callers can act per failure mode instead of string-sniffing messages. `daemon start` reports a foreign-user daemon distinctly. The unreachable error is normalised to an `Error` instance at the producer so consumers cannot crash on string/object throws. - `startRpcSocketServer` validates incoming JSON via `@metamask/utils.isJsonRpcRequest` instead of casting `JSON.parse`'s output. The per-connection 30 s idle timer is now `unref`'d and cleared on socket close/error so an aborted probe cannot stall shutdown. - Server-side dispatch failures and unexpected socket errors flow through an optional `log` callback. The persistence layer's `subscribeToChanges` accepts the same callback. `daemon-entry.ts` wires both to the daemon log, so failures that previously vanished into `stdio: 'ignore'` now land in `daemon.log`. CLI UX - `daemon stop` reports `Daemon is not running` when there is nothing to stop, instead of returning silently. - `daemon purge` uses a static import for `@inquirer/confirm` so the prompt can be mocked in tests; whitelist deletion now extends to the SQLite `-wal` / `-shm` sidecars. Tests - Un-exclude `commands/` from coverage; add command-level tests for `start`, `stop`, `status`, `purge`, `call` covering the user-visible branches (responsive/absent/unreachable, PID-mismatch warning, refuse vs. proceed in purge, ENOENT/ECONNREFUSED friendly hints, JSON-array validation, TTY vs. piped output, etc.). A shared `runCommand` harness in `src/test/` invokes commands without going through `Config.load`. - New `src/daemon/socket-integration.test.ts` exercises a real Unix socket round trip: success response, error response, methodNotFound, shutdown RPC, concurrent in-flight requests, and pipelined-request rejection. Every other test mocks one side of this boundary; this guards the seams. - `isErrorWithCode` is now duck-typed instead of `instanceof Error` so errno values that crossed a Node-built-in realm boundary (under jest's `--experimental-vm-modules`) still classify correctly. Docs - `packages/wallet-cli/CHANGELOG.md` now lists the package's initial user-facing surface (commands + SQLite persistence) without internal hardening detail. 239 wallet-cli tests pass at 100% line/branch/function coverage; 9 wallet tests pass. Lint, constraints, both changelogs validate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
External review caught issues that the prior local validation missed
because I only ran tests + lint, not `yarn build`.
Build (now passes `yarn build` from the monorepo root)
- call.ts: type rpcParams as `Json[]` instead of `unknown[]` so the
`params` field actually satisfies `JsonRpcParams`.
- purge.ts / prompts.ts: factor the `@inquirer/confirm` dynamic import
out into a small `prompts.ts` wrapper. The previous static import broke
ts-bridge's CJS emit because the package is ESM-only.
- daemon-spawn.ts: rework exitInfo to a tagged container so TS can
re-narrow it after the await boundary.
- persistence.ts: keep the two `@ts-expect-error` directives on the
dynamically-constructed event subscriptions — they were stale under
the workspace-local build (which loses event-type information) but
load-bearing under the root build, where the messenger's full event
union is visible.
Concurrency: write PID before opening the database
- The order was `claimDaemonSlot → createWallet → wx write`. Two
concurrent `daemon start` invocations could both pass the preflight,
both open `<dataDir>/wallet.db`, and both run first-run SRP import
before one lost the wx race. Move the wx write before `createWallet`
so the loser fails fast without touching the DB. Adjust cleanup paths
to handle wallet/store being undefined.
- `claimDaemonSlot` now also refuses to clobber when the recorded PID
is alive AND the socket is absent (e.g. sibling daemon mid-startup
before bind, or socket manually rm'd from under a live daemon).
Symmetric with the unreachable+alive case.
Security: filesystem permissions
- `mkdirSync(dataDir, { recursive: true, mode: 0o700 })` so another
local user cannot traverse into the daemon's data dir.
- `chmod(socketPath, 0o600)` immediately after listen() so the socket
is owner-only. The daemon explicitly exposes the full wallet messenger
to anyone who can connect, so these permissions are the only access
boundary.
Docs
- `packages/wallet/CHANGELOG.md`: link PR #8446 alongside the issue,
matching AGENTS.md's PR-link convention.
241 wallet-cli tests pass at 100% coverage; 9 wallet tests pass.
`yarn build`, `yarn lint:eslint`, `yarn lint:misc:check`,
`yarn constraints`, both `changelog:validate`s all clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `rpc-socket-server`: gate the handler lookup behind `Object.prototype.hasOwnProperty.call(handlers, method)` so a request with `method: "toString"` (or `"constructor"`, etc.) returns `methodNotFound` instead of resolving to an Object.prototype member. Closes the Cursor Bugbot prototype-method-invocation finding. - CI: `better-sqlite3@12.9.0` only ships prebuilt binaries for Node 20+ (declared `engines.node = "20.x || 22.x || 23.x || 24.x | 25.x"`), so the wallet-cli test job on 18.x fails when `prebuild-install` can't find a matching binary. Mirror the existing exclusion for `@metamask/wallet` in `.github/workflows/lint-build-test.yml`, bump `packages/wallet-cli`'s declared `engines.node` to `>=20`, and update the yarn constraint to allow that single deviation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- stop-daemon: signal the recorded PID when the socket is absent but the process is still alive. Previous behaviour treated the PID file as stale, returned success, and let `purge` wipe the SQLite DB while a live daemon still held handles. Trades a small recycled-PID risk for closing a real DB-corruption window. - daemon-entry: `chmod(dataDir, 0o700)` after `mkdirSync`. The `mode` option is ignored when the directory already exists, so first-run- with-existing-dir installs were leaving the dir at default umask. - Move `prompts.ts` out of `src/commands/daemon/` and into `src/daemon/` so oclif no longer discovers it as a `daemon:prompts` command (visible as a `command daemon:prompts not found` warning in `mm --help`). - CODEOWNERS: list `@MetaMask/ocap-kernel` alongside `@MetaMask/core- platform` for `packages/wallet-cli` to match `teams.json`. - CHANGELOG: drop the issue link from the persistence entry (both packages); AGENTS.md asks for PR links, not issue links. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 12, 2026
4 tasks
pull Bot
pushed a commit
to dmrazzy/core
that referenced
this pull request
Jun 22, 2026
## Explanation
`@metamask/wallet-cli` runs a background daemon that hosts a
`@metamask/wallet` instance; the CLI talks to it over a Unix socket.
This PR adds that transport layer. It is purely additive plumbing — no
`mm` subcommand consumes it yet (commands land in a follow-up), so there
is no user-visible behavior change.
- **`socket-line`** — `writeLine`/`readLine` for newline-delimited
framing over a `net.Socket`, with an optional read timeout and listener
cleanup.
- **`daemon-client`** — `sendCommand` opens a connection, writes one
JSON-RPC request, reads one response, and closes; it correlates the
response `id` with the request and retries once on transient connection
errors (`ECONNREFUSED`/`ECONNRESET`). `pingDaemon` is a lightweight
`getStatus` health probe that distinguishes "no daemon" (`absent`) from
"daemon present but unreachable", classifying the latter by failure mode
(`refused` / `timeout` / `permission` / `protocol` / `other`).
- **`rpc-socket-server`** — `startRpcSocketServer` listens on a Unix
socket and dispatches one JSON-RPC request per connection to a handler
map. It intercepts a built-in `shutdown` method, enforces
one-request-per-connection, times out idle connections, and returns a
`close()` handle.
- **`daemon-spawn`** — `ensureDaemon` spawns a detached daemon process
and polls until the socket is responsive, refusing to take over a wedged
or foreign-owned socket. Resolves the entry point from `dist` (prod) or
`src` via `tsx` (dev).
- **`stop-daemon`** — `stopDaemon` escalates from a graceful `shutdown`
RPC through `SIGTERM` to `SIGKILL`, then removes the PID and socket
files best-effort.
- **`prompts`** — `confirmPurge` wraps the ESM-only `@inquirer/confirm`
via dynamic import.
- **`types`** — adds `RpcHandler`, `RpcHandlerMap`, `DaemonStatusInfo`,
and `DaemonSpawnConfig`.
Adds `@inquirer/confirm` and `@metamask/rpc-errors` dependencies. Every
daemon module is covered to the package's 100% coverage thresholds, plus
a `socket-integration.test.ts` that exercises the client and server
together over a real Unix socket (framing, id correlation, the
one-request-per-connection invariant, and real shutdown timing).
### Hardening applied on top of the original branch
A multi-agent review of the ported code surfaced a few issues worth
fixing before any command consumes this transport. These are the only
deviations from the original `rekm/wallet-cli` modules (otherwise lifted
verbatim):
- **Owner-only socket permissions** (`rpc-socket-server.ts`) —
`listen()` created the Unix socket with umask-derived (typically
world-accessible) permissions. Since the daemon hosts an unlocked
wallet, any local user could connect and drive it. The server now
`chmod`s the socket to `0600` immediately after binding.
- **Fail fast on a failed spawn** (`daemon-spawn.ts`) — a `ChildProcess`
`'error'` (bad interpreter, `EACCES`, `ENOENT`) was logged to stderr but
never recorded, so the readiness loop polled for the full 30s and then
threw a generic timeout, discarding the real cause. The error is now
captured and surfaced immediately.
- **No unhandled `'error'` during a write** (`socket-line.ts`) —
`writeLine` relied solely on the write callback; a socket `'error'`
arriving mid-write had no listener and would crash the process instead
of rejecting. It now attaches a one-shot `'error'` listener for the
duration of the write.
- **Test robustness** (`socket-integration.test.ts`) — replaced an
`await import('node:net')` with a static import so the file runs
standalone without `--experimental-vm-modules`.
## References
- The daemon transport modules originate from Erik Marks's wallet-cli
branch (`rekm/wallet-cli`, MetaMask#8446). They are lifted essentially verbatim
from there, split out into this standalone, additive PR and adjusted to
apply on the current `main` (see the hardening deltas above).
## Checklist
- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [ ] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Touches local IPC and process lifecycle around an unlocked-wallet
daemon, including owner-only socket permissions and signal-based stop;
additive only with no CLI wiring yet, but mistakes could affect security
or orphan processes once commands land.
>
> **Overview**
> Adds **additive daemon IPC plumbing** to `@metamask/wallet-cli` so the
CLI can talk to a background wallet daemon over a Unix socket. Nothing
in this PR wires it into `mm` subcommands yet.
>
> The stack is newline-delimited JSON-RPC: **`socket-line`** for
framing, **`daemon-client`** (`sendCommand` with request/response id
checks and one retry on transient connect errors; **`pingDaemon`** with
`absent` / `responsive` / categorized `unreachable`), and
**`startRpcSocketServer`** (one request per connection, handler map,
built-in `shutdown`, idle timeouts). **`ensureDaemon`** spawns a
detached process and polls readiness while refusing to replace a wedged
or other-user socket; **`stopDaemon`** escalates shutdown RPC → SIGTERM
→ SIGKILL and cleans up PID/socket files. Shared **`utils`** cover PID
files and signaling; **`confirmPurge`** dynamically imports
`@inquirer/confirm`. **`types.ts`** gains RPC and spawn config types.
>
> **Security / robustness hardening** in this PR: socket files are
**`chmod` 0600** after bind (wallet daemon must not be
world-connectable), failed child **`spawn` errors fail fast** instead of
a 30s timeout, and **`writeLine`** handles mid-write socket errors.
Dependencies **`@metamask/rpc-errors`** and **`@inquirer/confirm`** are
added; changelog documents the transport layer. Broad unit tests plus
**`socket-integration.test.ts`** exercise real sockets end-to-end.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
556cbd7. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 task
sirtimid
added a commit
that referenced
this pull request
Jun 23, 2026
Add `createWallet`, which constructs a `@metamask/wallet` `Wallet` backed by the SQLite `KeyValueStore`, hydrates it from persisted state, imports the secret recovery phrase on first run, and returns a resilient `dispose` teardown handle (unsubscribe → wallet.destroy → store.close). Add the daemon process entry point (`daemon-entry.ts`) that wires `createWallet` into the JSON-RPC socket server with PID-slot claiming and graceful shutdown. Adapted from Erik Marks's #8446 modules with these deviations: - Use the current `@metamask/wallet` `instanceOptions` API, populating the now-required `storageService` (in-memory adapter), `connectivityController` (`AlwaysOnlineAdapter`), and `remoteFeatureFlagController` slots. Drop the old flat `Wallet({...})` options. - Drop `infuraProjectId` from the `Wallet` call (NetworkController is not yet wired on `main`); keep the env guard with a TODO(#9001). - Return `{ wallet, dispose }` instead of `{ wallet, store }`. - Construct a short-lived metadata probe so `loadState` can filter persisted rows against the live controller metadata. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sirtimid
added a commit
that referenced
this pull request
Jun 23, 2026
Add `createWallet`, which constructs a `@metamask/wallet` `Wallet` backed by the SQLite `KeyValueStore`, hydrates it from persisted state, imports the secret recovery phrase on first run, and returns a resilient `dispose` teardown handle (unsubscribe → wallet.destroy → store.close). Add the daemon process entry point (`daemon-entry.ts`) that wires `createWallet` into the JSON-RPC socket server with PID-slot claiming and graceful shutdown. Adapted from Erik Marks's #8446 modules with these deviations: - Use the current `@metamask/wallet` `instanceOptions` API, populating the now-required `storageService` (in-memory adapter), `connectivityController` (`AlwaysOnlineAdapter`), and `remoteFeatureFlagController` slots. Drop the old flat `Wallet({...})` options. - Drop `infuraProjectId` from the `Wallet` call (NetworkController is not yet wired on `main`); keep the env guard with a TODO(#9001). - Return `{ wallet, dispose }` instead of `{ wallet, store }`. - Construct a short-lived metadata probe so `loadState` can filter persisted rows against the live controller metadata. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sirtimid
added a commit
that referenced
this pull request
Jun 23, 2026
Add `createWallet`, which constructs a `@metamask/wallet` `Wallet` backed by the SQLite `KeyValueStore`, hydrates it from persisted state, imports the secret recovery phrase on first run, and returns a resilient `dispose` teardown handle (unsubscribe → wallet.destroy → store.close). Add the daemon process entry point (`daemon-entry.ts`) that wires `createWallet` into the JSON-RPC socket server with PID-slot claiming and graceful shutdown. Adapted from Erik Marks's #8446 modules with these deviations: - Use the current `@metamask/wallet` `instanceOptions` API, populating the now-required `storageService` (in-memory adapter), `connectivityController` (`AlwaysOnlineAdapter`), and `remoteFeatureFlagController` slots. Drop the old flat `Wallet({...})` options. - Drop `infuraProjectId` from the `Wallet` call (NetworkController is not yet wired on `main`); keep the env guard with a TODO(#9001). - Return `{ wallet, dispose }` instead of `{ wallet, store }`. - Construct a short-lived metadata probe so `loadState` can filter persisted rows against the live controller metadata. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sirtimid
added a commit
that referenced
this pull request
Jun 25, 2026
Add `createWallet`, which constructs a `@metamask/wallet` `Wallet` backed by the SQLite `KeyValueStore`, hydrates it from persisted state, imports the secret recovery phrase on first run, and returns a resilient `dispose` teardown handle (unsubscribe → wallet.destroy → store.close). Add the daemon process entry point (`daemon-entry.ts`) that wires `createWallet` into the JSON-RPC socket server with PID-slot claiming and graceful shutdown. Adapted from Erik Marks's #8446 modules with these deviations: - Use the current `@metamask/wallet` `instanceOptions` API, populating the now-required `storageService` (in-memory adapter), `connectivityController` (`AlwaysOnlineAdapter`), and `remoteFeatureFlagController` slots. Drop the old flat `Wallet({...})` options. - Drop `infuraProjectId` from the `Wallet` call (NetworkController is not yet wired on `main`); keep the env guard with a TODO(#9001). - Return `{ wallet, dispose }` instead of `{ wallet, store }`. - Construct a short-lived metadata probe so `loadState` can filter persisted rows against the live controller metadata. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

Introduces an
oclifCLI 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:
Try calling an available controller action:
Actions params must be specified as a stringified JSON array:
When done, stop the daemon and clean up:
Summary
wallet-clipackage scaffolded with oclif (mmbinary)@metamask/walletinstance as a detached background process, communicating over JSON-RPC via Unix domain socketsstart,stop,status,purge,callcallcommand dispatches arbitrary messenger actions to the running daemon (e.g.mm daemon call AccountsController:listAccounts)Persistence (closes #8682)
The Node-only SQLite persistence layer that previously lived in
@metamask/wallet/persistencemoves to@metamask/wallet-cliso the wallet package stays platform-agnostic. The daemon now persists controller state across restarts.KeyValueStore,loadState, andsubscribeToChangesfrompackages/wallet/src/persistence/topackages/wallet-cli/src/persistence/along with their tests; relocate thebetter-sqlite3+@types/better-sqlite3deps and theprebuild-installblock of the test-prep script../persistencesubpath export from@metamask/wallet(breaking change — captured in its changelog).createWalletnow opens aKeyValueStoreat<dataDir>/wallet.db, hydrates theWalletfromloadState, wiressubscribeToChangesagainst the wallet's messenger +controllerMetadata, and only imports the SRP on first run (detected by absence of a persistedKeyringController.vault).daemon-entry.tscloses the store during shutdown and during startup-failure cleanup (logged, idempotent withwallet.destroy()).subscribeToChanges/importSecretRecoveryPhrase/etc.) the wallet is destroyed before the store closes so persistence handlers unsubscribe cleanly. On a first-run failure the on-disk database (wallet.db+-wal+-shm) is removed so a retry can't latch onto an orphaned partial vault.subscribeToChanges'controllerMetadataparameter is nowReadonly<Record<string, Readonly<StateMetadataConstraint>>>, matchingWallet.controllerMetadataexactly.On subsequent runs the persisted vault is reused and the SRP is unused; the wallet still starts locked (
KeyringController.isUnlockedispersist: false), so unlocking it remains the caller's responsibility — a follow-up will wire that into the daemon.Note
Medium Risk
Adds a new CLI + background daemon that can dispatch arbitrary
@metamask/walletmessenger actions over a local Unix socket, which is operationally/security sensitive if socket permissions or secret handling are misconfigured. Most changes are additive and well-tested, but they introduce new process/spawn/shutdown and IPC behavior.Overview
Introduces a new
@metamask/wallet-clipackage (oclifmmbinary) that can start a detached wallet daemon and interact with it over newline-delimited JSON-RPC on a Unix domain socket.Adds daemon lifecycle and IPC plumbing (
ensureDaemonspawn/polling,startRpcSocketServerrequest handling +shutdown, clientsendCommand/pingDaemon, PID/socket/log management) and CLI commands tostart,stop,status,purge, andcallarbitrary wallet messenger actions (with optional JSON-array params and timeouts).Wires the new package into the monorepo (tsconfig refs, README/dependency graph, CODEOWNERS/teams ownership, ESLint exceptions, yarn workspace/export validation exceptions) and updates
@metamask/walletexports to exposeimportSecretRecoveryPhrasefor CLI wallet bootstrapping.Reviewed by Cursor Bugbot for commit 41a2397. Bugbot is set up for automated code reviews on this repo. Configure here.