Skip to content

Commit cb0c672

Browse files
committed
Merge remote-tracking branch 'origin/main' into sirtimid/integrate-snaps-network-endowment
2 parents fd045f4 + 190af28 commit cb0c672

19 files changed

Lines changed: 398 additions & 210 deletions
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
name: evm-wallet-docker-e2e
3+
description: Run the evm-wallet Docker e2e tests (build, start stack, wait for healthy, test, diagnose failures).
4+
---
5+
6+
Run all commands from the repo root unless noted.
7+
8+
## 1. Verify Docker is running
9+
10+
```bash
11+
docker info 2>&1 | head -5
12+
```
13+
14+
If it fails, tell the user Docker is not running and ask them to start Docker Desktop (or the daemon), then wait for confirmation before continuing.
15+
16+
## 2. Build the repo and Docker images
17+
18+
```bash
19+
yarn workspace @ocap/evm-wallet-experiment docker:build 2>&1 | tail -30
20+
```
21+
22+
This builds the full monorepo then builds the Docker images. It may take a few minutes. Report any errors from the tail output.
23+
24+
## 3. Tear down any existing stack, then start fresh
25+
26+
Always bring the stack down first to avoid stale container state (e.g. spent delegation budgets from a previous run leaking into the new run).
27+
28+
```bash
29+
yarn workspace @ocap/evm-wallet-experiment docker:down 2>&1 | tail -10
30+
```
31+
32+
Then start the stack:
33+
34+
```bash
35+
yarn workspace @ocap/evm-wallet-experiment docker:ensure-logs && \
36+
yarn workspace @ocap/evm-wallet-experiment docker:compose up -d 2>&1 | tail -20
37+
```
38+
39+
## 4. Wait for all services to be healthy
40+
41+
Poll every 10 seconds (up to 3 minutes / 18 attempts). All 8 services must reach `(healthy)` status before proceeding:
42+
43+
- `evm`, `bundler`
44+
- `kernel-home-bundler-7702`, `kernel-away-bundler-7702`
45+
- `kernel-home-bundler-hybrid`, `kernel-away-bundler-hybrid`
46+
- `kernel-home-peer-relay`, `kernel-away-peer-relay`
47+
48+
```bash
49+
i=0; while [ $i -lt 18 ]; do
50+
i=$((i+1))
51+
ps_out=$(yarn workspace @ocap/evm-wallet-experiment docker:ps 2>&1)
52+
healthy=$(echo "$ps_out" | grep -c "(healthy)" || true)
53+
echo "Attempt $i/18: $healthy/8 healthy"
54+
if [ "$healthy" -ge 8 ]; then echo "Stack ready."; break; fi
55+
if [ "$i" -eq 18 ]; then echo "Timed out:"; echo "$ps_out"; exit 1; fi
56+
sleep 10
57+
done
58+
```
59+
60+
If the loop exits with a timeout, show the last `docker:ps` output and stop — do not proceed to the tests.
61+
62+
## 5. Run the e2e tests
63+
64+
```bash
65+
yarn workspace @ocap/evm-wallet-experiment test:e2e:docker 2>&1 | tail -80
66+
```
67+
68+
The vitest reporter also writes structured results to `packages/evm-wallet-experiment/logs/test-results.json`.
69+
70+
## 6. Diagnose failures
71+
72+
If tests fail, investigate in this order:
73+
74+
### Structured test results
75+
76+
```bash
77+
cat packages/evm-wallet-experiment/logs/test-results.json
78+
```
79+
80+
Look at the `testResults` array for failed tests and their error messages.
81+
82+
### Service logs
83+
84+
Container logs are written to `packages/evm-wallet-experiment/logs/`. Check the service(s) relevant to the failing test mode first:
85+
86+
```bash
87+
tail -150 packages/evm-wallet-experiment/logs/<service>.log
88+
```
89+
90+
Service log files:
91+
92+
- `evm.log` — Anvil chain (check for on-chain errors)
93+
- `kernel-home-bundler-7702.log`, `kernel-away-bundler-7702.log`
94+
- `kernel-home-bundler-hybrid.log`, `kernel-away-bundler-hybrid.log`
95+
- `kernel-home-peer-relay.log`, `kernel-away-peer-relay.log`
96+
97+
Start with the pair(s) involved in the failing test, then `evm.log` for on-chain issues.

packages/evm-wallet-experiment/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# @ocap/evm-wallet-experiment
22

3-
A capability-driven EVM wallet implemented as an OCAP kernel subcluster. It uses the [MetaMask Delegation Framework (Gator)](https://github.com/MetaMask/delegation-framework) for delegated transaction authority. **Hybrid** smart accounts submit ERC-4337 UserOperations through a bundler; **stateless EIP-7702** home accounts (mnemonic path) redeem delegations with normal EIP-1559 transactions via your JSON-RPC provider (e.g. Infura), without a bundler. The wallet subcluster isolates key management, Ethereum RPC communication, and delegation lifecycle into separate vats, enforcing the principle of least authority across the entire signing pipeline.
3+
A capability-driven EVM wallet, implemented as an OCAP kernel subcluster. It uses the [MetaMask Delegation Framework (Gator)](https://github.com/MetaMask/delegation-framework) for delegated transaction authority. **Hybrid** smart accounts submit ERC-4337 UserOperations through a bundler; **stateless EIP-7702** home accounts (mnemonic path) redeem delegations with normal EIP-1559 transactions via your JSON-RPC provider (e.g. Infura), without a bundler. The wallet subcluster isolates key management, Ethereum RPC communication, and delegation lifecycle into separate vats, enforcing the principle of least authority across the entire signing pipeline.
44

55
For a deeper explanation of the components and data flow, see [How It Works](./docs/how-it-works.md). For deploying the wallet on a home device + VPS with OpenClaw, see the [Setup Guide](./docs/setup-guide.md).
66

@@ -712,7 +712,7 @@ yarn workspace @ocap/evm-wallet-experiment build
712712
yarn workspace @ocap/evm-wallet-experiment lint:fix
713713
```
714714

715-
For Docker Compose setup (interactive simulation and E2E tests), see [docs/docker.md](./docs/docker.md). Docker Model Runner with `ai/qwen3.5:4B-UD-Q4_K_XL` is required for the interactive simulation's OpenClaw AI agent.
715+
For Docker Compose setup (local demo and E2E tests), see [docs/docker.md](./docs/docker.md). Docker Model Runner with `ai/qwen3.5:4B-UD-Q4_K_XL` is required for the local demo's OpenClaw AI agent.
716716

717717
## Testing
718718

@@ -803,7 +803,7 @@ DELEGATION_MODE=bundler-7702 yarn workspace @ocap/evm-wallet-experiment test:e2e
803803
yarn workspace @ocap/evm-wallet-experiment docker:down
804804
```
805805

806-
Full home/away delegation flow across three delegation modes (`bundler-7702`, `bundler-hybrid`, `peer-relay`) running in parallel. The stack requires Docker Model Runner. See [docs/docker.md](./docs/docker.md) for prerequisites, stack details, and troubleshooting. For manual interactive simulation, see [docs/simulation.md](./docs/simulation.md).
806+
Full home/away delegation flow across three delegation modes (`bundler-7702`, `bundler-hybrid`, `peer-relay`) running in parallel. The stack requires Docker Model Runner. See [docs/docker.md](./docs/docker.md) for prerequisites, stack details, and troubleshooting. For manual interactive simulation, see [docs/demo-local.md](./docs/demo-local.md).
807807

808808
## Supported Chains
809809

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Used by `yarn docker:demo:up` (--env-file). Switches
2+
# `kernel-away-bundler-7702` to the `demo` Dockerfile target (OpenClaw).
3+
KERNEL_AWAY_7702_TARGET=demo

packages/evm-wallet-experiment/docker/.env.interactive

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/evm-wallet-experiment/docker/Dockerfile.kernel-base

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ COPY --from=builder /build /app
6262
RUN mkdir -p /logs /run/ocap
6363

6464
# ---------------------------------------------------------------------------
65-
# Target: interactive — kernel + OpenClaw + wallet plugin (used interactively)
65+
# Target: demo — kernel + OpenClaw + wallet plugin (used in local demo)
6666
# ---------------------------------------------------------------------------
67-
FROM kernel AS interactive
67+
FROM kernel AS demo
6868

6969
# OpenClaw loads local plugins as TypeScript via jiti (no extra TS runner in the image).
70-
# `package.json` docker:interactive:setup starts the gateway via
70+
# `package.json` docker:demo:setup starts the gateway via
7171
# `node /usr/local/lib/node_modules/openclaw/openclaw.mjs` (global bin PATH is unreliable under `docker exec`).
7272
RUN npm install -g openclaw@2026.4.1

packages/evm-wallet-experiment/docker/MAINTAINERS.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Docker stack — maintainer notes
22

3-
Local E2E stack for `@ocap/evm-wallet-experiment`: Anvil + deployed contracts, Pimlico Alto, and six kernel containers (three `kernel-home-*` / `kernel-away-*` pairs). See `package.json` scripts (`docker:compose`, `test:e2e:docker`, etc.). Each pair is gated by a Compose **profile** (**`7702`**, **`4337`**, **`relay`**); **`yarn docker:up`** / **`docker:compose`** pass **all three** so Vitest Docker E2E and full-stack dev see every kernel. **`yarn docker:interactive:up`** enables **one** profile (default **`bundler-7702`** delegation mode → profile **`7702`**). Shared kernel **build / volumes / entrypoint / depends_on** live in root **`x-kernel-standard`**; **`x-kernel-build-core`** holds **`context`** / **`dockerfile`** so **`kernel-away-bundler-7702`** can set **`build.target`** from **`${KERNEL_AWAY_7702_TARGET:-kernel}`**. Per-pair **ports**, **`environment`**, and **`healthcheck.test`** stay explicit.
3+
Local E2E stack for `@ocap/evm-wallet-experiment`: Anvil + deployed contracts, Pimlico Alto, and six kernel containers (three `kernel-home-*` / `kernel-away-*` pairs). See `package.json` scripts (`docker:compose`, `test:e2e:docker`, etc.). Each pair is gated by a Compose **profile** (**`7702`**, **`4337`**, **`relay`**); **`yarn docker:up`** / **`docker:compose`** pass **all three** so Vitest Docker E2E and full-stack dev see every kernel. **`yarn docker:demo:up`** enables **one** profile (default **`bundler-7702`** delegation mode → profile **`7702`**). Shared kernel **build / volumes / entrypoint / depends_on** live in root **`x-kernel-standard`**; **`x-kernel-build-core`** holds **`context`** / **`dockerfile`** so **`kernel-away-bundler-7702`** can set **`build.target`** from **`${KERNEL_AWAY_7702_TARGET:-kernel}`**. Per-pair **ports**, **`environment`**, and **`healthcheck.test`** stay explicit.
44

55
## Startup order
66

@@ -30,7 +30,7 @@ Copy the top-level **Digest** (index), then set in `docker-compose.yml`:
3030

3131
Keep the comment above that line in sync with the command you used.
3232

33-
### OpenClaw (interactive image only)
33+
### OpenClaw (demo image only)
3434

3535
`Dockerfile.kernel-base` installs a **fixed** global CLI version (`openclaw@…`). The gateway loads **`openclaw-plugin/index.ts`** via **jiti**; nothing in the image invokes `tsx`. Bump OpenClaw deliberately when you want new gateway behavior; avoid `@latest` here.
3636

@@ -62,16 +62,16 @@ Host-side scripts (e.g. `yarn docker:setup:wallets`) use the workspace **`tsx`**
6262

6363
- **`yarn docker:up`** and **`yarn test:e2e:docker`** expect the full stack, including [**Compose `models`**](https://docs.docker.com/ai/compose/models-and-compose/) on each **`kernel-away-*`** service. That requires **Docker Compose v2.38+** and [**Docker Model Runner**](https://docs.docker.com/ai/model-runner/) enabled.
6464
- Top-level **`models.llm`** pins **`ai/qwen3.5:4B-UD-Q4_K_XL`** with **`context_size: 32768`** and **`runtime_flags: ['--ctx-size','32768']`** so llama.cpp does not stay at DMR’s 4096 default (OpenClaw + tools need more). Pull if needed: **`docker model pull ai/qwen3.5:4B-UD-Q4_K_XL`**. If requests still hit 4096, run **`docker model configure --context-size 32768 ai/qwen3.5:4B-UD-Q4_K_XL`** on the host and recreate containers.
65-
- Vitest Docker E2E does **not** call the LLM today, but away containers still receive **`LLM_URL`** / **`LLM_MODEL`** for consistency with interactive OpenClaw and future tests.
65+
- Vitest Docker E2E does **not** call the LLM today, but away containers still receive **`LLM_URL`** / **`LLM_MODEL`** for consistency with the demo OpenClaw stack and future tests.
6666

67-
## Interactive stack (`docker/.env.interactive` + one pair profile)
67+
## Demo stack (`docker/.env.demo` + one pair profile)
6868

69-
- **`yarn docker:compose:interactive`** runs **`node docker/run-interactive-compose.mjs`**, which passes **`--env-file docker/.env.interactive`** ( **`KERNEL_AWAY_7702_TARGET=interactive`** for the 7702 away image) and **one** **`--profile`** (**`7702`**, **`4337`**, or **`relay`**). Default delegation mode is **`bundler-7702`** → profile **`7702`** (same mode strings as Docker E2E **`DELEGATION_MODE`**).
70-
- **Choose the pair**: set **`OCAP_INTERACTIVE_PAIR`** to **`bundler-7702`**, **`bundler-hybrid`**, or **`peer-relay`**, or pass **`--pair <value>`** before compose subcommands (after **`yarn … --`** if needed), e.g. **`yarn docker:interactive:up -- --pair bundler-hybrid`**.
71-
- **`yarn docker:interactive:setup`** runs wallet setup; OpenClaw **`setup-openclaw.mjs`** + gateway run **only** when the pair is **`bundler-7702`** (the image with OpenClaw). Other pairs skip those steps with a short log line.
72-
- OpenClaw UI history for 7702 lives under **`$HOME/.openclaw`** on the **`ocap-run`** volume; use **`yarn docker:interactive:reset-openclaw`** then **`yarn docker:interactive:setup`**, or **`docker compose … down -v`** for a full volume wipe. LLM wiring is **only** in **`docker-compose.yml`** (**`models:`**).
69+
- **`yarn docker:compose:demo`** runs **`node docker/run-demo-compose.mjs`**, which passes **`--env-file docker/.env.demo`** ( **`KERNEL_AWAY_7702_TARGET=demo`** for the 7702 away image) and **one** **`--profile`** (**`7702`**, **`4337`**, or **`relay`**). Default delegation mode is **`bundler-7702`** → profile **`7702`** (same mode strings as Docker E2E **`DELEGATION_MODE`**).
70+
- **Choose the pair**: set **`OCAP_DEMO_PAIR`** to **`bundler-7702`**, **`bundler-hybrid`**, or **`peer-relay`**, or pass **`--pair <value>`** before compose subcommands (after **`yarn … --`** if needed), e.g. **`yarn docker:demo:up -- --pair bundler-hybrid`**.
71+
- **`yarn docker:demo:setup`** runs wallet setup; OpenClaw **`setup-openclaw.mjs`** + gateway run **only** when the pair is **`bundler-7702`** (the image with OpenClaw). Other pairs skip those steps with a short log line.
72+
- OpenClaw UI history for 7702 lives under **`$HOME/.openclaw`** on the **`ocap-run`** volume; use **`yarn docker:demo:reset-openclaw`** then **`yarn docker:demo:setup`**, or **`docker compose … down -v`** for a full volume wipe. LLM wiring is **only** in **`docker-compose.yml`** (**`models:`**).
7373

74-
**Raw `docker compose -f docker/docker-compose.yml up`** without **`--profile`** starts **evm** and **bundler** only (no kernels). Prefer **`yarn docker:up`** or the interactive scripts above.
74+
**Raw `docker compose -f docker/docker-compose.yml up`** without **`--profile`** starts **evm** and **bundler** only (no kernels). Prefer **`yarn docker:up`** or the demo scripts above.
7575

7676
## `yarn docker:delegate`
7777

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* eslint-disable n/no-sync, n/no-process-env, n/no-process-exit */
2+
import { spawnSync } from 'node:child_process';
3+
4+
import {
5+
awayServiceForDemoPair,
6+
homeServiceForDemoPair,
7+
demoDockerComposeArgs,
8+
DEMO_PACKAGE_ROOT,
9+
} from './demo-compose-lib.mjs';
10+
11+
const [side, ...rest] = process.argv.slice(2);
12+
13+
if (side !== 'away' && side !== 'home') {
14+
console.error(`Usage: attach.mjs <away|home> [--pair <pair>]`);
15+
process.exit(1);
16+
}
17+
18+
const { pair, dockerArgs } = demoDockerComposeArgs(rest);
19+
const service =
20+
side === 'away' ? awayServiceForDemoPair(pair) : homeServiceForDemoPair(pair);
21+
22+
const spawned = spawnSync(
23+
'docker',
24+
[...dockerArgs, 'exec', '-it', service, 'bash'],
25+
{
26+
cwd: DEMO_PACKAGE_ROOT,
27+
stdio: 'inherit',
28+
env: process.env,
29+
},
30+
);
31+
process.exit(spawned.status ?? 1);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/* eslint-disable n/no-sync, n/no-process-env, n/no-process-exit */
2+
import { spawnSync } from 'node:child_process';
3+
4+
import {
5+
homeServiceForDemoPair,
6+
demoDockerComposeArgs,
7+
DEMO_PACKAGE_ROOT,
8+
} from './demo-compose-lib.mjs';
9+
10+
const SCRIPT_ON_HOST = 'docker/create-delegation.mjs';
11+
const SCRIPT_IN_CONTAINER =
12+
'/app/packages/evm-wallet-experiment/docker/create-delegation.mjs';
13+
14+
const argv = process.argv.slice(2);
15+
const { pair, dockerArgs } = demoDockerComposeArgs(argv);
16+
const home = homeServiceForDemoPair(pair);
17+
18+
const cp = spawnSync(
19+
'docker',
20+
[...dockerArgs, 'cp', SCRIPT_ON_HOST, `${home}:${SCRIPT_IN_CONTAINER}`],
21+
{ cwd: DEMO_PACKAGE_ROOT, stdio: 'inherit', env: process.env },
22+
);
23+
if (cp.status !== 0) {
24+
process.exit(cp.status ?? 1);
25+
}
26+
27+
const envArgs = ['--env', `DELEGATION_MODE=${pair}`];
28+
if (process.env.CAVEAT_ETH_LIMIT) {
29+
envArgs.push('--env', `CAVEAT_ETH_LIMIT=${process.env.CAVEAT_ETH_LIMIT}`);
30+
}
31+
32+
const exec = spawnSync(
33+
'docker',
34+
[
35+
...dockerArgs,
36+
'exec',
37+
...envArgs,
38+
home,
39+
'node',
40+
'--conditions',
41+
'development',
42+
SCRIPT_IN_CONTAINER,
43+
],
44+
{ cwd: DEMO_PACKAGE_ROOT, stdio: 'inherit', env: process.env },
45+
);
46+
process.exit(exec.status ?? 1);
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/* eslint-disable n/no-sync, n/no-process-env, n/no-process-exit, jsdoc/require-jsdoc */
2+
/**
3+
* Shared parsing + `docker compose` argv for demo stack (one home/away pair).
4+
* Keep delegation-mode keys in sync with `test/e2e/docker/helpers/docker-e2e-kernel-services.ts`.
5+
*/
6+
import { spawnSync } from 'node:child_process';
7+
import { dirname, join } from 'node:path';
8+
import { fileURLToPath } from 'node:url';
9+
10+
const dockerLibDir = dirname(fileURLToPath(import.meta.url));
11+
export const DEMO_PACKAGE_ROOT = join(dockerLibDir, '..');
12+
13+
const COMPOSE_FILE = join(DEMO_PACKAGE_ROOT, 'docker/docker-compose.yml');
14+
const ENV_FILE = join(DEMO_PACKAGE_ROOT, 'docker/.env.demo');
15+
16+
/** @type {Record<string, string>} */
17+
export const DEMO_PAIR_TO_PROFILE = {
18+
'bundler-7702': '7702',
19+
'bundler-hybrid': '4337',
20+
'peer-relay': 'relay',
21+
};
22+
23+
export const DEFAULT_DEMO_PAIR = 'bundler-7702';
24+
25+
export function awayServiceForDemoPair(pair) {
26+
if (pair === 'bundler-7702') {
27+
return 'kernel-away-bundler-7702';
28+
}
29+
if (pair === 'bundler-hybrid') {
30+
return 'kernel-away-bundler-hybrid';
31+
}
32+
if (pair === 'peer-relay') {
33+
return 'kernel-away-peer-relay';
34+
}
35+
throw new Error(`Unknown demo pair: ${pair}`);
36+
}
37+
38+
export function homeServiceForDemoPair(pair) {
39+
if (pair === 'bundler-7702') {
40+
return 'kernel-home-bundler-7702';
41+
}
42+
if (pair === 'bundler-hybrid') {
43+
return 'kernel-home-bundler-hybrid';
44+
}
45+
if (pair === 'peer-relay') {
46+
return 'kernel-home-peer-relay';
47+
}
48+
throw new Error(`Unknown demo pair: ${pair}`);
49+
}
50+
51+
export function parseDemoComposeArgv(argv) {
52+
let pair = process.env.OCAP_DEMO_PAIR ?? DEFAULT_DEMO_PAIR;
53+
const rest = [...argv];
54+
const i = rest.indexOf('--pair');
55+
if (i !== -1 && rest[i + 1]) {
56+
pair = rest[i + 1];
57+
rest.splice(i, 2);
58+
}
59+
const profile = DEMO_PAIR_TO_PROFILE[pair];
60+
if (!profile) {
61+
console.error(
62+
`Unknown pair "${pair}". Use: ${Object.keys(DEMO_PAIR_TO_PROFILE).join(', ')} (env OCAP_DEMO_PAIR, or --pair before compose subcommands).`,
63+
);
64+
process.exit(1);
65+
}
66+
return { pair, profile, rest };
67+
}
68+
69+
export function demoDockerComposeArgs(argv) {
70+
const { pair, profile, rest } = parseDemoComposeArgv(argv);
71+
return {
72+
pair,
73+
profile,
74+
rest,
75+
dockerArgs: [
76+
'compose',
77+
'-f',
78+
COMPOSE_FILE,
79+
'--env-file',
80+
ENV_FILE,
81+
'--profile',
82+
profile,
83+
...rest,
84+
],
85+
};
86+
}
87+
88+
export function runDemoCompose(argv) {
89+
const { pair, profile, dockerArgs } = demoDockerComposeArgs(argv);
90+
if (process.env.DEBUG_OCAP_DEMO_COMPOSE) {
91+
console.error(
92+
`[ocap demo compose] OCAP_DEMO_PAIR=${pair} profile=${profile}`,
93+
);
94+
}
95+
const spawned = spawnSync('docker', dockerArgs, {
96+
cwd: DEMO_PACKAGE_ROOT,
97+
stdio: 'inherit',
98+
env: process.env,
99+
});
100+
if (spawned.error) {
101+
throw spawned.error;
102+
}
103+
process.exit(spawned.status ?? 1);
104+
}

0 commit comments

Comments
 (0)