Skip to content

Commit 35520d6

Browse files
committed
feat: auto-resolve base images for all drivers
1 parent 08ecf5b commit 35520d6

22 files changed

+580
-34
lines changed

cmd/claw/agent.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ func resolveAgentAddConfig(ctx *agentAddContext, opts agentAddOptions) (*agentAd
623623

624624
if cfg.ClawType == "" {
625625
if promptMode {
626-
value, err := promptSelect(reader, os.Stdout, "Claw type", []string{"openclaw", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "hermes", "generic"}, 0)
626+
value, err := promptSelect(reader, os.Stdout, "Claw type", scaffoldClawTypes, 0)
627627
if err != nil {
628628
return nil, err
629629
}
@@ -1013,7 +1013,7 @@ func rewireContracts(servicesNode *yaml.Node, sourcePath, targetPath string) int
10131013

10141014
func init() {
10151015
agentAddCmd.Flags().StringVar(&agentNameFlag, "agent", "", "Agent name (service and directory name)")
1016-
agentAddCmd.Flags().StringVar(&agentTypeFlag, "type", "", "Claw type (openclaw, nanoclaw, microclaw, nullclaw, nanobot, picoclaw, hermes, generic)")
1016+
agentAddCmd.Flags().StringVar(&agentTypeFlag, "type", "", "Claw type ("+strings.Join(scaffoldClawTypes, ", ")+")")
10171017
agentAddCmd.Flags().StringVar(&agentModelFlag, "model", "", "Primary model (provider/model)")
10181018
agentAddCmd.Flags().StringVar(&agentCllamaFlag, "cllama", "", "Use cllama proxy (yes/no/inherit)")
10191019
agentAddCmd.Flags().StringVar(&agentPlatformFlag, "platform", "", "Platform handle (discord, slack, telegram, none)")

cmd/claw/agent_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,12 @@ func TestAgentAddTypeDefaults(t *testing.T) {
170170
baseImage string
171171
}{
172172
{name: "generic", agentName: "genericone", clawType: "generic", baseImage: "alpine:3.20"},
173-
{name: "nanoclaw", agentName: "nanoclawone", clawType: "nanoclaw", baseImage: "node:22-slim"},
174-
{name: "microclaw", agentName: "microclawone", clawType: "microclaw", baseImage: "node:22-slim"},
175-
{name: "nullclaw", agentName: "nullclawone", clawType: "nullclaw", baseImage: "node:22-slim"},
173+
{name: "hermes", agentName: "hermesone", clawType: "hermes", baseImage: "hermes:latest"},
174+
{name: "nanoclaw", agentName: "nanoclawone", clawType: "nanoclaw", baseImage: "nanoclaw-orchestrator:latest"},
175+
{name: "microclaw", agentName: "microclawone", clawType: "microclaw", baseImage: "microclaw:latest"},
176+
{name: "nullclaw", agentName: "nullclawone", clawType: "nullclaw", baseImage: "nullclaw:latest"},
176177
{name: "nanobot", agentName: "nanobotone", clawType: "nanobot", baseImage: "nanobot:latest"},
177-
{name: "picoclaw", agentName: "picoclawone", clawType: "picoclaw", baseImage: "docker.io/sipeed/picoclaw:latest"},
178-
{name: "hermes", agentName: "hermesone", clawType: "hermes", baseImage: "ghcr.io/mostlydev/hermes-base:v2026.3.17"},
178+
{name: "picoclaw", agentName: "picoclawone", clawType: "picoclaw", baseImage: "picoclaw:latest"},
179179
}
180180

181181
for _, tc := range tests {
@@ -217,7 +217,7 @@ func TestAgentAddTypeFlagUsageListsAllScaffoldTypes(t *testing.T) {
217217
}
218218

219219
usage := flag.Usage
220-
for _, typ := range []string{"openclaw", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "hermes", "generic"} {
220+
for _, typ := range []string{"openclaw", "hermes", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "generic"} {
221221
if !strings.Contains(usage, typ) {
222222
t.Fatalf("expected agent add --type usage to include %q, got: %s", typ, usage)
223223
}

cmd/claw/compose_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func TestResolveComposeGeneratedPathWithPodFileMissingGenerated(t *testing.T) {
8282
}
8383

8484
func TestBuiltinDriversRegistered(t *testing.T) {
85-
for _, clawType := range []string{"openclaw", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw"} {
85+
for _, clawType := range []string{"openclaw", "hermes", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw"} {
8686
if _, err := driver.Lookup(clawType); err != nil {
8787
t.Fatalf("expected driver %q to be registered in CLI package: %v", clawType, err)
8888
}

cmd/claw/init.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ func resolveInitConfig(dir string, opts initScaffoldOptions, interactive bool) (
431431

432432
if cfg.ClawType == "" {
433433
if interactive {
434-
v, err := promptSelect(reader, os.Stdout, "Claw type", []string{"openclaw", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "hermes", "generic"}, 0)
434+
v, err := promptSelect(reader, os.Stdout, "Claw type", scaffoldClawTypes, 0)
435435
if err != nil {
436436
return nil, fmt.Errorf("prompt claw type: %w", err)
437437
}
@@ -695,7 +695,7 @@ func init() {
695695
initCmd.Flags().StringVar(&initFromPath, "from", "", "Path to existing OpenClaw config directory to migrate from")
696696
initCmd.Flags().StringVar(&initProject, "project", "", "Project name used for x-claw.pod and image prefix")
697697
initCmd.Flags().StringVar(&initAgent, "agent", "", "Primary agent name (service + directory name)")
698-
initCmd.Flags().StringVar(&initType, "type", "", "Claw type (openclaw, nanoclaw, microclaw, nullclaw, nanobot, picoclaw, hermes, generic)")
698+
initCmd.Flags().StringVar(&initType, "type", "", "Claw type ("+strings.Join(scaffoldClawTypes, ", ")+")")
699699
initCmd.Flags().StringVar(&initModel, "model", "", "Primary model (provider/model)")
700700
initCmd.Flags().StringVar(&initCllama, "cllama", "", "Use cllama proxy (yes/no)")
701701
initCmd.Flags().StringVar(&initPlatform, "platform", "", "Platform handle (discord, slack, telegram, none)")

cmd/claw/init_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,12 @@ func TestInitScaffoldTypeDefaults(t *testing.T) {
126126
baseImage string
127127
}{
128128
{name: "generic", clawType: "generic", baseImage: "alpine:3.20"},
129-
{name: "nanoclaw", clawType: "nanoclaw", baseImage: "node:22-slim"},
130-
{name: "microclaw", clawType: "microclaw", baseImage: "node:22-slim"},
131-
{name: "nullclaw", clawType: "nullclaw", baseImage: "node:22-slim"},
129+
{name: "hermes", clawType: "hermes", baseImage: "hermes:latest"},
130+
{name: "nanoclaw", clawType: "nanoclaw", baseImage: "nanoclaw-orchestrator:latest"},
131+
{name: "microclaw", clawType: "microclaw", baseImage: "microclaw:latest"},
132+
{name: "nullclaw", clawType: "nullclaw", baseImage: "nullclaw:latest"},
132133
{name: "nanobot", clawType: "nanobot", baseImage: "nanobot:latest"},
133-
{name: "picoclaw", clawType: "picoclaw", baseImage: "docker.io/sipeed/picoclaw:latest"},
134-
{name: "hermes", clawType: "hermes", baseImage: "ghcr.io/mostlydev/hermes-base:v2026.3.17"},
134+
{name: "picoclaw", clawType: "picoclaw", baseImage: "picoclaw:latest"},
135135
}
136136

137137
for _, tc := range tests {
@@ -163,7 +163,7 @@ func TestInitTypeFlagUsageListsAllScaffoldTypes(t *testing.T) {
163163
}
164164

165165
usage := flag.Usage
166-
for _, typ := range []string{"openclaw", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "hermes", "generic"} {
166+
for _, typ := range []string{"openclaw", "hermes", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "generic"} {
167167
if !strings.Contains(usage, typ) {
168168
t.Fatalf("expected init --type usage to include %q, got: %s", typ, usage)
169169
}

cmd/claw/scaffold_helpers.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const (
2222

2323
var validNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_-]*$`)
2424

25+
var scaffoldClawTypes = []string{"openclaw", "hermes", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "generic"}
26+
2527
func shouldPromptInteractively() bool {
2628
info, err := os.Stdin.Stat()
2729
if err != nil {
@@ -153,29 +155,29 @@ func parseClawType(value string) (string, error) {
153155
switch v {
154156
case "":
155157
return "", fmt.Errorf("claw type is required")
156-
case "openclaw", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "hermes", "generic":
158+
case "openclaw", "hermes", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "generic":
157159
return v, nil
158160
default:
159-
return "", fmt.Errorf("invalid claw type %q (allowed: openclaw, nanoclaw, microclaw, nullclaw, nanobot, picoclaw, hermes, generic)", value)
161+
return "", fmt.Errorf("invalid claw type %q (allowed: %s)", value, strings.Join(scaffoldClawTypes, ", "))
160162
}
161163
}
162164

163165
func defaultBaseImageForClawType(clawType string) string {
164166
switch strings.TrimSpace(strings.ToLower(clawType)) {
165167
case "openclaw":
166168
return "openclaw:latest"
169+
case "hermes":
170+
return "hermes:latest"
167171
case "nanoclaw":
168-
return "node:22-slim"
172+
return "nanoclaw-orchestrator:latest"
169173
case "microclaw":
170-
return "node:22-slim"
174+
return "microclaw:latest"
171175
case "nullclaw":
172-
return "node:22-slim"
176+
return "nullclaw:latest"
173177
case "nanobot":
174178
return "nanobot:latest"
175179
case "picoclaw":
176-
return "docker.io/sipeed/picoclaw:latest"
177-
case "hermes":
178-
return "ghcr.io/mostlydev/hermes-base:v2026.3.17"
180+
return "picoclaw:latest"
179181
case "generic":
180182
return "alpine:3.20"
181183
default:

cmd/claw/scaffold_helpers_test.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import (
88
func TestParseClawTypeAcceptsSupportedValues(t *testing.T) {
99
tests := []string{
1010
"openclaw",
11+
"hermes",
1112
"nanoclaw",
1213
"microclaw",
1314
"nullclaw",
1415
"nanobot",
1516
"picoclaw",
16-
"hermes",
1717
"generic",
1818
}
1919

@@ -36,8 +36,10 @@ func TestParseClawTypeRejectsUnknownValue(t *testing.T) {
3636
if !strings.Contains(err.Error(), "invalid claw type") {
3737
t.Fatalf("unexpected error: %v", err)
3838
}
39-
if !strings.Contains(err.Error(), "nanobot") || !strings.Contains(err.Error(), "hermes") {
40-
t.Fatalf("expected error to list nanobot/hermes, got: %v", err)
39+
for _, expected := range []string{"hermes", "nanobot", "picoclaw"} {
40+
if !strings.Contains(err.Error(), expected) {
41+
t.Fatalf("expected error to list %s, got: %v", expected, err)
42+
}
4143
}
4244
}
4345

@@ -48,12 +50,12 @@ func TestDefaultBaseImageForClawType(t *testing.T) {
4850
want string
4951
}{
5052
{name: "openclaw", clawType: "openclaw", want: "openclaw:latest"},
51-
{name: "nanoclaw", clawType: "nanoclaw", want: "node:22-slim"},
52-
{name: "microclaw", clawType: "microclaw", want: "node:22-slim"},
53-
{name: "nullclaw", clawType: "nullclaw", want: "node:22-slim"},
53+
{name: "hermes", clawType: "hermes", want: "hermes:latest"},
54+
{name: "nanoclaw", clawType: "nanoclaw", want: "nanoclaw-orchestrator:latest"},
55+
{name: "microclaw", clawType: "microclaw", want: "microclaw:latest"},
56+
{name: "nullclaw", clawType: "nullclaw", want: "nullclaw:latest"},
5457
{name: "nanobot", clawType: "nanobot", want: "nanobot:latest"},
55-
{name: "picoclaw", clawType: "picoclaw", want: "docker.io/sipeed/picoclaw:latest"},
56-
{name: "hermes", clawType: "hermes", want: "ghcr.io/mostlydev/hermes-base:v2026.3.17"},
58+
{name: "picoclaw", clawType: "picoclaw", want: "picoclaw:latest"},
5759
{name: "generic", clawType: "generic", want: "alpine:3.20"},
5860
{name: "unknown", clawType: "something-else", want: "alpine:3.20"},
5961
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Base Image Auto-Resolution for All Drivers
2+
3+
**Date:** 2026-03-21
4+
**Status:** Approved
5+
6+
## Problem
7+
8+
Most Clawfiles in-tree use repo-local driver tags such as `openclaw:latest`, `hermes:latest`, `microclaw:latest`, and `nullclaw:latest`, but not all of them. Current exceptions include:
9+
10+
- `examples/picoclaw/Clawfile` using `FROM docker.io/sipeed/picoclaw:latest`
11+
- nanoclaw examples using `FROM nanoclaw-orchestrator:latest`
12+
13+
Today, only the openclaw driver implements `BaseImageProvider`, so only it auto-builds when the local base image is missing. Users of the other six drivers must manually build or pull base images before `claw build` works.
14+
15+
Maintaining Clawdapus-owned pre-built images for every runtime is the wrong burden. Some upstreams already publish official images, some publish installers or packages, and nanoclaw currently needs a Clawdapus-compatible orchestrator wrapper. The implementation needs to follow the packaging reality of each driver instead of forcing one recipe style onto all of them.
16+
17+
## Solution
18+
19+
Each driver ships a `baseimage.go` file implementing `BaseImageProvider`. When `claw build` encounters a missing `FROM` image, the existing `ensureBaseImage()` in `internal/build/build.go` already handles resolution — no changes needed to the build pipeline. We just need to fill in the missing implementations.
20+
21+
Each `baseimage.go` produces a self-contained Dockerfile string. The rule is:
22+
23+
- prefer the real upstream packaging path
24+
- use a local compatibility alias over an official upstream image when that is the upstream distribution model
25+
- use a source-build wrapper only when no stable published base fits the current Clawdapus runtime contract
26+
27+
That keeps the images real, but avoids rebuilding tooling that upstream already ships.
28+
29+
## Per-Driver Dockerfile Recipes
30+
31+
### openclaw (already exists — no changes)
32+
33+
- Base: `node:22-slim`
34+
- Install: `curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-prompt --no-onboard --method npm`
35+
- Entrypoint: `openclaw gateway --port 18789 --bind loopback`
36+
- Extras: build-essential, python3, cmake (native deps for npm packages)
37+
38+
### hermes
39+
40+
- Tag: `hermes:latest`
41+
- Base: `ghcr.io/astral-sh/uv:python3.11-bookworm-slim`
42+
- Install: clone `https://github.com/NousResearch/hermes-agent` and `uv pip install --system "/opt/hermes-agent[messaging,cron]"`
43+
- Entrypoint: `hermes gateway start`
44+
- Extras: `bash`, `ca-certificates`, `curl`, `git`, `jq`, `procps`, `tini`
45+
- Notes: use the real Hermes repo and install the packaged CLI entrypoint instead of a stub shell script
46+
47+
### nanobot
48+
49+
- Tag: `nanobot:latest`
50+
- Base: `ghcr.io/astral-sh/uv:python3.12-bookworm-slim`
51+
- Install: `uv pip install --system --no-cache nanobot-ai`
52+
- Entrypoint: `nanobot gateway`
53+
- Extras: `ca-certificates`, `curl`, `git`, `jq`, `procps`, `tini`
54+
55+
### picoclaw
56+
57+
- Tag: `picoclaw:latest`
58+
- Base: `docker.io/sipeed/picoclaw:latest`
59+
- Install: none; local base image is a compatibility alias over the upstream image
60+
- Entrypoint: `picoclaw gateway`
61+
- Extras: inherit upstream runtime as-is
62+
- Notes: this keeps the local `picoclaw:latest` tag usable for auto-resolution without forking the upstream container recipe
63+
64+
### nullclaw
65+
66+
- Tag: `nullclaw:latest`
67+
- Base: `ghcr.io/nullclaw/nullclaw:latest`
68+
- Install: none; local base image is a compatibility alias over the upstream image
69+
- Entrypoint: inherited upstream `nullclaw gateway --port 3000 --host ::`
70+
- Extras: inherit upstream runtime as-is
71+
- Notes: this avoids reimplementing the upstream Zig build and preserves the current HOME/config behavior the driver already targets
72+
73+
### microclaw
74+
75+
- Tag: `microclaw:latest`
76+
- Base: `ghcr.io/microclaw/microclaw:latest`
77+
- Install: add `procps` and create `/app/config` for the file mount used by the driver
78+
- Entrypoint: `microclaw start`
79+
- Extras: inherit upstream runtime plus `procps`
80+
- Notes: the upstream image is the real runtime; the only local change is making the image satisfy Clawdapus healthcheck and mount assumptions
81+
82+
### nanoclaw
83+
84+
- Tag: `nanoclaw-orchestrator:latest`
85+
- Builder: `node:22-bookworm-slim` + clone `https://github.com/qwibitai/nanoclaw.git` + `npm ci && npm run build`
86+
- Runtime: `node:22-bookworm-slim` + Docker CLI copied from `docker:27-cli`
87+
- Entrypoint: `node /workspace/dist/index.js`
88+
- Extras: `ca-certificates`, `git`, `procps`, `python3`, `make`, `g++`, `tini`, global `@anthropic-ai/claude-code`
89+
- Notes: nanoclaw is the one driver that still needs a source-build compatibility image because Clawdapus expects an orchestrator container with Docker CLI access and Claude Code SDK wiring
90+
91+
## File Layout
92+
93+
### New files (one per driver)
94+
95+
- `internal/driver/hermes/baseimage.go`
96+
- `internal/driver/nanobot/baseimage.go`
97+
- `internal/driver/nanoclaw/baseimage.go`
98+
- `internal/driver/nullclaw/baseimage.go`
99+
- `internal/driver/microclaw/baseimage.go`
100+
- `internal/driver/picoclaw/baseimage.go`
101+
102+
### New test files (one per driver)
103+
104+
- `internal/driver/<name>/baseimage_test.go` — same pattern as openclaw: assert interface satisfaction, non-empty tag/dockerfile
105+
106+
### No changes needed
107+
108+
- `internal/build/build.go``ensureBaseImage()` already handles the resolution
109+
- `internal/driver/types.go``BaseImageProvider` interface already exists
110+
- `internal/driver/openclaw/baseimage.go` — already correct
111+
- Rollcall `Dockerfile.*-base` files — stay as spike test fixtures
112+
113+
### Adjacent doc cleanup
114+
115+
- fix stale Hermes upstream references to point at `https://github.com/NousResearch/hermes-agent`
116+
117+
## Follow-up
118+
119+
- optional cleanup: decide later whether to standardize `examples/picoclaw/Clawfile` on local `picoclaw:latest`; the current fully qualified upstream image still works via Docker's normal pull path

examples/quickstart/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ cp .env.example .env
4040

4141
```bash
4242
source .env
43-
claw build -t quickstart-assistant ./agents/assistant
43+
claw build -t quickstart-assistant:latest ./agents/assistant
4444
claw up -f claw-pod.yml -d
4545
```
4646

47+
On the first run, `claw build` auto-builds the local `openclaw:latest` base image if it is missing.
48+
4749
## 4. Verify
4850

4951
```bash

0 commit comments

Comments
 (0)