Skip to content

Commit 7ff75b3

Browse files
committed
feat(evm-wallet-experiment): OpenClaw LLM via Docker Model Runner in Compose
- Declare top-level Compose models (ai/qwen3.5:4B-UD-Q4_K_XL) and bind to kernel-away-bundler-7702; remove in-stack llm proxy + entrypoint-llm-proxy.mjs. - setup-openclaw requires DMR-injected LLM_URL/LLM_MODEL, openai-completions, /v1 base normalization; trim docker-e2e-stack-constants llm defaults. - package.json: interactive compose without empty profile, openclaw gateway via node path, up --build, docker:interactive:reset-openclaw. - context_size + runtime_flags --ctx-size 32768; MAINTAINERS troubleshooting (configure, volume persistence). Made-with: Cursor
1 parent 78a4415 commit 7ff75b3

9 files changed

Lines changed: 83 additions & 112 deletions

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,6 @@ RUN mkdir -p /logs /run/ocap
6767
FROM kernel AS interactive
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
71+
# `node /usr/local/lib/node_modules/openclaw/openclaw.mjs` (global bin PATH is unreliable under `docker exec`).
7072
RUN npm install -g openclaw@2026.4.1

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

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

3-
Local E2E stack for `@ocap/evm-wallet-experiment`: Anvil + deployed contracts, Pimlico Alto, and two kernel containers (`home`, `away`). See `package.json` scripts (`docker:compose`, `test:e2e:docker`, etc.).
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.).
44

55
## Startup order
66

77
Compose encodes this dependency chain:
88

99
1. **`evm`** becomes healthy when `/run/ocap/contracts.json` exists (written only after `deploy-contracts.mjs` finishes).
1010
2. **`bundler`** waits on that file, reads `EntryPoint`, then starts Alto.
11-
3. **`home` and `away`** wait on **both** `evm` and **`bundler` healthy** so wallet setup does not race Alto boot.
11+
3. **Kernel services** wait on **both** `evm` and **`bundler` healthy** so wallet setup does not race Alto boot.
1212

1313
If you add a service that kernels need before they are ready, extend `depends_on` and healthchecks accordingly.
1414

@@ -48,7 +48,6 @@ Host-side scripts (e.g. `yarn docker:setup:wallets`) use the workspace **`tsx`**
4848

4949
- **`evm`**: File-based (`contracts.json`). The image itself does not define `HEALTHCHECK`; Compose is the source of truth.
5050
- **`bundler`**: JSON-RPC `eth_supportedEntryPoints` must return a **non-empty** array. If Alto changes RPC surface, adjust the probe in `docker-compose.yml`.
51-
- **`llm`**: HTTP GET `/` on the proxy; **5xx** (e.g. upstream unreachable) marks the service unhealthy.
5251

5352
## Kernel image build (`Dockerfile.kernel-base`)
5453

@@ -59,11 +58,18 @@ Host-side scripts (e.g. `yarn docker:setup:wallets`) use the workspace **`tsx`**
5958

6059
`docker-compose.yml` embeds **well-known Anvil private keys** for Alto. That is intentional for an isolated local chain. **Do not reuse this pattern** for any network that is exposed or shared.
6160

62-
## Interactive profile
61+
## Interactive stack (`docker-compose.interactive.yml`)
6362

64-
- **`llm`** defaults `LLM_UPSTREAM` to `http://host.docker.internal:8080`. On **Linux**, `host.docker.internal` may be missing unless you add `extra_hosts` or another reachability strategy; document any project-standard workaround here when you add one.
65-
- **`docker-compose.interactive.yml`** overrides `away` (OpenClaw + LLM). Ensure the **`interactive`** profile is used when you expect those services.
63+
- Requires **Docker Compose v2.38+** and [**Docker Model Runner**](https://docs.docker.com/ai/model-runner/) enabled.
64+
- Merges over **`kernel-away-bundler-7702`**: **`interactive`** image target (OpenClaw) and Compose [**`models`**](https://docs.docker.com/ai/compose/models-and-compose/) binding **`llm`****`ai/qwen3.5:4B-UD-Q4_K_XL`** with **`context_size: 32768`** (DMR’s default 4096 is too small for OpenClaw + tool prompts). Larger context uses more RAM.
65+
- Run **`yarn docker:compose:interactive`**. Pull the model first if needed: **`docker model pull ai/qwen3.5:4B-UD-Q4_K_XL`**.
66+
- **`compose.interactive.yml`** sets **`context_size`** and **`runtime_flags: ['--ctx-size','32768']`** so llama.cpp does not stay at the 4096 default. If requests still fail with 4096, run on the host: **`docker model configure --context-size 32768 ai/qwen3.5:4B-UD-Q4_K_XL`**, then recreate the stack.
67+
- OpenClaw UI history lives under **`$HOME/.openclaw`** in the away container (on the **`ocap-run`** named volume). Rebuilding images does **not** clear it. Use **`yarn docker:interactive:reset-openclaw`**, then **`yarn docker:interactive:setup`**, or **`docker compose … down -v`** (wipes **all** kernel/contract state on that volume — use with care).
68+
69+
## Optional **`ollama`** profile
70+
71+
- Service **`ollama`** uses profile **`ollama`** for an in-stack Ollama server. Interactive OpenClaw uses **DMR via Compose `models`**, not this service, unless you change the stack.
6672

6773
## Ports and conflicts
6874

69-
Published ports include **8545**, **4337**, **11434** (profile), and **UDP 4001/4002**. They can clash with other stacks on the host; use Compose [profiles](https://docs.docker.com/compose/profiles/) or alternate port mappings if needed.
75+
Published TCP ports include **8545** (Anvil), **4337** (bundler). Kernels publish **UDP 4011–4032** (QUIC, three pairs). The **`ollama`** profile does not publish **11434** to the host by default in the current compose file; check `docker-compose.yml` if that changes. Use alternate mappings if these clash with other stacks.
Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
1-
# Compose override for interactive use (OpenClaw + LLM).
1+
# Compose override for interactive use (OpenClaw + Docker Model Runner).
2+
#
3+
# Requires Docker Compose v2.38+ and Docker Model Runner enabled.
4+
# Model: https://hub.docker.com/r/ai/qwen3.5/tags — tag 4B-UD-Q4_K_XL (digest prefix 6dfe4f74414).
25
#
36
# Usage:
47
# yarn docker:interactive:up
8+
# (`up --build` so `kernel-away-bundler-7702` uses the `interactive` image with OpenClaw.)
59
#
610
# Then run wallet + OpenClaw setup:
711
# yarn docker:interactive:setup
12+
#
13+
# DMR injects `LLM_URL` and `LLM_MODEL` into `kernel-away-bundler-7702` (short `models: - llm` syntax).
14+
# OpenClaw is configured for the OpenAI-compatible `/v1` API (`openai-completions`).
15+
#
16+
# OpenClaw and shell access use compose service `kernel-away-bundler-7702` (not `away`).
17+
#
18+
# If you still see "4096 tokens" from the runner, `context_size` in Compose may be ignored
19+
# (known DMR/llama.cpp edge cases). Use `runtime_flags` below and/or on the host:
20+
# docker model configure --context-size 32768 ai/qwen3.5:4B-UD-Q4_K_XL
21+
#
22+
# OpenClaw chat/session data lives on volume `ocap-run` under this service's HOME
23+
# (`.../kernel-away-bundler-7702/.openclaw`), not in the image — `docker build` does not
24+
# clear it. Run `yarn docker:interactive:reset-openclaw` (or `docker compose down -v`) to wipe.
825
services:
926
kernel-away-bundler-7702:
1027
build:
1128
target: interactive
12-
environment:
13-
- LLM_BASE_URL=${LLM_BASE_URL:-http://llm:11434/v1}
14-
- LLM_MODEL=${LLM_MODEL:-glm-4.7-flash}
15-
- LLM_API_TYPE=${LLM_API_TYPE:-openai-completions}
16-
depends_on:
17-
llm:
18-
condition: service_healthy
29+
models:
30+
- llm
31+
32+
models:
33+
llm:
34+
model: ai/qwen3.5:4B-UD-Q4_K_XL
35+
# DMR llama.cpp defaults to 4096; OpenClaw + wallet tools exceed that.
36+
context_size: 32768
37+
# Explicit llama.cpp ctx (Compose `context_size` alone is sometimes not applied).
38+
runtime_flags:
39+
- '--ctx-size'
40+
- '32768'

packages/evm-wallet-experiment/docker/docker-compose.yml

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,30 +51,6 @@ services:
5151
timeout: 5s
5252
retries: 20
5353

54-
llm:
55-
profiles: [interactive]
56-
build:
57-
context: ../../../
58-
dockerfile: packages/evm-wallet-experiment/docker/Dockerfile.kernel-base
59-
target: kernel
60-
environment:
61-
- LLM_UPSTREAM=${LLM_UPSTREAM:-http://host.docker.internal:8080}
62-
networks: [ocap-e2e]
63-
ports: ['11434:11434']
64-
entrypoint:
65-
- node
66-
- /app/packages/evm-wallet-experiment/docker/entrypoint-llm-proxy.mjs
67-
healthcheck:
68-
test:
69-
- CMD
70-
- node
71-
- -e
72-
- >-
73-
fetch("http://127.0.0.1:11434/").then((r)=>process.exit(r.status<500?0:1)).catch(()=>process.exit(1))
74-
interval: 3s
75-
timeout: 5s
76-
retries: 10
77-
7854
ollama:
7955
profiles: [ollama]
8056
image: ollama/ollama
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
11
{
2-
"llm": {
3-
"upstreamOllama": "http://ollama:11434",
4-
"baseUrl": "http://llm:11434",
5-
"model": "qwen2.5:0.5b",
6-
"apiType": "ollama"
7-
},
82
"anvilChainId": 31337,
93
"openclawPluginPathContainer": "/app/packages/evm-wallet-experiment/openclaw-plugin"
104
}

packages/evm-wallet-experiment/docker/docker-e2e-stack-constants.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ const jsonPath = join(dir, 'docker-e2e-stack-constants.json');
1313

1414
/**
1515
* @typedef {object} DockerStackConfig
16-
* @property {{ upstreamOllama: string, baseUrl: string, model: string, apiType: string }} llm - Defaults for LLM proxy and OpenClaw provider.
1716
* @property {number} anvilChainId - Chain ID for the local Anvil stack in Docker E2E.
1817
* @property {string} openclawPluginPathContainer - Wallet plugin path inside the kernel container.
1918
*/

packages/evm-wallet-experiment/docker/entrypoint-llm-proxy.mjs

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

packages/evm-wallet-experiment/docker/setup-openclaw.mjs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
* This replaces the fragile sequence of `openclaw onboard` +
88
* `openclaw config set` calls that broke on every OpenClaw release.
99
*
10-
* Env vars (all optional, with Docker-friendly defaults):
11-
* LLM_BASE_URL — LLM provider base URL (default: http://llm:11434)
12-
* LLM_MODEL — Model ID (default: qwen2.5:0.5b)
13-
* LLM_API_TYPE — OpenClaw API type: ollama | openai-completions | … (default: ollama)
10+
* Expects Docker Compose **models** + Docker Model Runner to inject:
11+
* LLM_URL — OpenAI-compatible API base (normalized to end with `/v1` below)
12+
* LLM_MODEL — Model id for requests
13+
*
14+
* @see https://docs.docker.com/ai/compose/models-and-compose/
1415
*
1516
* Usage:
1617
* node /app/packages/evm-wallet-experiment/docker/setup-openclaw.mjs
@@ -25,9 +26,33 @@ import { dockerConfig } from './docker-e2e-stack-constants.mjs';
2526
const home = process.env.HOME || '/run/ocap/away';
2627
const ocDir = resolve(home, '.openclaw');
2728

28-
const llmBaseUrl = process.env.LLM_BASE_URL || dockerConfig.llm.baseUrl;
29-
const llmModel = process.env.LLM_MODEL || dockerConfig.llm.model;
30-
const llmApiType = process.env.LLM_API_TYPE || dockerConfig.llm.apiType;
29+
const llmUrlRaw = process.env.LLM_URL;
30+
const llmModel = process.env.LLM_MODEL;
31+
32+
if (!llmUrlRaw || !llmModel) {
33+
throw new Error(
34+
'openclaw-setup: LLM_URL and LLM_MODEL must be set. ' +
35+
'Use docker-compose.interactive.yml with top-level `models:` and Docker Model Runner enabled.',
36+
);
37+
}
38+
39+
/**
40+
* OpenClaw `openai-completions` expects a base URL whose paths resolve under `/v1/...`.
41+
*
42+
* @param {string} url - Injected model runner URL (may omit `/v1`).
43+
* @returns {string} Normalized base URL ending with `/v1` (no trailing slash after v1).
44+
*/
45+
function openAiCompletionsBaseUrl(url) {
46+
const trimmed = url.replace(/\/+$/u, '');
47+
if (trimmed.endsWith('/v1')) {
48+
return trimmed;
49+
}
50+
return `${trimmed}/v1`;
51+
}
52+
53+
const llmBaseUrl = openAiCompletionsBaseUrl(llmUrlRaw);
54+
/** DMR serves an OpenAI-shaped API alongside Ollama compatibility. */
55+
const llmApiType = 'openai-completions';
3156
const providerId = 'llm';
3257
const pluginPath = dockerConfig.openclawPluginPathContainer;
3358

@@ -109,7 +134,7 @@ const config = {
109134
const cfgPath = resolve(ocDir, 'openclaw.json');
110135
writeFileSync(cfgPath, JSON.stringify(config, null, 2));
111136
console.log(
112-
`[openclaw-setup] config written: ${providerId}/${llmModel} (api: ${llmApiType})`,
137+
`[openclaw-setup] config written: ${providerId}/${llmModel} (api: ${llmApiType}, base: ${llmBaseUrl})`,
113138
);
114139

115140
// -- Write auth profiles for the provider --

packages/evm-wallet-experiment/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,17 @@
5757
"test:node:peer-e2e": "yarn build && node --conditions development test/e2e/run-peer-e2e.mjs",
5858
"test:node:spending-limits": "yarn build && node --conditions development test/e2e/run-spending-limits-e2e.mjs",
5959
"docker:compose": "docker compose -f docker/docker-compose.yml",
60-
"docker:compose:interactive": "docker compose -f docker/docker-compose.yml -f docker/docker-compose.interactive.yml --profile interactive",
60+
"docker:compose:interactive": "docker compose -f docker/docker-compose.yml -f docker/docker-compose.interactive.yml",
6161
"docker:build": "yarn docker:compose build",
6262
"docker:build:force": "yarn docker:compose build --no-cache",
6363
"docker:up": "yarn docker:compose up",
6464
"docker:down": "yarn docker:compose down",
65-
"docker:interactive:up": "yarn docker:compose:interactive up",
65+
"docker:interactive:up": "yarn docker:compose:interactive up --build",
6666
"docker:interactive:down": "yarn docker:compose:interactive down",
6767
"docker:setup:wallets": "yarn tsx test/e2e/docker/setup-wallets.ts",
68-
"docker:interactive:setup": "yarn docker:setup:wallets && yarn docker:compose:interactive exec away node /app/packages/evm-wallet-experiment/docker/setup-openclaw.mjs && yarn docker:compose:interactive exec -d away openclaw gateway && echo 'OpenClaw configured + gateway started. Shell in with: yarn docker:compose:interactive exec away bash'",
69-
"docker:delegate": "yarn docker:compose cp docker/create-delegation.mjs home:/app/packages/evm-wallet-experiment/docker/create-delegation.mjs && yarn docker:compose exec home node --conditions development /app/packages/evm-wallet-experiment/docker/create-delegation.mjs",
68+
"docker:interactive:setup": "yarn docker:setup:wallets && yarn docker:compose:interactive exec kernel-away-bundler-7702 node /app/packages/evm-wallet-experiment/docker/setup-openclaw.mjs && yarn docker:compose:interactive exec -d kernel-away-bundler-7702 node /usr/local/lib/node_modules/openclaw/openclaw.mjs gateway && echo 'OpenClaw configured + gateway started. Shell in with: yarn docker:compose:interactive exec kernel-away-bundler-7702 bash'",
69+
"docker:interactive:reset-openclaw": "yarn docker:compose:interactive exec kernel-away-bundler-7702 rm -rf /run/ocap/kernel-away-bundler-7702/.openclaw && echo 'OpenClaw state removed from ocap-run volume. Run: yarn docker:interactive:setup'",
70+
"docker:delegate": "yarn docker:compose cp docker/create-delegation.mjs kernel-home-bundler-7702:/app/packages/evm-wallet-experiment/docker/create-delegation.mjs && yarn docker:compose exec kernel-home-bundler-7702 node --conditions development /app/packages/evm-wallet-experiment/docker/create-delegation.mjs",
7071
"docker:logs": "yarn docker:compose logs -f --tail=100",
7172
"test:e2e:docker": "vitest run --config vitest.config.docker.ts"
7273
},

0 commit comments

Comments
 (0)