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
3 changes: 0 additions & 3 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@ body:
options:
- openclaw
- hermes
- nanoclaw
- nanobot
- picoclaw
- nullclaw
- microclaw
- generic / none
- not driver-related
validations:
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ cmd/clawdash/node_modules/
testdata/*
!testdata/openclaw-stub/
!testdata/openclaw-stub-nonroot/
!testdata/microclaw-stub/
!testdata/nanobot-stub/
!testdata/tool-stub/
*-notes.md
.claude/settings.local.json
Expand Down
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,12 @@ Driver directories currently in-tree:
- `internal/driver/openclaw`
- `internal/driver/hermes`
- `internal/driver/nanobot`
- `internal/driver/nanoclaw`
- `internal/driver/picoclaw`
- `internal/driver/microclaw`
- `internal/driver/nullclaw`
- `internal/driver/shared`

NanoClaw, MicroClaw, and NullClaw were retired in ADR-026; their `CLAW_TYPE`s fail
closed with a migration message.

Do not assume older docs mentioning only a subset are current.

## Runtime Model That Exists Today
Expand All @@ -162,13 +162,13 @@ Do not assume older docs mentioning only a subset are current.

## Repo-Specific Gotchas

- Bug fixes in one driver often apply to all 7. When fixing driver behavior (permissions, config defaults, env vars), check all drivers in `internal/driver/*/driver.go` and `config.go` — not just the one mentioned in the issue.
- Bug fixes in one driver often apply to all 4. When fixing driver behavior (permissions, config defaults, env vars), check all drivers in `internal/driver/*/driver.go` and `config.go` — not just the one mentioned in the issue.
- Runtime directories created by `Materialize()` use `0o777` (not `0o700`) so container users with different uids can write. Do not regress this.
- All drivers set `mention_only` (or equivalent like `requireMention`, `DISCORD_REQUIRE_MENTION`) for Discord channels. Without this, multi-agent pods enter feedback loops.
- All drivers explicitly set `HOME` in the container env map to match their config mount path. Container base images may run as root or a different user than expected.
- `cllama/` is a git submodule pointing to a public SSH repo. Fresh `git clone` leaves it empty. Infra images (cllama, clawdash) are published to ghcr.io as public packages to avoid this for end users. `cllama/` has its own `.git` — changes require two commits: one inside `cllama/` (for feeds/proxy code), then `git add cllama && git commit` in the repo root to update the pointer. Shell working directory can silently drift to `cllama/` between commands — use absolute paths for git operations or verify with `pwd` first.
- `internal/feeds/` and other cllama internals live at `cllama/internal/`, not at the repo root.
- Infra image remediation is explicit now: `claw pull` owns pinned infra freshness *and* built-in local runner alias freshness (`openclaw:latest`, `nanobot:latest`, `nanoclaw-orchestrator:latest`, etc.) refreshed via `docker build --pull --no-cache`; `claw build` owns pod `build:` services and consumes an already-refreshed runner alias; `claw up` points at one or the other when something is missing. `claw up --fix` is the opt-in auto-remediation path. Use `claw pull --no-runners` for the fast pinned-infra-only path. See ADR-024.
- Infra image remediation is explicit now: `claw pull` owns pinned infra freshness *and* built-in local runner alias freshness (`openclaw:latest`, `nanobot:latest`, `picoclaw:latest`, etc.) refreshed via `docker build --pull --no-cache`; `claw build` owns pod `build:` services and consumes an already-refreshed runner alias; `claw up` points at one or the other when something is missing. `claw up --fix` is the opt-in auto-remediation path. Use `claw pull --no-runners` for the fast pinned-infra-only path. See ADR-024.
- Runner base provenance: `claw build` rewrites `FROM <alias>:latest` to `FROM <alias>:v<version>` in `Dockerfile.generated` and stamps three labels (`claw.runner.built-against`, `claw.runner.image-id`, `claw.runner.recipe-sha`). `claw up` reads `claw.runner.image-id` and prints a soft drift hint when the local alias has moved on; it does not auto-rebuild. Service images built with a manual `docker build` of a runner base must also create a versioned sibling tag, otherwise `claw build` will fail-closed with a `claw pull` remediation hint.
- Managed services require `claw up -d` because post-apply verification is fail-closed.
- Multi-proxy cllama is represented in the data model but runtime currently fails fast if more than one proxy type is declared.
Expand Down
50 changes: 25 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ The Clawfile extends the Dockerfile with directives that the `claw build` prepro

| Directive | Purpose |
|---|---|
| `CLAW_TYPE` | Selects the runtime driver (openclaw, hermes, nanobot, picoclaw, nanoclaw, microclaw, nullclaw) |
| `CLAW_TYPE` | Selects the runtime driver (openclaw, hermes, nanobot, picoclaw) |
| `AGENT` | Names the behavioral contract file |
| `PERSONA` | Imports a persona workspace — local path or OCI artifact ref |
| `MODEL` | Binds named model slots to providers |
Expand All @@ -285,18 +285,21 @@ The Clawfile extends the Dockerfile with directives that the `claw build` prepro

Pick a driver based on what you need. All drivers support `MODEL`, `AGENT`, `CLLAMA`, and `CONFIGURE`.

| | `openclaw` | `hermes` | `nanoclaw` | `nanobot` | `picoclaw` | `nullclaw` | `microclaw` |
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| **Runtime** | [OpenClaw](https://openclaw.ai) | [Hermes](https://github.com/NousResearch/hermes-agent) | NanoClaw / Claude Code-compatible orchestrator | [Nanobot](https://github.com/HKUDS/nanobot) | [PicoClaw](https://github.com/sipeed/picoclaw) | [NullClaw](https://github.com/nullclaw/nullclaw) | [MicroClaw](https://github.com/microclaw/microclaw) |
| `claw init` scaffold | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| HANDLE: Discord | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ |
| HANDLE: Telegram | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ |
| HANDLE: Slack | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ |
| HANDLE: long-tail ¹ | — | — | — | — | ✅ | — | — |
| INVOKE (cron) | ✅ | ✅ | — | ✅ | ✅ | ✅ | — |
| Structured health | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Read-only rootfs | ✅ | ✅ | no | ✅ | ✅ | ✅ | no |
| Non-root container | — | — | — | — | ✅ | — | — |
| | `openclaw` | `hermes` | `nanobot` | `picoclaw` |
|---|:---:|:---:|:---:|:---:|
| **Runtime** | [OpenClaw](https://openclaw.ai) | [Hermes](https://github.com/NousResearch/hermes-agent) | [Nanobot](https://github.com/HKUDS/nanobot) | [PicoClaw](https://github.com/sipeed/picoclaw) |
| `claw init` scaffold | ✅ | ✅ | ✅ | ✅ |
| HANDLE: Discord | ✅ | ✅ | ✅ | ✅ |
| HANDLE: Telegram | ✅ | ✅ | ✅ | ✅ |
| HANDLE: Slack | ✅ | ✅ | ✅ | ✅ |
| HANDLE: long-tail ¹ | — | — | — | ✅ |
| INVOKE (cron) | ✅ | ✅ | ✅ | ✅ |
| Structured health | ✅ | ✅ | ✅ | ✅ |
| Read-only rootfs | ✅ | ✅ | ✅ | ✅ |
| Non-root container | — | — | — | ✅ |

Retired drivers (`nanoclaw`, `microclaw`, `nullclaw`) fail `claw up` with a migration
message; see [ADR-026](docs/decisions/026-runner-adoption-and-retirement.md).

¹ PicoClaw long-tail: WhatsApp, Feishu, LINE, QQ, DingTalk, OneBot, WeCom, WeCom App, Pico, MaixCam.
`claw init` scaffolds `generic` (alpine:3.20, no driver enforcement) for custom runtimes.
Expand All @@ -319,29 +322,26 @@ The OpenClaw driver now maps the supported `channel://discord` routing controls

---

## Nullclaw `CONFIGURE` Examples
## Driver `CONFIGURE` Examples

Use these when you want high-level `HANDLE` defaults, but need runtime-specific policy details.

```dockerfile
# Base identity on a platform:
HANDLE discord

# "Can talk on" -> pin to one guild/server
CONFIGURE nullclaw config set channels.discord.accounts.main.guild_id "123456789012345678"
# Enable/adjust a channel beyond HANDLE defaults
CONFIGURE picoclaw config set channels.discord.enabled true

# "Can talk to" -> require mention in group chats
CONFIGURE nullclaw config set channels.discord.accounts.main.require_mention true
# Override the gateway port
CONFIGURE picoclaw config set gateway.port 19000

# Telegram allowlist for DMs
CONFIGURE nullclaw config set channels.telegram.accounts.main.allow_from ["111111111","222222222"]

# Slack transport mode selection
CONFIGURE nullclaw config set channels.slack.accounts.main.mode "socket"
# Pin a fallback model name
CONFIGURE picoclaw config set agents.defaults.model_name "fallback"
```

Notes:
- `CONFIGURE` is driver-side DSL here (`nullclaw config set <path> <value>`), applied to generated `config.json`.
- `CONFIGURE` is driver-side DSL (`<runner> config set <path> <value>`), applied to the generated config.
- Values are parsed as JSON when possible: booleans/numbers/arrays/objects should be unquoted; strings should be quoted.
- `CONFIGURE` runs after defaults, so it overrides what `HANDLE` generated.

Expand All @@ -353,7 +353,7 @@ Notes:
block-beta
columns 1
contract["Behavioral Contract\nread-only bind mount\nAGENTS.md — purpose, on the host\nSurvives full container compromise"]
runner["Runner\nOpenClaw · NanoClaw · Claude Code · custom"]
runner["Runner\nOpenClaw · Hermes · Nanobot · PicoClaw"]
persona["Persona\nMemory · history · style · knowledge"]
proxy["cllama — governance proxy\nIntercepts prompts outbound + responses inbound\nRunner never knows it's there"]

Expand Down
7 changes: 2 additions & 5 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Docker, and a real Discord server. They are not CI tests.

The two primary spike paths are:

- `TestSpikeRollCall`: the broad driver-parity validation path. Boots all 7 driver
- `TestSpikeRollCall`: the broad driver-parity validation path. Boots all 4 retained driver
types plus `cllama` passthrough and `clawdash`, sends a Discord roll call, and
verifies runtime-specific responses.
- `TestSpikeComposeUp`: the deeper trading-desk validation path. Focuses on artifact
Expand All @@ -54,9 +54,6 @@ cross-driver support. It uses [`examples/rollcall/`](./examples/rollcall/) and
exercises:

- `openclaw`
- `nullclaw`
- `microclaw`
- `nanoclaw`
- `nanobot`
- `picoclaw`
- `hermes`
Expand All @@ -65,7 +62,7 @@ exercises:

### What it validates

- Base images build for all 7 driver families
- Base images build for all 4 retained driver families
- Agent images build from their `Clawfile`s
- `claw up` succeeds on the rollcall pod
- All agent containers converge to healthy/running state
Expand Down
2 changes: 1 addition & 1 deletion cmd/claw-api/agent_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestAgentsListFiltersByServiceScopeAndLiveContext(t *testing.T) {
})
writeAgentContextFixture(t, contextRoot, "analyst-0", map[string]string{
"service": "analyst",
"type": "nanoclaw",
"type": "nanobot",
})

var sawAuth string
Expand Down
3 changes: 0 additions & 3 deletions cmd/claw-api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ import (
manifestpkg "github.com/mostlydev/clawdapus/internal/clawdash"
"github.com/mostlydev/clawdapus/internal/driver"
_ "github.com/mostlydev/clawdapus/internal/driver/hermes"
_ "github.com/mostlydev/clawdapus/internal/driver/microclaw"
_ "github.com/mostlydev/clawdapus/internal/driver/nanobot"
_ "github.com/mostlydev/clawdapus/internal/driver/nanoclaw"
_ "github.com/mostlydev/clawdapus/internal/driver/nullclaw"
_ "github.com/mostlydev/clawdapus/internal/driver/openclaw"
_ "github.com/mostlydev/clawdapus/internal/driver/picoclaw"
schedulepkg "github.com/mostlydev/clawdapus/internal/schedule"
Expand Down
5 changes: 1 addition & 4 deletions cmd/claw/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,6 @@ func TestAgentAddTypeDefaults(t *testing.T) {
}{
{name: "generic", agentName: "genericone", clawType: "generic", baseImage: "alpine:3.20"},
{name: "hermes", agentName: "hermesone", clawType: "hermes", baseImage: hermes.BaseImageTag},
{name: "nanoclaw", agentName: "nanoclawone", clawType: "nanoclaw", baseImage: "nanoclaw-orchestrator:latest"},
{name: "microclaw", agentName: "microclawone", clawType: "microclaw", baseImage: "microclaw:latest"},
{name: "nullclaw", agentName: "nullclawone", clawType: "nullclaw", baseImage: "nullclaw:latest"},
{name: "nanobot", agentName: "nanobotone", clawType: "nanobot", baseImage: "nanobot:latest"},
{name: "picoclaw", agentName: "picoclawone", clawType: "picoclaw", baseImage: "picoclaw:latest"},
}
Expand Down Expand Up @@ -266,7 +263,7 @@ func TestAgentAddTypeFlagUsageListsAllScaffoldTypes(t *testing.T) {
}

usage := flag.Usage
for _, typ := range []string{"openclaw", "hermes", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "generic"} {
for _, typ := range []string{"openclaw", "hermes", "nanobot", "picoclaw", "generic"} {
if !strings.Contains(usage, typ) {
t.Fatalf("expected agent add --type usage to include %q, got: %s", typ, usage)
}
Expand Down
3 changes: 0 additions & 3 deletions cmd/claw/compose_health.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ import (

"github.com/mostlydev/clawdapus/internal/driver"
_ "github.com/mostlydev/clawdapus/internal/driver/hermes"
_ "github.com/mostlydev/clawdapus/internal/driver/microclaw"
_ "github.com/mostlydev/clawdapus/internal/driver/nanobot"
_ "github.com/mostlydev/clawdapus/internal/driver/nanoclaw"
_ "github.com/mostlydev/clawdapus/internal/driver/nullclaw"
_ "github.com/mostlydev/clawdapus/internal/driver/openclaw"
_ "github.com/mostlydev/clawdapus/internal/driver/picoclaw"
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/claw/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func TestComposePassthroughAllowsStaleGeneratedOnlyForBuild(t *testing.T) {
}

func TestBuiltinDriversRegistered(t *testing.T) {
for _, clawType := range []string{"openclaw", "hermes", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw"} {
for _, clawType := range []string{"openclaw", "hermes", "nanobot", "picoclaw"} {
if _, err := driver.Lookup(clawType); err != nil {
t.Fatalf("expected driver %q to be registered in CLI package: %v", clawType, err)
}
Expand Down
5 changes: 1 addition & 4 deletions cmd/claw/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,6 @@ func TestInitScaffoldTypeDefaults(t *testing.T) {
}{
{name: "generic", clawType: "generic", baseImage: "alpine:3.20"},
{name: "hermes", clawType: "hermes", baseImage: hermes.BaseImageTag},
{name: "nanoclaw", clawType: "nanoclaw", baseImage: "nanoclaw-orchestrator:latest"},
{name: "microclaw", clawType: "microclaw", baseImage: "microclaw:latest"},
{name: "nullclaw", clawType: "nullclaw", baseImage: "nullclaw:latest"},
{name: "nanobot", clawType: "nanobot", baseImage: "nanobot:latest"},
{name: "picoclaw", clawType: "picoclaw", baseImage: "picoclaw:latest"},
}
Expand Down Expand Up @@ -362,7 +359,7 @@ func TestInitTypeFlagUsageListsAllScaffoldTypes(t *testing.T) {
}

usage := flag.Usage
for _, typ := range []string{"openclaw", "hermes", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "generic"} {
for _, typ := range []string{"openclaw", "hermes", "nanobot", "picoclaw", "generic"} {
if !strings.Contains(usage, typ) {
t.Fatalf("expected init --type usage to include %q, got: %s", typ, usage)
}
Expand Down
14 changes: 6 additions & 8 deletions cmd/claw/scaffold_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"unicode"

"github.com/mostlydev/clawdapus/internal/driver"
"github.com/mostlydev/clawdapus/internal/driver/hermes"
)

Expand All @@ -24,7 +25,7 @@ const (

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

var scaffoldClawTypes = []string{"openclaw", "hermes", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "generic"}
var scaffoldClawTypes = []string{"openclaw", "hermes", "nanobot", "picoclaw", "generic"}

func shouldPromptInteractively() bool {
info, err := os.Stdin.Stat()
Expand Down Expand Up @@ -157,9 +158,12 @@ func parseClawType(value string) (string, error) {
switch v {
case "":
return "", fmt.Errorf("claw type is required")
case "openclaw", "hermes", "nanoclaw", "microclaw", "nullclaw", "nanobot", "picoclaw", "generic":
case "openclaw", "hermes", "nanobot", "picoclaw", "generic":
return v, nil
default:
if err := driver.RetirementError(v); err != nil {
return "", err
}
return "", fmt.Errorf("invalid claw type %q (allowed: %s)", value, strings.Join(scaffoldClawTypes, ", "))
}
}
Expand All @@ -170,12 +174,6 @@ func defaultBaseImageForClawType(clawType string) string {
return "openclaw:latest"
case "hermes":
return hermes.BaseImageTag
case "nanoclaw":
return "nanoclaw-orchestrator:latest"
case "microclaw":
return "microclaw:latest"
case "nullclaw":
return "nullclaw:latest"
case "nanobot":
return "nanobot:latest"
case "picoclaw":
Expand Down
22 changes: 16 additions & 6 deletions cmd/claw/scaffold_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ func TestParseClawTypeAcceptsSupportedValues(t *testing.T) {
tests := []string{
"openclaw",
"hermes",
"nanoclaw",
"microclaw",
"nullclaw",
"nanobot",
"picoclaw",
"generic",
Expand Down Expand Up @@ -45,6 +42,22 @@ func TestParseClawTypeRejectsUnknownValue(t *testing.T) {
}
}

func TestParseClawTypeRejectsRetiredValuesWithMigrationGuidance(t *testing.T) {
for _, clawType := range []string{"nanoclaw", "microclaw", "nullclaw"} {
t.Run(clawType, func(t *testing.T) {
_, err := parseClawType(clawType)
if err == nil {
t.Fatalf("expected %s to be rejected", clawType)
}
for _, want := range []string{clawType, "retired", "ADR-026", `CLAW_TYPE "hermes"`} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("retirement error %q does not contain %q", err, want)
}
}
})
}
}

func TestDefaultBaseImageForClawType(t *testing.T) {
tests := []struct {
name string
Expand All @@ -53,9 +66,6 @@ func TestDefaultBaseImageForClawType(t *testing.T) {
}{
{name: "openclaw", clawType: "openclaw", want: "openclaw:latest"},
{name: "hermes", clawType: "hermes", want: hermes.BaseImageTag},
{name: "nanoclaw", clawType: "nanoclaw", want: "nanoclaw-orchestrator:latest"},
{name: "microclaw", clawType: "microclaw", want: "microclaw:latest"},
{name: "nullclaw", clawType: "nullclaw", want: "nullclaw:latest"},
{name: "nanobot", clawType: "nanobot", want: "nanobot:latest"},
{name: "picoclaw", clawType: "picoclaw", want: "picoclaw:latest"},
{name: "generic", clawType: "generic", want: "alpine:3.20"},
Expand Down
10 changes: 2 additions & 8 deletions cmd/claw/schedule_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func buildScheduleManifest(p *pod.Pod, resolved map[string]*driver.ResolvedClaw)

func supportsExternalScheduler(clawType string) bool {
switch strings.TrimSpace(clawType) {
case "openclaw", "hermes", "nanobot", "picoclaw", "nullclaw":
case "openclaw", "hermes", "nanobot", "picoclaw":
return true
default:
return false
Expand Down Expand Up @@ -150,12 +150,6 @@ func resolveWakeAdapter(clawType, target string, inv driver.Invocation) (schedul
Target: target,
Command: []string{"picoclaw", "agent", "-m", inv.Message},
}, nil
case "nullclaw":
return schedule.Wake{
Adapter: "nullclaw-exec",
Target: target,
Command: []string{"nullclaw", "agent", "-m", inv.Message},
}, nil
default:
return schedule.Wake{}, fmt.Errorf("unsupported wake adapter for driver %q", clawType)
}
Expand All @@ -166,7 +160,7 @@ func scheduleWakeWarning(serviceName, clawType string, inv driver.Invocation) st
return ""
}
switch strings.TrimSpace(clawType) {
case "picoclaw", "nullclaw":
case "picoclaw":
return fmt.Sprintf("service %q: driver %q external scheduler wake does not support invoke.to=%q; firing direct agent message without delivery routing", serviceName, clawType, inv.To)
default:
return ""
Expand Down
4 changes: 2 additions & 2 deletions cmd/claw/schedule_manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func TestBuildScheduleManifestUsesServiceTimezoneWithoutCalendar(t *testing.T) {
manifest, err := buildScheduleManifest(&pod.Pod{Name: "ops"}, map[string]*driver.ResolvedClaw{
"bot": {
ServiceName: "bot",
ClawType: "nullclaw",
ClawType: "picoclaw",
Timezone: "America/New_York",
Invocations: []driver.Invocation{
{
Expand All @@ -134,7 +134,7 @@ func TestBuildScheduleManifestFallsBackToUTCWithoutServiceTimezone(t *testing.T)
manifest, err := buildScheduleManifest(&pod.Pod{Name: "ops"}, map[string]*driver.ResolvedClaw{
"bot": {
ServiceName: "bot",
ClawType: "nullclaw",
ClawType: "picoclaw",
Invocations: []driver.Invocation{
{
ID: "podjob01",
Expand Down
Loading
Loading