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
2 changes: 1 addition & 1 deletion .agents/skills/developing-vm-cli/references/vm-cli.md
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

119 changes: 58 additions & 61 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ so they don't get lost.

- [x] **Remove home directory mount as default** _(done: e770d69)_

- [ ] **Auto-commit mechanism for inner openclaw changes**
The agent inside the VM writes to `data/` (configs, workspace files,
etc.) but can't commit — git runs on the host. We need a way for the
inner openclaw to request a commit. Rough idea: the agent writes a
file like `data/.git-request` containing the commit message; a `fs`
watcher on the host picks it up, stages `data/`, commits with that
message, and removes the request file. Could be a long-running
background process or integrated into the CLI's manage mode.
- [x] **Auto-commit mechanism for inner openclaw changes** _(done: v0.9.0)_
Implemented as the checkpoint system: `claw checkpoint --message "reason"`
writes a signal file to `data/.checkpoint-request`; the host daemon's
`checkpoint-watch` task detects it, runs `git add data/ && git commit`,
and removes the file. The checkpoint capability also installs a skill
into the workspace so the agent knows how to use it.

- [x] **Headless / preconfigured provisioning (skip the wizard)** _(done: 14b93d1)_

Expand All @@ -39,22 +37,19 @@ so they don't get lost.
wizard or via config. Playwright + Chromium is the highest value
add — web access is a core capability gap right now.

- [ ] **Automate manual post-setup steps**
Things currently done by hand after onboarding that should be part of
the provisioning flow. Based on real usage:
- **Docker permissions** — add the openclaw user to the docker group
so the agent can run containers without sudo
- **Sandbox disabled** — for trusted single-user setups, disable the
openclaw sandbox. Needs a config set during post-onboard setup.
Should probably be a wizard option ("trusted environment?") since
it's a security trade-off.
- **Workspace on shared mount** — already done (we set
`agents.defaults.workspace` to `/mnt/project/data/workspace`)
- **Headless Chromium** — install and configure so the agent can
browse. Overlaps with the pre-installed tooling item above.
- **Heartbeat security reviews** — configure periodic security
review tasks. Needs investigation into how openclaw schedules
these (cron? built-in scheduler?).
- [ ] **Automate manual post-setup steps** _(partially done)_
Some items are now handled by the bootstrap flow; others remain.
- [x] **Sandbox disabled** — wizard option, bootstrap sets
`agents.defaults.sandbox.mode off` when configured
- [x] **Workspace on shared mount** — bootstrap sets
`agents.defaults.workspace /mnt/project/data/workspace`
- [ ] **Docker permissions** — add the openclaw user to the docker group
so the agent can run containers without sudo
- [ ] **Headless Chromium** — install and configure so the agent can
browse. Overlaps with the pre-installed tooling item above.
- [ ] **Heartbeat security reviews** — configure periodic security
review tasks. Needs investigation into how openclaw schedules
these (cron? built-in scheduler?).

- [ ] **Post-provision setup commands for optional services**
Allow configuring 1Password, Tailscale, etc. on an already-running VM.
Expand All @@ -68,13 +63,25 @@ so they don't get lost.
These reuse the same provisioning logic from the wizard steps but
can target an existing instance.

- [ ] **Manage mount points after VM creation**
Currently mounts are only configurable at create time via `config.mounts`.
After that, the only way to add or remove a mount is editing
`~/.lima/<vm>/lima.yaml` directly and restarting. clawctl should own this:
- `clawctl mount add <vm> <host-path> --mount-point <guest-path> [--writable]`
- `clawctl mount remove <vm> <guest-path>`
- `clawctl mount list <vm>`
Under the hood: edit the Lima yaml and restart the VM. Lima doesn't
support hot-adding mounts, so a restart is required — the command should
warn and confirm. Also update `clawctl.json` so the mount survives a
future rebuild.

- [x] **`clawctl restart` with health verification** _(done: v0.4.0)_

- [ ] **In-place upgrades**
When openclaw ships a new version, update the VM without rebuilding.
Re-run the idempotent provisioning scripts, restart the daemon. State
survives because it lives in `data/`. A simple
`clawctl upgrade <name>` command.
- [x] **In-place upgrades** _(done: v0.16.0)_
Implemented as `clawctl update`: checks for new releases, downloads
and self-replaces the host binary, then pushes the new `claw` binary
to all running VMs and runs `claw migrate` for capability migrations.
Stopped VMs get a `pendingClawUpdate` flag and are updated on next start.

- [ ] **Skill portability — make clawctl aware of the skills convention**
Openclaw already has a natural convention: each skill lives in
Expand All @@ -101,38 +108,28 @@ so they don't get lost.

Longer term: skill sharing between instances, maybe a registry.

- [ ] **VM-side CLI (`claw`) — agent tooling inside the VM**
Split the tooling into two CLIs: `clawctl` (host, VM lifecycle) and
`claw` (VM-side, agent tooling). Separate packages in a monorepo,
sharing code but independently built.

`claw` owns everything that happens _inside_ the VM:
- `claw bootstrap` — post-onboarding setup (daemon install, config set,
workspace init). Replaces the imperative shell commands in `bin/cli.tsx`.
- `claw doctor` — health checks beyond `openclaw doctor` (mount
verification, env vars, PATH, service status).
- `claw create skill` — scaffold a new skill directory with SKILL.md,
scripts/, package.json in the right structure.
- `claw update` — self-update the VM-side CLI (pulled from host mount
or downloaded).
- Future: any agent-facing commands (skill management, config, etc.)

**How it gets there:** Built at provisioning time (`bun build --compile`),
copied into the VM during provisioning. `clawctl upgrade` rebuilds and
pushes the new binary.

**Host→VM interface:** `clawctl` calls `claw` commands inside the VM
instead of raw `bash -lc` strings. `claw` returns structured output
(JSON or exit codes) so the host can parse reliably. This replaces the
current pattern of regex-parsing shell output.

**What this subsumes:** The host-side CLI proxy (`clawctl openclaw` / `oc`)
already exists. With `claw` on the VM side, it would become
`clawctl oc <command>` → `limactl shell ... claw <command>`. The proxy
logic is just dispatch, `claw` does the real work.

**Naming rationale:** `clawctl` = control plane (host), `claw` = the
tool itself (VM). Short and natural for interactive use inside the VM.
- [x] **VM-side CLI (`claw`) — agent tooling inside the VM** _(done: v0.8.0)_
Implemented as `@clawctl/vm-cli`. Commands: `claw provision <phase>`,
`claw doctor`, `claw checkpoint`, `claw migrate`. Built with
`bun build --compile` for linux-arm64, deployed into VM at
`/usr/local/bin/claw`. All commands return structured JSON. Host calls
claw via `limactl shell` instead of raw bash strings.
- [ ] **`claw create skill`** — scaffold a new skill directory (not yet implemented)

- [ ] **Adopt native OpenClaw installations into a clawctl VM**
Many users have OpenClaw running natively on their machine. `clawctl adopt`
would create a VM that takes over an existing native installation:
- Detect the native OpenClaw data dirs (state, config, workspace)
- Create a new VM with mounts pointing at the existing data
- Provision the VM (idempotent — packages already installed natively
get skipped)
- Stop the native daemon, start the VM-based one
- Optionally move data into the clawctl project directory layout

This is the general-purpose version of the one-off migration done for
the original Klaus VM (which was adopted from a pre-clawctl Lima setup).
The native case is harder because data paths vary and the native daemon
must be stopped cleanly.

---

Expand Down
32 changes: 32 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,38 @@ Trade-offs:

The eventual goal is to keep Ink active during onboarding by embedding the subprocess output in a virtual terminal surface. This would allow a contextual guidance sidebar with tips based on what the onboarding wizard is currently showing. Requires `node-pty` for PTY management and `xterm-headless` for ANSI parsing into a renderable screen buffer.

## CLI Command Conventions

### Instance resolution

Every command that targets an instance uses `requireInstance(opts)` from
host-core. It resolves the instance in this order:

1. Explicit `-i <name>` / `--instance <name>` flag
2. Local `.clawctl` context file (set by `clawctl use`)
3. Global context (`~/.config/clawctl/context.json`)
4. Error if none found

### Positional `[name]` argument

Commands that **only** target an instance (no other positional args)
offer `[name]` as a convenience positional:

```
clawctl status [name] # OK — no other positionals
clawctl start [name] # OK
clawctl mount list [name] # OK
```

Commands that have **other required positional arguments** must NOT use
`[name]` — Commander consumes the first positional as the optional name,
swallowing the real argument. Use `-i` or context resolution instead:

```
clawctl mount add <host-path> <guest-path> # No [name] — would eat <host-path>
clawctl mount remove <guest-path> # No [name] — would eat <guest-path>
```

## Error Handling

- Each step handles its own errors and displays them inline
Expand Down
48 changes: 48 additions & 0 deletions packages/cli/bin/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import {
runDaemonLogs,
runDaemonRun,
runUpdate,
runMountList,
runMountAdd,
runMountRemove,
} from "../src/commands/index.js";
import { ensureDaemon } from "@clawctl/daemon";
import { checkAndPromptUpdate } from "../src/update-hook.js";
Expand Down Expand Up @@ -170,6 +173,51 @@ program
await runUse(name, opts);
});

const mountCmd = program
.command("mount")
.description("Manage VM mount points")
.action(() => {
mountCmd.help();
});

mountCmd
.command("list [name]")
.description("List all mounts for an instance")
.option("-i, --instance <name>", "Instance to target")
.action(async (name: string | undefined, opts: { instance?: string }) => {
await runMountList(driver, { instance: opts.instance ?? name });
});

mountCmd
.command("add")
.description("Add a host directory mount to the VM")
.argument("<host-path>", "Host directory to mount")
.argument("<guest-path>", "Mount point inside the VM")
.option("-i, --instance <name>", "Instance to target")
.option("--writable", "Mount as read-write (default: read-only)")
.option("--no-restart", "Update config but don't restart the VM")
.showHelpAfterError(true)
.action(
async (
hostPath: string,
guestPath: string,
opts: { instance?: string; writable?: boolean; noRestart?: boolean },
) => {
await runMountAdd(driver, opts, hostPath, guestPath);
},
);

mountCmd
.command("remove")
.description("Remove a mount from the VM")
.argument("<guest-path>", "Mount point to remove")
.option("-i, --instance <name>", "Instance to target")
.option("--no-restart", "Update config but don't restart the VM")
.showHelpAfterError(true)
.action(async (guestPath: string, opts: { instance?: string; noRestart?: boolean }) => {
await runMountRemove(driver, opts, guestPath);
});

const daemonCmd = program.command("daemon").description("Manage the background daemon");

daemonCmd
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export {
runDaemonRun,
} from "./daemon.js";
export { runUpdate } from "./update.js";
export { runMountList, runMountAdd, runMountRemove } from "./mount.js";
Loading
Loading