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
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.

10 changes: 5 additions & 5 deletions docs/1password-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ OP_SERVICE_ACCOUNT_TOKEN=ops_your_token_here

```json
{
"services": {
"onePassword": {
"capabilities": {
"one-password": {
"serviceAccountToken": "env://OP_SERVICE_ACCOUNT_TOKEN"
}
}
Expand All @@ -154,8 +154,8 @@ in memory only -- never written to disk.

```json
{
"services": {
"onePassword": {
"capabilities": {
"one-password": {
"serviceAccountToken": "env://OP_SERVICE_ACCOUNT_TOKEN"
}
},
Expand All @@ -169,7 +169,7 @@ in memory only -- never written to disk.
}
```

Config files with `op://` references require `services.onePassword` to be
Config files with `op://` references require `capabilities["one-password"]` to be
configured (validation will reject configs with `op://` refs but no 1Password
service account).

Expand Down
73 changes: 60 additions & 13 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,62 @@ The split is intentional:

## Writing a capability

A capability is a `CapabilityDef` constant. Here's the structure:
A capability is a `CapabilityDef` constant. Optional capabilities declare a
`configDef` that drives config validation (Zod is derived automatically),
TUI form rendering, sidebar help, and secret sanitization — all from one
definition.

```typescript
import { defineCapabilityConfig } from "@clawctl/types";
import type { CapabilityDef } from "@clawctl/types";

// 1. Define the config type — this is the contract
type MyToolConfig = {
apiToken: string;
region?: "us" | "eu";
};

export const myTool: CapabilityDef = {
name: "my-tool",
label: "My Tool",
version: "1.0.0",
core: false, // true = always enabled
dependsOn: ["homebrew"], // runs after homebrew in same phase
enabled: (
config, // when to activate (non-core only)
) => "my-tool" in (config.capabilities ?? {}),

enabled: (config) => config.capabilities?.["my-tool"] !== undefined,

// 2. Unified config definition — replaces configSchema, formDef, secretFields
// defineCapabilityConfig<T>() validates field paths at compile time
configDef: defineCapabilityConfig<MyToolConfig>({
sectionLabel: "My Tool",
sectionHelp: { title: "My Tool", lines: ["Integration with My Tool service."] },
fields: [
{
path: "apiToken", // typed: must be keyof MyToolConfig
label: "API Token",
type: "password",
required: true,
secret: true, // stripped from clawctl.json, masked in TUI
placeholder: "mt-...",
help: { title: "API Token", lines: ["Your My Tool API token."] },
},
{
path: "region",
label: "Region",
type: "select",
defaultValue: "us",
options: [
{ label: "US", value: "us" },
{ label: "EU", value: "eu" },
],
},
],
summary: (v) => (v.apiToken ? `My Tool (${v.region ?? "us"})` : ""),
}),

// 3. Lifecycle hooks (VM-side provisioning)
hooks: {
"provision-tools": {
// hook key = phase or pre:/post: phase
execContext: "user", // "root" or "user"
execContext: "user",
steps: [
{
name: "my-tool-install",
Expand All @@ -103,7 +140,6 @@ export const myTool: CapabilityDef = {
},
],
doctorChecks: [
// optional health checks
{
name: "path-my-tool",
availableAfter: "provision-tools",
Expand All @@ -114,7 +150,6 @@ export const myTool: CapabilityDef = {
],
},
bootstrap: {
// post-onboard AGENTS.md section
execContext: "user",
steps: [
{
Expand All @@ -131,15 +166,27 @@ export const myTool: CapabilityDef = {
};
```

The user's config file uses the capabilities map:

```json
{
"capabilities": {
"my-tool": { "apiToken": "mt-abc123", "region": "eu" }
}
}
```

### Registration

After writing the module:

1. Export it from `packages/capabilities/src/index.ts`
2. Import it in `packages/vm-cli/src/capabilities/registry.ts` and add it
to `ALL_CAPABILITIES`
1. Export it from `packages/capabilities/src/index.ts` and add it to
`ALL_CAPABILITIES`
2. If the capability needs host-side setup (token validation, auth flows),
add a hook entry in `packages/host-core/src/capability-hooks.ts`

The runner discovers hooks automatically from the registry.
The TUI wizard, config validation, sidebar help, and secret sanitization
all derive from the `configDef` automatically — no other files to edit.

### Step functions

Expand Down
42 changes: 21 additions & 21 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ and `project` are required. Everything else is optional and has sensible default
"network": {
"forwardGateway": true,
"gatewayPort": 18789,
"gatewayToken": "my-secret-token",
"gatewayToken": "my-secret-token"
},
"capabilities": {
"one-password": {
"serviceAccountToken": "ops_..."
},
"tailscale": {
"authKey": "tskey-auth-...",
"mode": "serve"
}
},
"services": {
"onePassword": {
"serviceAccountToken": "ops_..."
}
},
"provider": {
"type": "anthropic",
"apiKey": "sk-ant-...",
Expand Down Expand Up @@ -107,23 +107,23 @@ VM resource allocation. Omit the entire section to use defaults.

Network and connectivity settings.

| Field | Type | Default | Description |
| ------------------- | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `forwardGateway` | boolean | `true` | Forward the gateway port from guest to host. Set `false` if using Tailscale only — the gateway is reachable over the tailnet. |
| `gatewayPort` | number | `18789` | Host-side port for the gateway forward. Must be 1024–65535. |
| `gatewayToken` | string | — | Gateway auth token. Auto-generated if not set. |
| `tailscale` | object | — | If present, connects the VM to your Tailscale network non-interactively. |
| `tailscale.authKey` | string | — | Tailscale auth key (`tskey-auth-...`). Generate one at [Tailscale Admin → Keys](https://login.tailscale.com/admin/settings/keys). |
| `tailscale.mode` | string | `"serve"` | Gateway mode: `"serve"` (HTTPS on tailnet), `"funnel"` (public HTTPS), or `"off"`. See [Tailscale Setup](tailscale-setup.md#gateway-integration). |
| Field | Type | Default | Description |
| ---------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `forwardGateway` | boolean | `true` | Forward the gateway port from guest to host. Set `false` if using Tailscale only — the gateway is reachable over the tailnet. |
| `gatewayPort` | number | `18789` | Host-side port for the gateway forward. Must be 1024–65535. |
| `gatewayToken` | string | — | Gateway auth token. Auto-generated if not set. |

## `services`
## `capabilities`

External service integrations. Presence of a section means "configure this service."
Capability integrations. Presence of a capability section means "configure this capability."

| Field | Type | Description |
| --------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `onePassword` | object | If present, validates and persists a 1Password service account token in the VM. Enables `op://` reference resolution. |
| `onePassword.serviceAccountToken` | string | 1Password service account token (`ops_...`) or `env://VAR_NAME` reference. Stored at `~/.openclaw/credentials/op-token` inside the VM. |
| Field | Type | Default | Description |
| ---------------------------------- | ------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `one-password` | object | — | If present, validates and persists a 1Password service account token in the VM. Enables `op://` reference resolution. |
| `one-password.serviceAccountToken` | string | — | 1Password service account token (`ops_...`) or `env://VAR_NAME` reference. Stored at `~/.openclaw/credentials/op-token` inside the VM. |
| `tailscale` | object | — | If present, connects the VM to your Tailscale network non-interactively. |
| `tailscale.authKey` | string | — | Tailscale auth key (`tskey-auth-...`). Generate one at [Tailscale Admin → Keys](https://login.tailscale.com/admin/settings/keys). |
| `tailscale.mode` | string | `"serve"` | Gateway mode: `"serve"` (HTTPS on tailnet), `"funnel"` (public HTTPS), or `"off"`. See [Tailscale Setup](tailscale-setup.md#gateway-integration). |

## `tools`

Expand Down Expand Up @@ -206,7 +206,7 @@ Telegram channel configuration (optional). Applied during bootstrap after onboar
String values in the config can use URI references instead of plaintext secrets:

- **`env://VAR_NAME`** — resolved from the host environment at config-load time. Bun auto-loads `.env` files.
- **`op://vault/item/field`** — resolved inside the VM via `op read` after 1Password setup. Requires `services.onePassword`.
- **`op://vault/item/field`** — resolved inside the VM via `op read` after 1Password setup. Requires `capabilities["one-password"]`.

This means configs with `op://` and `env://` references contain zero plaintext
secrets and can be safely committed to git. See
Expand Down
6 changes: 3 additions & 3 deletions docs/headless-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ All three modes run the same provisioning stages:
2. **Install Lima** — via Homebrew, if not already present
3. **Create and provision VM** — generate lima.yaml, boot Ubuntu 24.04, run provisioning
4. **Verify installed tools** — Node.js 22, Tailscale, Homebrew, 1Password CLI
5. **Set up 1Password** — if `services.onePassword` is configured
5. **Set up 1Password** — if `capabilities["one-password"]` is configured
6. **Resolve secrets** — if `op://` references are present in the config
7. **Connect Tailscale** — if `network.tailscale` is configured
7. **Connect Tailscale** — if `capabilities.tailscale` is configured
8. **Bootstrap gateway** — if `provider` is configured (runs `openclaw onboard --non-interactive`)
9. **Register instance** — write `clawctl.json` and update the instance registry

Expand Down Expand Up @@ -91,7 +91,7 @@ from the environment at load time:
"type": "anthropic",
"apiKey": "env://ANTHROPIC_API_KEY"
},
"network": {
"capabilities": {
"tailscale": {
"authKey": "env://TAILSCALE_AUTH_KEY"
}
Expand Down
43 changes: 40 additions & 3 deletions packages/capabilities/src/capabilities/one-password/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dedent from "dedent";
import { join } from "path";
import { PROJECT_MOUNT_POINT } from "@clawctl/types";
import { PROJECT_MOUNT_POINT, defineCapabilityConfig } from "@clawctl/types";
import type { CapabilityDef } from "@clawctl/types";
import {
provisionOpCli,
Expand All @@ -12,14 +12,51 @@ import { secretManagementSkillContent } from "./skill.js";

const SKILLS_DIR = join(PROJECT_MOUNT_POINT, "data", "workspace", "skills");

type OnePasswordConfig = {
serviceAccountToken: string;
};

export const onePassword: CapabilityDef = {
name: "one-password",
label: "1Password",
version: "1.0.0",
core: false,
dependsOn: ["homebrew"],
enabled: (config) =>
config.capabilities?.["one-password"] !== undefined || config.onePassword === true,
enabled: (config) => config.capabilities?.["one-password"] !== undefined,
configDef: defineCapabilityConfig<OnePasswordConfig>({
sectionLabel: "1Password",
sectionHelp: {
title: "1Password",
lines: [
"Inject secrets via op:// refs.",
"Installs the op CLI and skills.",
"",
"Requires a service account token.",
],
},
fields: [
{
path: "serviceAccountToken",
label: "SA Token",
type: "password",
required: true,
secret: true,
placeholder: "1Password service account token",
help: {
title: "1Password Token",
lines: [
"Service account token for the",
"1Password CLI.",
"",
"Create one at:",
" my.1password.com/developer",
" /service-accounts",
],
},
},
],
summary: (v) => (v.serviceAccountToken ? "1Password" : ""),
}),
hooks: {
// Phase 1: Install the op CLI binary
"provision-tools": {
Expand Down
62 changes: 60 additions & 2 deletions packages/capabilities/src/capabilities/tailscale.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,73 @@
import { defineCapabilityConfig } from "@clawctl/types";
import type { CapabilityDef } from "@clawctl/types";

const TAILSCALE_INSTALL_URL = "https://tailscale.com/install.sh";

type TailscaleConfig = {
authKey: string;
mode?: "off" | "serve" | "funnel";
};

export const tailscale: CapabilityDef = {
name: "tailscale",
label: "Tailscale",
version: "1.0.0",
core: false,
dependsOn: ["system-base"],
enabled: (config) =>
config.capabilities?.["tailscale"] !== undefined || config.tailscale === true,
enabled: (config) => config.capabilities?.["tailscale"] !== undefined,
configDef: defineCapabilityConfig<TailscaleConfig>({
sectionLabel: "Tailscale",
sectionHelp: {
title: "Tailscale",
lines: [
"Connect the VM to a Tailscale",
"network for secure remote access.",
"",
"Requires a pre-authenticated key.",
],
},
fields: [
{
path: "authKey",
label: "Auth Key",
type: "password",
required: true,
secret: true,
placeholder: "tskey-auth-...",
help: {
title: "Tailscale Auth Key",
lines: [
"Pre-authenticated key from your",
"Tailscale admin panel.",
"",
"Generate at:",
" login.tailscale.com/admin",
" /settings/keys",
],
},
},
{
path: "mode",
label: "Mode",
type: "select",
defaultValue: "serve",
options: [
{ label: "serve", value: "serve" },
{ label: "funnel", value: "funnel" },
{ label: "off", value: "off" },
],
help: {
title: "Tailscale Mode",
lines: [
"serve — HTTPS on your tailnet",
"funnel — public access via Tailscale",
"off — install but don't expose",
],
},
},
],
summary: (v) => (v.authKey ? `Tailscale (${v.mode ?? "serve"})` : ""),
}),
hooks: {
"provision-system": {
execContext: "root",
Expand Down
Loading
Loading