Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .claude/skills/evm-wallet-docker-e2e/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
name: evm-wallet-docker-e2e
description: Run the evm-wallet Docker e2e tests (build, start stack, wait for healthy, test, diagnose failures).
---

Run all commands from the repo root unless noted.

## 1. Verify Docker is running

```bash
docker info 2>&1 | head -5
```

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.

## 2. Build the repo and Docker images

```bash
yarn workspace @ocap/evm-wallet-experiment docker:build 2>&1 | tail -30
```

This builds the full monorepo then builds the Docker images. It may take a few minutes. Report any errors from the tail output.

## 3. Tear down any existing stack, then start fresh

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).

```bash
yarn workspace @ocap/evm-wallet-experiment docker:down 2>&1 | tail -10
```

Then start the stack:

```bash
yarn workspace @ocap/evm-wallet-experiment docker:ensure-logs && \
yarn workspace @ocap/evm-wallet-experiment docker:compose up -d 2>&1 | tail -20
```

## 4. Wait for all services to be healthy

Poll every 10 seconds (up to 3 minutes / 18 attempts). All 8 services must reach `(healthy)` status before proceeding:

- `evm`, `bundler`
- `kernel-home-bundler-7702`, `kernel-away-bundler-7702`
- `kernel-home-bundler-hybrid`, `kernel-away-bundler-hybrid`
- `kernel-home-peer-relay`, `kernel-away-peer-relay`

```bash
i=0; while [ $i -lt 18 ]; do
i=$((i+1))
ps_out=$(yarn workspace @ocap/evm-wallet-experiment docker:ps 2>&1)
healthy=$(echo "$ps_out" | grep -c "(healthy)" || true)
echo "Attempt $i/18: $healthy/8 healthy"
if [ "$healthy" -ge 8 ]; then echo "Stack ready."; break; fi
if [ "$i" -eq 18 ]; then echo "Timed out:"; echo "$ps_out"; exit 1; fi
sleep 10
done
```

If the loop exits with a timeout, show the last `docker:ps` output and stop — do not proceed to the tests.

## 5. Run the e2e tests

```bash
yarn workspace @ocap/evm-wallet-experiment test:e2e:docker 2>&1 | tail -80
```

The vitest reporter also writes structured results to `packages/evm-wallet-experiment/logs/test-results.json`.

## 6. Diagnose failures

If tests fail, investigate in this order:

### Structured test results

```bash
cat packages/evm-wallet-experiment/logs/test-results.json
```

Look at the `testResults` array for failed tests and their error messages.

### Service logs

Container logs are written to `packages/evm-wallet-experiment/logs/`. Check the service(s) relevant to the failing test mode first:

```bash
tail -150 packages/evm-wallet-experiment/logs/<service>.log
```

Service log files:

- `evm.log` — Anvil chain (check for on-chain errors)
- `kernel-home-bundler-7702.log`, `kernel-away-bundler-7702.log`
- `kernel-home-bundler-hybrid.log`, `kernel-away-bundler-hybrid.log`
- `kernel-home-peer-relay.log`, `kernel-away-peer-relay.log`

Start with the pair(s) involved in the failing test, then `evm.log` for on-chain issues.
6 changes: 3 additions & 3 deletions packages/evm-wallet-experiment/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @ocap/evm-wallet-experiment

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.
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.

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).

Expand Down Expand Up @@ -712,7 +712,7 @@ yarn workspace @ocap/evm-wallet-experiment build
yarn workspace @ocap/evm-wallet-experiment lint:fix
```

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.
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.

## Testing

Expand Down Expand Up @@ -803,7 +803,7 @@ DELEGATION_MODE=bundler-7702 yarn workspace @ocap/evm-wallet-experiment test:e2e
yarn workspace @ocap/evm-wallet-experiment docker:down
```

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).
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).

## Supported Chains

Expand Down
3 changes: 3 additions & 0 deletions packages/evm-wallet-experiment/docker/.env.demo
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Used by `yarn docker:demo:up` (--env-file). Switches
# `kernel-away-bundler-7702` to the `demo` Dockerfile target (OpenClaw).
KERNEL_AWAY_7702_TARGET=demo
3 changes: 0 additions & 3 deletions packages/evm-wallet-experiment/docker/.env.interactive

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ COPY --from=builder /build /app
RUN mkdir -p /logs /run/ocap

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

# OpenClaw loads local plugins as TypeScript via jiti (no extra TS runner in the image).
# `package.json` docker:interactive:setup starts the gateway via
# `package.json` docker:demo:setup starts the gateway via
# `node /usr/local/lib/node_modules/openclaw/openclaw.mjs` (global bin PATH is unreliable under `docker exec`).
RUN npm install -g openclaw@2026.4.1
18 changes: 9 additions & 9 deletions packages/evm-wallet-experiment/docker/MAINTAINERS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Docker stack — maintainer notes

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.
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.

## Startup order

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

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

### OpenClaw (interactive image only)
### OpenClaw (demo image only)

`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.

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

- **`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.
- 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.
- 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.
- 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.

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

- **`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`**).
- **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`**.
- **`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.
- 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:`**).
- **`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`**).
- **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`**.
- **`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.
- 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:`**).

**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.
**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.

## `yarn docker:delegate`

Expand Down
31 changes: 31 additions & 0 deletions packages/evm-wallet-experiment/docker/attach.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* eslint-disable n/no-sync, n/no-process-env, n/no-process-exit */
import { spawnSync } from 'node:child_process';

import {
awayServiceForDemoPair,
homeServiceForDemoPair,
demoDockerComposeArgs,
DEMO_PACKAGE_ROOT,
} from './demo-compose-lib.mjs';

const [side, ...rest] = process.argv.slice(2);

if (side !== 'away' && side !== 'home') {
console.error(`Usage: attach.mjs <away|home> [--pair <pair>]`);
process.exit(1);
}

const { pair, dockerArgs } = demoDockerComposeArgs(rest);
const service =
side === 'away' ? awayServiceForDemoPair(pair) : homeServiceForDemoPair(pair);

const spawned = spawnSync(
'docker',
[...dockerArgs, 'exec', '-it', service, 'bash'],
{
cwd: DEMO_PACKAGE_ROOT,
stdio: 'inherit',
env: process.env,
},
);
process.exit(spawned.status ?? 1);
46 changes: 46 additions & 0 deletions packages/evm-wallet-experiment/docker/delegate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* eslint-disable n/no-sync, n/no-process-env, n/no-process-exit */
import { spawnSync } from 'node:child_process';

import {
homeServiceForDemoPair,
demoDockerComposeArgs,
DEMO_PACKAGE_ROOT,
} from './demo-compose-lib.mjs';

const SCRIPT_ON_HOST = 'docker/create-delegation.mjs';
const SCRIPT_IN_CONTAINER =
'/app/packages/evm-wallet-experiment/docker/create-delegation.mjs';

const argv = process.argv.slice(2);
const { pair, dockerArgs } = demoDockerComposeArgs(argv);
const home = homeServiceForDemoPair(pair);

const cp = spawnSync(
'docker',
[...dockerArgs, 'cp', SCRIPT_ON_HOST, `${home}:${SCRIPT_IN_CONTAINER}`],
{ cwd: DEMO_PACKAGE_ROOT, stdio: 'inherit', env: process.env },
);
if (cp.status !== 0) {
process.exit(cp.status ?? 1);
}

const envArgs = ['--env', `DELEGATION_MODE=${pair}`];
if (process.env.CAVEAT_ETH_LIMIT) {
envArgs.push('--env', `CAVEAT_ETH_LIMIT=${process.env.CAVEAT_ETH_LIMIT}`);
}

const exec = spawnSync(
'docker',
[
...dockerArgs,
'exec',
...envArgs,
home,
'node',
'--conditions',
'development',
SCRIPT_IN_CONTAINER,
],
{ cwd: DEMO_PACKAGE_ROOT, stdio: 'inherit', env: process.env },
);
process.exit(exec.status ?? 1);
104 changes: 104 additions & 0 deletions packages/evm-wallet-experiment/docker/demo-compose-lib.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* eslint-disable n/no-sync, n/no-process-env, n/no-process-exit, jsdoc/require-jsdoc */
/**
* Shared parsing + `docker compose` argv for demo stack (one home/away pair).
* Keep delegation-mode keys in sync with `test/e2e/docker/helpers/docker-e2e-kernel-services.ts`.
*/
import { spawnSync } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const dockerLibDir = dirname(fileURLToPath(import.meta.url));
export const DEMO_PACKAGE_ROOT = join(dockerLibDir, '..');

const COMPOSE_FILE = join(DEMO_PACKAGE_ROOT, 'docker/docker-compose.yml');
const ENV_FILE = join(DEMO_PACKAGE_ROOT, 'docker/.env.demo');

/** @type {Record<string, string>} */
export const DEMO_PAIR_TO_PROFILE = {
'bundler-7702': '7702',
'bundler-hybrid': '4337',
'peer-relay': 'relay',
};

export const DEFAULT_DEMO_PAIR = 'bundler-7702';

export function awayServiceForDemoPair(pair) {
if (pair === 'bundler-7702') {
return 'kernel-away-bundler-7702';
}
if (pair === 'bundler-hybrid') {
return 'kernel-away-bundler-hybrid';
}
if (pair === 'peer-relay') {
return 'kernel-away-peer-relay';
}
throw new Error(`Unknown demo pair: ${pair}`);
}

export function homeServiceForDemoPair(pair) {
if (pair === 'bundler-7702') {
return 'kernel-home-bundler-7702';
}
if (pair === 'bundler-hybrid') {
return 'kernel-home-bundler-hybrid';
}
if (pair === 'peer-relay') {
return 'kernel-home-peer-relay';
}
throw new Error(`Unknown demo pair: ${pair}`);
}

export function parseDemoComposeArgv(argv) {
let pair = process.env.OCAP_DEMO_PAIR ?? DEFAULT_DEMO_PAIR;
const rest = [...argv];
const i = rest.indexOf('--pair');
if (i !== -1 && rest[i + 1]) {
pair = rest[i + 1];
rest.splice(i, 2);
}
const profile = DEMO_PAIR_TO_PROFILE[pair];
if (!profile) {
console.error(
`Unknown pair "${pair}". Use: ${Object.keys(DEMO_PAIR_TO_PROFILE).join(', ')} (env OCAP_DEMO_PAIR, or --pair before compose subcommands).`,
);
process.exit(1);
}
return { pair, profile, rest };
}

export function demoDockerComposeArgs(argv) {
const { pair, profile, rest } = parseDemoComposeArgv(argv);
return {
pair,
profile,
rest,
dockerArgs: [
'compose',
'-f',
COMPOSE_FILE,
'--env-file',
ENV_FILE,
'--profile',
profile,
...rest,
],
};
}

export function runDemoCompose(argv) {
const { pair, profile, dockerArgs } = demoDockerComposeArgs(argv);
if (process.env.DEBUG_OCAP_DEMO_COMPOSE) {
console.error(
`[ocap demo compose] OCAP_DEMO_PAIR=${pair} profile=${profile}`,
);
}
const spawned = spawnSync('docker', dockerArgs, {
cwd: DEMO_PACKAGE_ROOT,
stdio: 'inherit',
env: process.env,
});
if (spawned.error) {
throw spawned.error;
}
process.exit(spawned.status ?? 1);
}
Loading
Loading