Skip to content

Commit 7fe14e2

Browse files
sirtimidclaude
andauthored
refactor(evm-wallet-experiment): use new kernel-cli queueMessage subcommand (#909)
## Summary Leverages the new `ocap daemon queueMessage <kref> <method> [args]` syntax (from #896) across all evm-wallet-experiment scripts, docs, and the OpenClaw plugin. - **Replace `daemon exec queueMessage '["kref","method",[args]]'`** with the cleaner `daemon queueMessage kref method '[args]'` — eliminates nested JSON escaping and simplifies argument construction throughout `setup-home.sh`, `setup-away.sh`, and `update-limits.sh` - **Remove `parse_capdata()` helper** (~30 lines per script) — the new CLI auto-decodes CapData via `prettifySmallcaps`, so manual parsing is no longer needed - **Simplify OpenClaw plugin** — drop `decodeCapData()`, `isCapDataLike()`, and `CapDataLike` type (~60 lines) since the CLI handles CapData decoding; plugin just JSON.parses the decoded output - **Add `OCAP_HOME` env var support** — `home-interactive.mjs` default db path, log messages in scripts, and docs all respect `OCAP_HOME` - **Fix wrong `OCAP_BIN` path** in `update-limits.sh` — was pointing to non-existent `packages/cli/`, now correctly points to `packages/kernel-cli/` - **Fix error detection in `update-limits.sh`** — `prettifySmallcaps` converts `#error` objects to strings like `[TypeError: msg]`, so the old `grep '"#error"'` would never match; now uses string-prefix detection Net result: **-108 lines** across 6 files. ## Test plan These are shell scripts and documentation — no automated test suite covers them directly. Verified by: - All four bash scripts pass `bash -n` syntax checks - Grepped the entire package to confirm zero remaining `parse_capdata` or `daemon exec queueMessage` references - Reviewed the new `daemon queueMessage` CLI source (`packages/kernel-cli/src/commands/daemon.ts`) to confirm argument handling matches the new script invocations - Reviewed `prettifySmallcaps` to confirm error object encoding and fix the detection logic accordingly 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes the CLI invocation contract and output parsing used by setup/limits scripts and the OpenClaw plugin; failures would surface as broken automation or mis-detected daemon/vat errors rather than isolated unit changes. > > **Overview** > **Switches the wallet setup flows to the new `kernel-cli` `daemon queueMessage` interface.** All docs and automation scripts replace `daemon exec queueMessage` nested-JSON calls with `daemon queueMessage <kref> <method> <argsJson>`, simplifying argument construction and updating the manual command examples. > > **Removes CapData unwrapping from consumers and updates error handling.** The OpenClaw plugin and shell scripts drop `CapData`/`parse_capdata` decoding logic and instead `JSON.parse` the CLI’s decoded output, with new heuristics for detecting prettified vat errors (e.g. `"[ErrorName: msg]"`). > > **Misc reliability/ops tweaks.** Adds `OCAP_HOME` support for interactive DB/log paths, fixes `update-limits.sh` to point at `packages/kernel-cli/dist/app.mjs`, refactors remote comms JSON construction in scripts, and increases remote-comms e2e test timeouts/backoff to reduce CI flakiness. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 70bf51b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0699759 commit 7fe14e2

9 files changed

Lines changed: 367 additions & 502 deletions

File tree

packages/evm-wallet-experiment/docs/setup-guide.md

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ The script will:
251251
4. Create a Hybrid smart account and fund it if needed (may trigger a MetaMask approval for the funding tx)
252252
5. Show the OCAP URL and `setup-away.sh` command
253253

254-
Use `--reset` to purge all kernel state and start fresh. The SQLite database is at `~/.ocap/kernel-interactive.sqlite`.
254+
Use `--reset` to purge all kernel state and start fresh. The SQLite database is at `$OCAP_HOME/kernel-interactive.sqlite` (defaults to `~/.ocap/kernel-interactive.sqlite`).
255255

256256
**Note:** Interactive mode uses a Hybrid smart account (different address from the EOA) instead of EIP-7702 stateless. This is because EIP-7702 requires signing an authorization transaction that MetaMask Mobile does not support. The smart account is auto-funded from the EOA if its balance is below 0.05 ETH.
257257

@@ -335,7 +335,7 @@ When creating a delegation manually, add caveats to the `caveats` array. The `te
335335

336336
```bash
337337
# Delegation with 0.05 ETH total limit and 0.01 ETH per-transaction limit
338-
yarn ocap daemon exec queueMessage '["ko4", "createDelegation", [{
338+
yarn ocap daemon queueMessage ko4 createDelegation '[{
339339
"delegate": "0xAWAY_SMART_ACCOUNT",
340340
"caveats": [
341341
{
@@ -350,7 +350,7 @@ yarn ocap daemon exec queueMessage '["ko4", "createDelegation", [{
350350
}
351351
],
352352
"chainId": 11155111
353-
}]]'
353+
}]'
354354
```
355355

356356
### Changing limits
@@ -436,7 +436,7 @@ The home device holds the master wallet keys and runs a kernel daemon that the a
436436
yarn ocap daemon start
437437
```
438438

439-
This starts the OCAP daemon at `~/.ocap/daemon.sock` with persistent storage at `~/.ocap/kernel.sqlite`.
439+
This starts the OCAP daemon at `~/.ocap/daemon.sock` with persistent storage at `~/.ocap/kernel.sqlite`. You can override the `~/.ocap` base directory by setting the `OCAP_HOME` environment variable (e.g. `export OCAP_HOME=/data/ocap`).
440440

441441
### 2b. Initialize remote comms (QUIC)
442442

@@ -495,34 +495,34 @@ Note the `rootKref` from the output (e.g. `ko4`). This is the wallet coordinator
495495

496496
```bash
497497
# Initialize with your mnemonic (SRP) — plaintext storage
498-
yarn ocap daemon exec queueMessage '["ko4", "initializeKeyring", [{"type": "srp", "mnemonic": "your twelve word mnemonic phrase here"}]]'
498+
yarn ocap daemon queueMessage ko4 initializeKeyring '[{"type": "srp", "mnemonic": "your twelve word mnemonic phrase here"}]'
499499

500500
# Or encrypt the mnemonic at rest with a password:
501501
SALT="$(node -e "process.stdout.write(require('crypto').randomBytes(16).toString('hex'))")"
502-
yarn ocap daemon exec queueMessage "[\"ko4\", \"initializeKeyring\", [{\"type\": \"srp\", \"mnemonic\": \"your twelve word mnemonic phrase here\", \"password\": \"your-password\", \"salt\": \"$SALT\"}]]"
502+
yarn ocap daemon queueMessage ko4 initializeKeyring "[{\"type\": \"srp\", \"mnemonic\": \"your twelve word mnemonic phrase here\", \"password\": \"your-password\", \"salt\": \"$SALT\"}]"
503503

504504
# Verify
505-
yarn ocap daemon exec queueMessage '["ko4", "getAccounts", []]'
505+
yarn ocap daemon queueMessage ko4 getAccounts
506506
```
507507

508508
When a password is provided, the mnemonic is encrypted with AES-256-GCM (PBKDF2 key derivation). After a daemon restart, the keyring will be locked — unlock it before signing:
509509

510510
```bash
511-
yarn ocap daemon exec queueMessage '["ko4", "unlockKeyring", ["your-password"]]'
511+
yarn ocap daemon queueMessage ko4 unlockKeyring '["your-password"]'
512512
```
513513

514514
### 2e. Configure the provider
515515

516516
```bash
517-
yarn ocap daemon exec queueMessage '["ko4", "configureProvider", [{"chainId": 11155111, "rpcUrl": "https://sepolia.infura.io/v3/YOUR_INFURA_KEY"}]]'
517+
yarn ocap daemon queueMessage ko4 configureProvider '[{"chainId": 11155111, "rpcUrl": "https://sepolia.infura.io/v3/YOUR_INFURA_KEY"}]'
518518
# For other chains, adjust chainId and rpcUrl:
519-
# yarn ocap daemon exec queueMessage '["ko4", "configureProvider", [{"chainId": 8453, "rpcUrl": "https://base-mainnet.infura.io/v3/YOUR_INFURA_KEY"}]]'
519+
# yarn ocap daemon queueMessage ko4 configureProvider '[{"chainId": 8453, "rpcUrl": "https://base-mainnet.infura.io/v3/YOUR_INFURA_KEY"}]'
520520
```
521521

522522
### 2f. Issue an OCAP URL for the away device
523523

524524
```bash
525-
yarn ocap daemon exec queueMessage '["ko4", "issueOcapUrl", []]'
525+
yarn ocap daemon queueMessage ko4 issueOcapUrl
526526
```
527527

528528
Save the returned `ocap:...` URL and the listen addresses from `getStatus` above — you'll give both to the away device.
@@ -565,19 +565,19 @@ The away wallet gets a throwaway key (for signing UserOps within delegations). U
565565

566566
```bash
567567
ENTROPY="0x$(node -e "process.stdout.write(require('crypto').randomBytes(32).toString('hex'))")"
568-
yarn ocap daemon exec queueMessage "[\"ko4\", \"initializeKeyring\", [{\"type\": \"throwaway\", \"entropy\": \"$ENTROPY\"}]]"
568+
yarn ocap daemon queueMessage ko4 initializeKeyring "[{\"type\": \"throwaway\", \"entropy\": \"$ENTROPY\"}]"
569569
```
570570

571571
### 3f. Connect to the home wallet
572572

573573
```bash
574-
yarn ocap daemon exec queueMessage '["ko4", "connectToPeer", ["ocap:zgAu...YOUR_OCAP_URL_HERE"]]'
574+
yarn ocap daemon queueMessage ko4 connectToPeer '["ocap:zgAu...YOUR_OCAP_URL_HERE"]'
575575
```
576576

577577
### 3g. Verify the connection
578578

579579
```bash
580-
yarn ocap daemon exec queueMessage '["ko4", "getCapabilities", []]'
580+
yarn ocap daemon queueMessage ko4 getCapabilities
581581
```
582582

583583
Should show `hasPeerWallet: true`.
@@ -594,43 +594,43 @@ For manual setup, the steps are:
594594

595595
```bash
596596
# Home device (EIP-7702 — EOA becomes the smart account):
597-
yarn ocap daemon exec queueMessage '["ko4", "createSmartAccount", [{"chainId": 11155111, "implementation": "stateless7702"}]]'
597+
yarn ocap daemon queueMessage ko4 createSmartAccount '[{"chainId": 11155111, "implementation": "stateless7702"}]'
598598

599599
# Away device (Hybrid — deploys on first UserOp):
600-
yarn ocap daemon exec queueMessage '["ko4", "createSmartAccount", [{"chainId": 11155111}]]'
600+
yarn ocap daemon queueMessage ko4 createSmartAccount '[{"chainId": 11155111}]'
601601
```
602602

603603
The home EOA's existing ETH balance is used directly for delegated transfers — no separate funding step needed.
604604

605605
2. Read the delegate address from the away device (sent automatically by the setup flow after peer connection):
606606

607607
```bash
608-
yarn ocap daemon exec queueMessage '["ko4", "getDelegateAddress", []]'
608+
yarn ocap daemon queueMessage ko4 getDelegateAddress
609609
```
610610

611611
3. Create the delegation on the home device (delegate = away smart account). See [Spending limits](#spending-limits) for adding caveats:
612612

613613
```bash
614-
yarn ocap daemon exec queueMessage '["ko4", "createDelegation", [{"delegate": "0xAWAY_SMART_ACCOUNT", "caveats": [], "chainId": 11155111}]]'
614+
yarn ocap daemon queueMessage ko4 createDelegation '[{"delegate": "0xAWAY_SMART_ACCOUNT", "caveats": [], "chainId": 11155111}]'
615615
```
616616

617617
4. Push the delegation to the away device (if connected):
618618

619619
```bash
620-
yarn ocap daemon exec queueMessage '["ko4", "pushDelegationToAway", [<DELEGATION_JSON>]]'
620+
yarn ocap daemon queueMessage ko4 pushDelegationToAway '[<DELEGATION_JSON>]'
621621
```
622622

623623
Or transfer manually if the away device is offline:
624624

625625
```bash
626626
# On the away device:
627-
yarn ocap daemon exec queueMessage '["ko4", "receiveDelegation", [<DELEGATION_JSON>]]'
627+
yarn ocap daemon queueMessage ko4 receiveDelegation '[<DELEGATION_JSON>]'
628628
```
629629

630630
5. Verify:
631631

632632
```bash
633-
yarn ocap daemon exec queueMessage '["ko4", "getCapabilities", []]'
633+
yarn ocap daemon queueMessage ko4 getCapabilities
634634
# Should show delegationCount: 1
635635
```
636636

@@ -659,26 +659,26 @@ You can also call the wallet coordinator directly. Replace `ko4` with your `root
659659

660660
```bash
661661
# List accounts
662-
yarn ocap daemon exec queueMessage '["ko4", "getAccounts", []]'
662+
yarn ocap daemon queueMessage ko4 getAccounts
663663

664664
# Check capabilities (local keys, peer wallet, delegations, bundler)
665-
yarn ocap daemon exec queueMessage '["ko4", "getCapabilities", []]'
665+
yarn ocap daemon queueMessage ko4 getCapabilities
666666

667667
# Sign a message
668-
yarn ocap daemon exec queueMessage '["ko4", "signMessage", ["hello world"]]'
668+
yarn ocap daemon queueMessage ko4 signMessage '["hello world"]'
669669

670670
# Sign a transaction
671-
yarn ocap daemon exec queueMessage '["ko4", "signTransaction", [{"to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", "value": "0x2386F26FC10000", "chainId": 11155111}]]'
671+
yarn ocap daemon queueMessage ko4 signTransaction '[{"to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", "value": "0x2386F26FC10000", "chainId": 11155111}]'
672672

673673
# Query the chain (eth_getBalance, eth_blockNumber, etc.)
674-
yarn ocap daemon exec queueMessage '["ko4", "request", ["eth_getBalance", ["0x71fA1599e6c6FE46CD2A798E136f3ba22863cF82", "latest"]]]'
675-
yarn ocap daemon exec queueMessage '["ko4", "request", ["eth_blockNumber", []]]'
674+
yarn ocap daemon queueMessage ko4 request '["eth_getBalance", ["0x71fA1599e6c6FE46CD2A798E136f3ba22863cF82", "latest"]]'
675+
yarn ocap daemon queueMessage ko4 request '["eth_blockNumber", []]'
676676

677677
# Create a delegation for another address
678-
yarn ocap daemon exec queueMessage '["ko4", "createDelegation", [{"delegate": "0x...", "caveats": [], "chainId": 11155111}]]'
678+
yarn ocap daemon queueMessage ko4 createDelegation '[{"delegate": "0x...", "caveats": [], "chainId": 11155111}]'
679679

680680
# List active delegations
681-
yarn ocap daemon exec queueMessage '["ko4", "listDelegations", []]'
681+
yarn ocap daemon queueMessage ko4 listDelegations
682682
```
683683

684684
## 6. How it works
@@ -690,7 +690,7 @@ Agent (AI)
690690
├─ wallet_send ──→ │
691691
└─ wallet_sign ──→ │
692692
693-
yarn ocap daemon exec queueMessage
693+
yarn ocap daemon queueMessage
694694
695695
OCAP Daemon (Unix socket)
696696
Lines changed: 37 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/**
22
* Daemon communication layer for the OpenClaw wallet plugin.
33
*
4-
* Spawns `ocap daemon exec` commands and decodes Endo CapData responses.
4+
* Spawns `ocap daemon queueMessage` commands. The CLI auto-decodes CapData
5+
* via prettifySmallcaps, so no manual CapData unwrapping is needed here.
56
*/
67
import { spawn } from 'node:child_process';
78

89
type ExecResult = { stdout: string; stderr: string; code: number | null };
9-
type CapDataLike = { body: string; slots: unknown[] };
1010

1111
export type WalletCallOptions = {
1212
cliPath: string;
@@ -51,89 +51,25 @@ export function makeWalletCaller(options: {
5151
}
5252

5353
/**
54-
* Check if a value looks like Endo CapData.
55-
*
56-
* @param value - The parsed JSON value.
57-
* @returns True when value has CapData shape.
58-
*/
59-
function isCapDataLike(value: unknown): value is CapDataLike {
60-
if (typeof value !== 'object' || value === null) {
61-
return false;
62-
}
63-
if (!('body' in value) || !('slots' in value)) {
64-
return false;
65-
}
66-
const { body } = value as { body?: unknown };
67-
const { slots } = value as { slots?: unknown };
68-
return typeof body === 'string' && Array.isArray(slots);
69-
}
70-
71-
/**
72-
* Decode daemon JSON output, unwrapping Endo CapData.
73-
*
74-
* @param raw - Raw stdout from `ocap daemon exec`.
75-
* @param method - Wallet method name (for better errors).
76-
* @returns The decoded value.
77-
*/
78-
function decodeCapData(raw: string, method: string): unknown {
79-
let parsed: unknown;
80-
try {
81-
parsed = JSON.parse(raw);
82-
} catch {
83-
throw new Error(`Wallet ${method} returned non-JSON output`);
84-
}
85-
86-
if (!isCapDataLike(parsed)) {
87-
return parsed;
88-
}
89-
90-
if (!parsed.body.startsWith('#')) {
91-
throw new Error(`Wallet ${method} returned invalid CapData body`);
92-
}
93-
94-
const bodyContent = parsed.body.slice(1);
95-
96-
// Handle error bodies from vat exceptions (e.g. "#error:message")
97-
if (bodyContent.startsWith('error:')) {
98-
throw new Error(`Wallet ${method} vat error: ${bodyContent.slice(6)}`);
99-
}
100-
101-
let decoded: unknown;
102-
try {
103-
decoded = JSON.parse(bodyContent);
104-
} catch {
105-
throw new Error(`Wallet ${method} returned undecodable CapData body`);
106-
}
107-
108-
// Handle Endo CapData error encoding: #{"#error": "message", ...}
109-
if (decoded !== null && typeof decoded === 'object' && '#error' in decoded) {
110-
const errorMsg = (decoded as Record<string, unknown>)['#error'];
111-
throw new Error(
112-
`Wallet ${method} failed: ${typeof errorMsg === 'string' ? errorMsg : JSON.stringify(errorMsg)}`,
113-
);
114-
}
115-
116-
return decoded;
117-
}
118-
119-
/**
120-
* Run an `ocap daemon exec` command and return its output.
54+
* Run an `ocap daemon queueMessage` command and return its output.
12155
*
12256
* @param options - Execution options.
12357
* @param options.cliPath - Path to the ocap CLI.
124-
* @param options.method - The daemon RPC method.
125-
* @param options.params - The method parameters.
58+
* @param options.walletKref - KRef of the target kernel object.
59+
* @param options.method - Method name to invoke.
60+
* @param options.argsJson - JSON-encoded array of arguments.
12661
* @param options.timeoutMs - Timeout in ms.
12762
* @returns The command result.
12863
*/
129-
async function runDaemonExec(options: {
64+
async function runDaemonQueueMessage(options: {
13065
cliPath: string;
66+
walletKref: string;
13167
method: string;
132-
params: unknown;
68+
argsJson: string;
13369
timeoutMs: number;
13470
}): Promise<ExecResult> {
135-
const { cliPath, method, params, timeoutMs } = options;
136-
const daemonArgs = ['daemon', 'exec', method, JSON.stringify(params)];
71+
const { cliPath, walletKref, method, argsJson, timeoutMs } = options;
72+
const daemonArgs = ['daemon', 'queueMessage', walletKref, method, argsJson];
13773

13874
// If cliPath points to a .mjs file, invoke it via node.
13975
const command = cliPath.endsWith('.mjs') ? 'node' : cliPath;
@@ -160,7 +96,9 @@ async function runDaemonExec(options: {
16096
try {
16197
child.kill('SIGKILL');
16298
} finally {
163-
reject(new Error(`ocap daemon exec timed out after ${timeoutMs}ms`));
99+
reject(
100+
new Error(`ocap daemon queueMessage timed out after ${timeoutMs}ms`),
101+
);
164102
}
165103
}, timeoutMs);
166104

@@ -179,15 +117,19 @@ async function runDaemonExec(options: {
179117
/**
180118
* Call a wallet coordinator method via the OCAP daemon.
181119
*
120+
* The CLI's `daemon queueMessage` auto-decodes CapData via prettifySmallcaps,
121+
* so the output is already a decoded JSON value.
122+
*
182123
* @param options - Call options.
183124
* @returns The decoded response value.
184125
*/
185126
async function callWallet(options: WalletCallOptions): Promise<unknown> {
186127
const { cliPath, walletKref, method, args, timeoutMs } = options;
187-
const result = await runDaemonExec({
128+
const result = await runDaemonQueueMessage({
188129
cliPath,
189-
method: 'queueMessage',
190-
params: [walletKref, method, args],
130+
walletKref,
131+
method,
132+
argsJson: JSON.stringify(args),
191133
timeoutMs,
192134
});
193135

@@ -196,5 +138,20 @@ async function callWallet(options: WalletCallOptions): Promise<unknown> {
196138
throw new Error(`Wallet ${method} failed (exit ${result.code}): ${detail}`);
197139
}
198140

199-
return decodeCapData(result.stdout.trim(), method);
141+
const raw = result.stdout.trim();
142+
let decoded: unknown;
143+
try {
144+
decoded = JSON.parse(raw);
145+
} catch {
146+
throw new Error(`Wallet ${method} returned non-JSON output`);
147+
}
148+
149+
// prettifySmallcaps converts #error objects to "[ErrorName: msg]" strings,
150+
// but also converts manifest constants to "[undefined]", "[NaN]", etc.
151+
// Distinguish errors by checking for the ": " separator after the bracket.
152+
if (typeof decoded === 'string' && /^\[.+: /u.test(decoded)) {
153+
throw new Error(`Wallet ${method} failed: ${decoded}`);
154+
}
155+
156+
return decoded;
200157
}

0 commit comments

Comments
 (0)