Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
3769228
refactor(evm-wallet): replace DelegationGrant with discriminated unio…
grypez Apr 15, 2026
b095d11
feat(evm-wallet): add delegator-vat for home-side grant building
grypez Apr 15, 2026
a6c3be3
feat(evm-wallet): add redeemer-vat for away-side grant storage
grypez Apr 15, 2026
0794527
feat(evm-wallet): add home-coordinator as home-side split of coordina…
grypez Apr 15, 2026
cf65949
feat(evm-wallet): rewrite delegation-twin for semantic grant enforcement
grypez Apr 19, 2026
2fbbc78
feat(evm-wallet): add away-coordinator with delegation routing and pe…
grypez Apr 19, 2026
2239220
refactor(evm-wallet): remove coordinator-vat, delegation-vat, delegat…
grypez Apr 15, 2026
5485331
test(evm-wallet-experiment): migrate Docker e2e tests to new coordina…
grypez Apr 20, 2026
6c08023
fix(evm-wallet): normalize token addresses and preserve twin spend co…
grypez Apr 20, 2026
ea5a069
refactor(evm-wallet): use superstruct to validate CapData shape in do…
grypez Apr 21, 2026
236baaa
refactor(evm-wallet): use superstruct to validate CapData in openclaw…
grypez Apr 21, 2026
109851c
refactor(evm-wallet): extract shared tx helpers to lib/tx-utils
grypez Apr 21, 2026
91c79d1
fix(evm-wallet): normalize token to lowercase before routing to deleg…
grypez Apr 21, 2026
5bde8e9
feat(evm-wallet-experiment): add docker:attach:away and docker:attach…
grypez Apr 17, 2026
5762ad1
refactor(evm-wallet-experiment): rename docker:interactive to docker:…
grypez Apr 17, 2026
41a4dee
Add claude skill for evm-wallet e2e testing
grypez Apr 21, 2026
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
89 changes: 89 additions & 0 deletions .claude/skills/evm-wallet-docker-e2e/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
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. Start the stack in detached mode

```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.
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,6 @@ RUN mkdir -p /logs /run/ocap
FROM kernel AS interactive

# 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
10 changes: 5 additions & 5 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 @@ -66,10 +66,10 @@ Host-side scripts (e.g. `yarn docker:setup:wallets`) use the workspace **`tsx`**

## Interactive stack (`docker/.env.interactive` + 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-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: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.

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

import {
awayServiceForInteractivePair,
homeServiceForInteractivePair,
interactiveDockerComposeArgs,
INTERACTIVE_PACKAGE_ROOT,
} from './interactive-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 } = interactiveDockerComposeArgs(rest);
const service =
side === 'away'
? awayServiceForInteractivePair(pair)
: homeServiceForInteractivePair(pair);

const spawned = spawnSync(
'docker',
[...dockerArgs, 'exec', '-it', service, 'bash'],
{
cwd: INTERACTIVE_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 {
homeServiceForInteractivePair,
interactiveDockerComposeArgs,
INTERACTIVE_PACKAGE_ROOT,
} from './interactive-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 } = interactiveDockerComposeArgs(argv);
const home = homeServiceForInteractivePair(pair);

const cp = spawnSync(
'docker',
[...dockerArgs, 'cp', SCRIPT_ON_HOST, `${home}:${SCRIPT_IN_CONTAINER}`],
{ cwd: INTERACTIVE_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: INTERACTIVE_PACKAGE_ROOT, stdio: 'inherit', env: process.env },
);
process.exit(exec.status ?? 1);
2 changes: 1 addition & 1 deletion packages/evm-wallet-experiment/docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Extension fields (ignored as services; hold YAML anchors for kernel DRY).
# Three pairs: bundler-7702 (4011/4012), bundler-hybrid (4021/4022), peer-relay (4031/4032).
# Each pair is behind a Compose profile so `yarn docker:interactive:up` can start one pair;
# Each pair is behind a Compose profile so `yarn docker:demo:up` can start one pair;
# E2E / `yarn docker:up` pass all three `--profile` flags: 7702, 4337, relay (see package.json `docker:compose`).
x-kernel-build-core: &kernel-build-core
context: ../../../
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ export function awayServiceForInteractivePair(pair) {
throw new Error(`Unknown interactive pair: ${pair}`);
}

export function homeServiceForInteractivePair(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 interactive pair: ${pair}`);
}

export function parseInteractiveComposeArgv(argv) {
let pair = process.env.OCAP_INTERACTIVE_PAIR ?? DEFAULT_INTERACTIVE_PAIR;
const rest = [...argv];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ if (spawned.status !== 0) {
}

console.log(
`OpenClaw state removed at ${openclawDir} on ocap-run volume. Run: yarn docker:interactive:setup`,
`OpenClaw state removed at ${openclawDir} on ocap-run volume. Run: yarn docker:demo:setup`,
);
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,5 @@ if (gateway.status !== 0) {
}

console.log(
`OpenClaw configured + gateway started (${away}). Shell: OCAP_INTERACTIVE_PAIR=${pair} yarn docker:compose:interactive -- exec ${away} bash`,
`OpenClaw configured + gateway started (${away}). Shell: OCAP_INTERACTIVE_PAIR=${pair} yarn docker:demo:attach:away`,
);
8 changes: 4 additions & 4 deletions packages/evm-wallet-experiment/docs/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The away kernel for `bundler-7702` has two Dockerfile targets:
| `kernel` | E2E tests (default) | Kernel daemon only |
| `interactive` | Interactive mode | Kernel daemon + OpenClaw CLI |

The `interactive` target is activated automatically by `docker:interactive:up` via `docker/.env.interactive`.
The `interactive` target is activated automatically by `docker:demo:up` via `docker/.env.interactive`.

---

Expand Down Expand Up @@ -71,7 +71,7 @@ Interactive mode activates one profile at a time. E2E test mode (`docker:up`) ac
| `ocap-run` (named volume) | `/run/ocap` | Kernel databases, daemon sockets, contract addresses, delegation context, OpenClaw state |
| `packages/evm-wallet-experiment/logs/` (bind-mount) | `/logs` | Per-service log files (`<service-name>.log`); persists across restarts and readable directly on the host |

The `logs/` directory is created automatically by `docker:up` and `docker:interactive:up` via the `docker:ensure-logs` script. Each container's entrypoint tees its stdout/stderr to `/logs/<service-name>.log`.
The `logs/` directory is created automatically by `docker:up` and `docker:demo:up` via the `docker:ensure-logs` script. Each container's entrypoint tees its stdout/stderr to `/logs/<service-name>.log`.

---

Expand Down Expand Up @@ -132,7 +132,7 @@ cat packages/evm-wallet-experiment/logs/kernel-home-bundler-7702.log
cat packages/evm-wallet-experiment/logs/test-results.json

# Check container health
docker compose -f packages/evm-wallet-experiment/docker/docker-compose.yml --profile 7702 ps
yarn workspace @ocap/evm-wallet-experiment docker:ps
```

Kernel containers write a readiness JSON file to `/run/ocap/<service>-ready.json` when the daemon is up. The host-side setup scripts poll this before proceeding.
Expand Down Expand Up @@ -160,5 +160,5 @@ After `test:e2e:docker` completes, structured pass/fail results are written to `

**Volume state is corrupted or stale**

- Full wipe: `docker compose -f packages/evm-wallet-experiment/docker/docker-compose.yml down -v`
- Full wipe: `yarn workspace @ocap/evm-wallet-experiment docker:down:volumes`
- Rebuild: `yarn workspace @ocap/evm-wallet-experiment docker:build:force`
Loading
Loading