Skip to content

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

Draft
rekmarks wants to merge 32 commits into
feat/wallet-libraryfrom
rekm/wallet-cli
Draft

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

Conversation

@rekmarks

@rekmarks rekmarks commented Apr 14, 2026

Copy link
Copy Markdown
Member

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)

Persistence (closes #8682)

The Node-only SQLite persistence layer that previously lived in @metamask/wallet/persistence moves to @metamask/wallet-cli so the wallet package stays platform-agnostic. The daemon now persists controller state across restarts.

  • Move KeyValueStore, loadState, and subscribeToChanges from packages/wallet/src/persistence/ to packages/wallet-cli/src/persistence/ along with their tests; relocate the better-sqlite3 + @types/better-sqlite3 deps and the prebuild-install block of the test-prep script.
  • Drop the ./persistence subpath export from @metamask/wallet (breaking change — captured in its changelog).
  • createWallet now opens a KeyValueStore at <dataDir>/wallet.db, hydrates the Wallet from loadState, wires subscribeToChanges against the wallet's messenger + controllerMetadata, and only imports the SRP on first run (detected by absence of a persisted KeyringController.vault).
  • daemon-entry.ts closes the store during shutdown and during startup-failure cleanup (logged, idempotent with wallet.destroy()).
  • On any post-construction failure (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' controllerMetadata parameter is now Readonly<Record<string, Readonly<StateMetadataConstraint>>>, matching Wallet.controllerMetadata exactly.
  • 161/161 wallet-cli tests pass at 100% line/branch/function/statement coverage.

On subsequent runs the persisted vault is reused and the SRP is unused; the wallet still starts locked (KeyringController.isUnlocked is persist: 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/wallet messenger 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-cli package (oclif mm binary) 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 (ensureDaemon spawn/polling, startRpcSocketServer request handling + shutdown, client sendCommand/pingDaemon, PID/socket/log management) and CLI commands to start, stop, status, purge, and call arbitrary 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/wallet exports to expose importSecretRecoveryPhrase for CLI wallet bootstrapping.

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

@socket-security

socket-security Bot commented Apr 14, 2026

Copy link
Copy Markdown

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

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​inquirer/​confirm@​6.0.7 ⏵ 6.0.11100 +11009896100
Added@​oclif/​core@​4.10.59810010097100

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 Outdated
@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 Outdated
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 rekmarks force-pushed the rekm/wallet-cli branch 2 times, most recently from 6687519 to 8e1bd9d Compare April 17, 2026 18:43

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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.

Comment thread packages/wallet-cli/src/daemon/rpc-socket-server.ts
@FrederikBolding FrederikBolding force-pushed the feat/wallet-library branch 2 times, most recently from d9daf4f to 88c6f09 Compare April 29, 2026 12:49
@rekmarks rekmarks force-pushed the rekm/wallet-cli branch from 8e1bd9d to 24ca871 Compare May 4, 2026 18:02
rekmarks and others added 5 commits May 4, 2026 11:04
…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>
@rekmarks rekmarks force-pushed the rekm/wallet-cli branch from 01855ed to 41a2397 Compare May 4, 2026 18:05
@rekmarks rekmarks marked this pull request as draft May 4, 2026 18:06
sirtimid and others added 8 commits May 11, 2026 20:03
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>
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>
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>
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.

2 participants