@clawops/cli 2.0.2 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +877 -804
- package/dist/{apply-WOUTUUQH.js → apply-HJ5GAKHW.js} +7 -7
- package/dist/{automation-UKRM3BD2.js → automation-MKCJ46I6.js} +1 -1
- package/dist/{bootstrap-HNGKY6WW.js → bootstrap-NN647RCY.js} +2 -2
- package/dist/{chunk-HSPHXG6W.js → chunk-3Z5TDXCJ.js} +2 -2
- package/dist/{chunk-CX5SL5HP.js → chunk-64PONSA5.js} +10 -1
- package/dist/{chunk-LEVIW2PV.js → chunk-BELTQGHT.js} +16 -2
- package/dist/{preflight-MWWM5JT3.js → chunk-BTW7NHOH.js} +3 -2
- package/dist/{chunk-A2I76FTA.js → chunk-ELU3XQEA.js} +1 -1
- package/dist/{chunk-5TXCUOZV.js → chunk-GFUK6KDR.js} +9 -9
- package/dist/{chunk-RATEIAFU.js → chunk-OFTAXLOY.js} +127 -13
- package/dist/{chunk-WTZ2CT35.js → chunk-QDNFADJI.js} +12 -5
- package/dist/{chunk-ZMMRIWFS.js → chunk-R3DTA35B.js} +22 -3
- package/dist/{preflight-EF52S3WW.js → chunk-TC2VYPLH.js} +3 -2
- package/dist/chunk-X56CQ6GB.js +157 -0
- package/dist/{chunk-BSLIYB5Q.js → chunk-XFN3YDZE.js} +1 -1
- package/dist/{chunk-SDZKCICH.js → chunk-YEBFVLOE.js} +12 -9
- package/dist/cli.js +169 -83
- package/dist/{context-M4JBBIAV.js → context-7VI3R6EK.js} +4 -4
- package/dist/flows-RI7FGHAM.js +120 -0
- package/dist/{generate-J6MIJGII.js → generate-JNJ63ZYZ.js} +6 -6
- package/dist/{harden-72JMRT4E.js → harden-3KKCR7BL.js} +541 -3
- package/dist/{mcp-wire-NTGL5ITF.js → mcp-wire-B57DUNOD.js} +10 -10
- package/dist/{package-3A6BOALU.js → package-SAUDFQLG.js} +46 -10
- package/dist/preflight-7W23GP4R.js +19 -0
- package/dist/preflight-IE2YWBAC.js +13 -0
- package/dist/private-only-DIA26I2E.js +74 -0
- package/dist/{server-RWM2JI3D.js → server-RHON6QMN.js} +10 -10
- package/dist/{stack-config-R4SPSUQZ.js → stack-config-LUTS2XAD.js} +2 -2
- package/dist/{state-PRBIIN7I.js → state-AOAYLYY5.js} +2 -2
- package/dist/{store-SDUR52Z5.js → store-OVB3DAJR.js} +1 -1
- package/dist/tailscale-cutover-6YP2QKFM.js +113 -0
- package/dist/{validate-YVI3BJ55.js → validate-JXKN5WD6.js} +1 -1
- package/package.json +38 -8
- package/spec/deploy-plan.schema.json +16 -1
- package/spec/mcp-tools.yaml +67 -9
package/README.md
CHANGED
|
@@ -3,1068 +3,1141 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@clawops/cli)
|
|
4
4
|
[](https://www.npmjs.com/package/@clawops/cli)
|
|
5
5
|
|
|
6
|
-
MCP-native infrastructure ops for OpenClaw
|
|
6
|
+
MCP-native infrastructure ops for OpenClaw, with read-only mode, destructive-action confirmation, and audit logs built in.
|
|
7
7
|
|
|
8
8
|
**clawops** is a CLI and [MCP](https://modelcontextprotocol.io/) server for deploying and operating
|
|
9
9
|
self-hosted [OpenClaw](https://github.com/openclaw/openclaw) instances. Provision on AWS, GCP,
|
|
10
|
-
Azure, or any Linux VM
|
|
10
|
+
Azure, or any Linux VM, then manage day-to-day operations from the terminal, or let Claude Code
|
|
11
11
|
and Cursor drive them through typed MCP tools with explicit safety controls.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
A patch release, and a large one: in 2.0.0 no cloud deploy succeeded by any path. Every item
|
|
18
|
-
below is a fix or an addition in 2.0.1. The reasoning behind each one is in its commit message,
|
|
19
|
-
and the decisions that came out of them are in [`docs/decisions/`](docs/decisions/).
|
|
20
|
-
|
|
21
|
-
### Deploying to a cloud
|
|
22
|
-
|
|
23
|
-
- `clawops plan` → `clawops apply` provisions a cloud stack and deploys OpenClaw onto it.
|
|
24
|
-
- `clawops up` deploys to AWS, GCP and Azure, running the same path as `plan` → `apply`.
|
|
25
|
-
- clawops installs the Pulumi CLI it needs into `~/.clawops/.pulumi-cli`, or uses a compatible
|
|
26
|
-
one already on `$PATH` ([ADR 0010](docs/decisions/0010-pulumi-cli-bootstrap.md)).
|
|
27
|
-
- clawops creates and stores the passphrase its state backend requires
|
|
28
|
-
([ADR 0011](docs/decisions/0011-state-passphrase.md)).
|
|
29
|
-
- `clawops plan` takes `--ssh-cidr`, `--gateway-cidr` and `--publish-gateway`, and `apply`
|
|
30
|
-
passes them to the cloud firewall. `auto` resolves this machine's address.
|
|
31
|
-
- `clawops plan` stops, and names the cause, when it cannot open the state backend.
|
|
32
|
-
- `--instance-type` takes a clawops alias (`micro`–`gpu`) or a machine type your cloud names
|
|
33
|
-
itself, and the plan records the concrete type.
|
|
34
|
-
- Deploys pin the account they were planned against: `gcp:project` on GCP,
|
|
35
|
-
`azure-native:subscriptionId` on Azure.
|
|
36
|
-
|
|
37
|
-
### Checking the account before you spend
|
|
38
|
-
|
|
39
|
-
- `clawops doctor --provider <cloud>` checks one cloud's credentials and account setup, with or
|
|
40
|
-
without a stack. `--instance-type` points the size check at the size you are deploying.
|
|
41
|
-
- **AWS** — the account the credentials resolve to, the state bucket, and whether the instance
|
|
42
|
-
type is offered in the region.
|
|
43
|
-
- **GCP** — the project, the APIs a deploy needs, and the state bucket.
|
|
44
|
-
- **Azure** — the subscription, the resource providers, the VM size, and the azblob credentials
|
|
45
|
-
Pulumi authenticates with.
|
|
46
|
-
- `clawops setup` runs the same checks and offers to fix what it safely can — enabling an API,
|
|
47
|
-
creating a state bucket with versioning on and public access blocked — naming the change
|
|
48
|
-
before making it.
|
|
49
|
-
- A check clawops could not perform reports as a warning naming the error, rather than as a
|
|
50
|
-
pass or a failure.
|
|
51
|
-
- Azure accepts your `az login`; a service principal is no longer required.
|
|
15
|
+
**What's new:** [2.0.1 and 2.0 release notes](#whats-new-in-201) below, and the full history
|
|
16
|
+
in [CHANGELOG.md](CHANGELOG.md).
|
|
52
17
|
|
|
53
|
-
|
|
18
|
+
---
|
|
54
19
|
|
|
55
|
-
|
|
56
|
-
for a name or writing a placeholder
|
|
57
|
-
([ADR 0012](docs/decisions/0012-state-bucket-naming.md)).
|
|
58
|
-
- A name you type instead is checked against the rules of the cloud that has to accept it.
|
|
59
|
-
- `clawops init` keeps the stacks already in your config.
|
|
60
|
-
- `clawops init` generates an SSH key that clawops can read. If you ran `init` before this
|
|
61
|
-
release, `clawops doctor` will tell you whether yours is usable.
|
|
62
|
-
- `gcloud config set project` is honoured.
|
|
63
|
-
- The setup wizard writes model configuration that OpenClaw accepts, and installs the plugin
|
|
64
|
-
your chosen provider needs.
|
|
65
|
-
- Amazon Bedrock works: the right transport, and an inference profile resolved against your
|
|
66
|
-
deployment region and recorded in the plan. Needs `bedrock:ListInferenceProfiles`.
|
|
20
|
+
## Who this is for
|
|
67
21
|
|
|
68
|
-
|
|
22
|
+
- **OpenClaw users** who want the simplest path to self-hosting across cloud or local VMs, with
|
|
23
|
+
reliable deploy, status checks, logs, backups, and upgrades in a single CLI.
|
|
24
|
+
- **Claude Code / Cursor / MCP users** looking for a real-world reference implementation of safe
|
|
25
|
+
infrastructure operations through MCP. Typed tool schemas, read-only mode, destructive-action
|
|
26
|
+
confirmation, and audit logs.
|
|
27
|
+
- **Self-hosted AI and local-first developers** who want to run their own AI assistant without
|
|
28
|
+
committing to Kubernetes, a managed SaaS platform, or a single cloud provider.
|
|
69
29
|
|
|
70
|
-
|
|
71
|
-
- `apply` reports progress as it goes instead of going quiet for minutes.
|
|
72
|
-
- A deploy that times out prints what the host was doing, from its bootstrap log.
|
|
73
|
-
- A host still installing Docker is treated as still booting rather than as a failed deploy.
|
|
30
|
+
---
|
|
74
31
|
|
|
75
|
-
|
|
32
|
+
## What clawops does
|
|
76
33
|
|
|
77
|
-
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
|
|
34
|
+
- Provisions and tears down OpenClaw infrastructure on **AWS, GCP, Azure, and local VMs** using
|
|
35
|
+
the Pulumi Automation API. You do not install Pulumi; clawops installs the CLI it needs into
|
|
36
|
+
`~/.clawops/.pulumi-cli` on first use.
|
|
37
|
+
- Manages day-to-day operations: status, logs, SSH, tunnels, config, agents, gateway, backups.
|
|
38
|
+
- Exposes every operation as a **typed MCP tool** so AI agents can drive ops safely.
|
|
39
|
+
- Enforces a **plan → review → apply** discipline for cloud deployments.
|
|
40
|
+
- Emits **JSON output everywhere** (`--json`) for scripting and automation.
|
|
41
|
+
- Never stores cloud credentials. Reads them from your environment's existing CLI profiles.
|
|
84
42
|
|
|
85
|
-
|
|
43
|
+
## What clawops does not do
|
|
86
44
|
|
|
87
|
-
-
|
|
88
|
-
|
|
89
|
-
-
|
|
90
|
-
|
|
45
|
+
- **No high availability or clustering.** Optimized for single-node deployments.
|
|
46
|
+
- **No Kubernetes.** It deploys to VMs, not container orchestration platforms.
|
|
47
|
+
- **No OpenClaw skill/agent authoring.** clawops manages infrastructure; what runs on it is up to
|
|
48
|
+
you and OpenClaw.
|
|
49
|
+
- **No TLS or domain automation** (yet). Bring your own reverse proxy or see
|
|
50
|
+
[`docs/limitations.md`](docs/limitations.md) for the manual path.
|
|
51
|
+
- **No credential storage.** Cloud credentials must be configured in your environment before using
|
|
52
|
+
clawops. They are never written to `~/.clawops/config.json`.
|
|
53
|
+
- **No native Windows.** WSL2 is fully supported; see [`docs/support-matrix.md`](docs/support-matrix.md).
|
|
91
54
|
|
|
92
|
-
|
|
55
|
+
---
|
|
93
56
|
|
|
94
|
-
|
|
95
|
-
`<= 2026.7.1-2` under the `legacy` dist-tag until **2027-03-31**:
|
|
57
|
+
## Quick Start
|
|
96
58
|
|
|
97
59
|
```bash
|
|
98
|
-
npm install -g @clawops/cli
|
|
99
|
-
|
|
60
|
+
npm install -g @clawops/cli
|
|
61
|
+
clawops setup
|
|
100
62
|
```
|
|
101
63
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
about *how clawops behaves*.
|
|
105
|
-
|
|
106
|
-
### Your deployment keeps its state
|
|
64
|
+
`clawops setup` is an interactive wizard that gets OpenClaw running in about 2 minutes. It
|
|
65
|
+
handles everything in one flow, no config files to write by hand, no commands to memorize.
|
|
107
66
|
|
|
108
|
-
|
|
109
|
-
state at all, so **every restart destroyed them** — and a restart is what `gateway restart`,
|
|
110
|
-
`gateway update` and `config set` all do.
|
|
67
|
+
### What the wizard does
|
|
111
68
|
|
|
112
|
-
|
|
113
|
-
default location, holding the config, the database and any provider plugins. Existing
|
|
114
|
-
deployments migrate on the next `up`/`apply`.
|
|
69
|
+
**Step 1. Choose a deployment target**
|
|
115
70
|
|
|
116
|
-
|
|
71
|
+
Pick an existing server you can SSH into (Linux or macOS), or a new cloud VM on AWS, GCP, or
|
|
72
|
+
Azure. Cloud deployments walk you through authenticating with the provider CLI if you aren't
|
|
73
|
+
already signed in.
|
|
117
74
|
|
|
118
|
-
|
|
119
|
-
flowchart TD
|
|
120
|
-
A["clawops plan"] --> B{"config valid<br/>against OpenClaw schema?"}
|
|
121
|
-
B -- no --> B1["refuse — plan is still<br/>a file you can edit"]
|
|
122
|
-
B -- yes --> C["clawops apply"]
|
|
123
|
-
C --> D{"OpenClaw version<br/>in supported range?"}
|
|
124
|
-
D -- no --> D1["refuse — names<br/>@clawops/cli@legacy"]
|
|
125
|
-
D -- yes --> E["provision host"]
|
|
126
|
-
E --> F["state dir, owned 1000:1000<br/>migrate any pre-2.0 config"]
|
|
127
|
-
F --> G["write config<br/>validated before writing"]
|
|
128
|
-
G --> H["install provider plugins<br/>while egress exists"]
|
|
129
|
-
H --> I["start gateway"]
|
|
130
|
-
I --> J{"/startupz says started?"}
|
|
131
|
-
J -- no --> J1["fail with the reason"]
|
|
132
|
-
J -- yes --> K{"configured providers<br/>all loaded?"}
|
|
133
|
-
K -- no --> K1["warn — healthy gateway,<br/>missing model backend"]
|
|
134
|
-
K -- yes --> L["done"]
|
|
135
|
-
```
|
|
75
|
+
**Step 2. Pick an LLM provider**
|
|
136
76
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"started" was inferred from `docker run` exiting 0.
|
|
77
|
+
Choose from Anthropic, OpenAI, Amazon Bedrock, Ollama, or others. The wizard prompts for your
|
|
78
|
+
API key and saves it locally (in `~/.clawops/secrets/`, chmod 600), it is never sent anywhere
|
|
79
|
+
except to OpenClaw on the target host when the config is applied.
|
|
141
80
|
|
|
142
|
-
|
|
81
|
+
**Step 3. Add chat integrations (optional)**
|
|
143
82
|
|
|
144
|
-
|
|
145
|
-
|
|
83
|
+
Select any combination of Discord, Telegram, Slack, WhatsApp, or Teams. The wizard collects each
|
|
84
|
+
integration's bot token the same way as the API key. Paste it in, reference an env var, or point
|
|
85
|
+
to a file.
|
|
146
86
|
|
|
147
|
-
|
|
148
|
-
flowchart TD
|
|
149
|
-
A["clawops gateway update X"] --> B{"X in supported range?"}
|
|
150
|
-
B -- no --> B1["refuse before pulling"]
|
|
151
|
-
B -- yes --> C["docker pull X"]
|
|
152
|
-
C --> D["snapshot state database"]
|
|
153
|
-
D -- cannot snapshot --> D1["refuse — no rollback point"]
|
|
154
|
-
D --> E{"target release understands<br/>this schema?"}
|
|
155
|
-
E -- no --> E1["refuse — downgrade across<br/>a schema boundary"]
|
|
156
|
-
E -- yes --> F["swap container"]
|
|
157
|
-
F --> G{"/startupz says started?"}
|
|
158
|
-
G -- yes --> H["done"]
|
|
159
|
-
G -- no --> I["one-shot doctor --fix<br/>in a throwaway container"]
|
|
160
|
-
I --> J["re-run, re-gate"]
|
|
161
|
-
J -- started --> K["done — reported as repaired"]
|
|
162
|
-
J -- still not --> L["roll back to previous image"]
|
|
163
|
-
L -- started --> M["rolled back, reason reported"]
|
|
164
|
-
L -- still not --> N["failed — snapshot path named"]
|
|
165
|
-
```
|
|
87
|
+
**Step 4. Wire your AI editor**
|
|
166
88
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
89
|
+
Select which AI apps should have access to clawops. Claude Desktop, Claude Code, Cursor,
|
|
90
|
+
Windsurf, VS Code, and Zed are all supported. The wizard writes an MCP server entry into each
|
|
91
|
+
app's config file using the absolute binary path so the app can launch it independently.
|
|
170
92
|
|
|
171
|
-
|
|
93
|
+
**Step 5. Deploy**
|
|
172
94
|
|
|
173
|
-
|
|
174
|
-
|
|
95
|
+
The wizard bootstraps OpenClaw on the target host over SSH (installs Docker, pulls the image,
|
|
96
|
+
starts the container), applies your LLM and integration config, generates a gateway auth token,
|
|
97
|
+
and prints a direct dashboard URL:
|
|
175
98
|
|
|
176
|
-
```mermaid
|
|
177
|
-
flowchart LR
|
|
178
|
-
A["gateway restart"] --> B["read current image"]
|
|
179
|
-
B -- no container --> B1["refuse — nothing to reuse.<br/>latest and stable point at 2.0"]
|
|
180
|
-
B --> C["read current publish scope"]
|
|
181
|
-
C --> D["recreate with the same<br/>version and reachability"]
|
|
182
|
-
D --> E{"/startupz says started?"}
|
|
183
|
-
E -- no --> E1["fail with the reason"]
|
|
184
|
-
E -- yes --> F["done"]
|
|
185
99
|
```
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
```mermaid
|
|
190
|
-
flowchart TD
|
|
191
|
-
A["clawops migrate"] --> B{"1.x container running?"}
|
|
192
|
-
B -- no --> B1["nothing to rescue — state was<br/>already lost to an earlier restart"]
|
|
193
|
-
B -- yes --> C["verified backup, inside the running container"]
|
|
194
|
-
C -- "backup fails" --> C1["refused — nothing touched"]
|
|
195
|
-
C --> D["extract state from the RUNNING container"]
|
|
196
|
-
D --> E["chown 1000:1000"]
|
|
197
|
-
E --> F["stop and remove 1.x"]
|
|
198
|
-
F --> G["synthesise a valid 2.0 config"]
|
|
199
|
-
G --> H["start 2.0 with the state directory"]
|
|
200
|
-
H --> I{"/startupz started?"}
|
|
201
|
-
I -- "no — schema still migrating" --> J["restart once"]
|
|
202
|
-
J --> K{"started?"}
|
|
203
|
-
K -- no --> K1["failed — points at the backup"]
|
|
204
|
-
K --> L["report"]
|
|
205
|
-
I -- yes --> L
|
|
206
|
-
L --> M["what carried over,<br/>device identity, config to review"]
|
|
100
|
+
✔ All done! OpenClaw is running.
|
|
101
|
+
ℹ Open dashboard: http://192.168.1.50:18789?token=<your-token>
|
|
102
|
+
ℹ Token saved to ~/.clawops/secrets/GATEWAY_TOKEN_my-stack
|
|
207
103
|
```
|
|
208
104
|
|
|
209
|
-
|
|
105
|
+
**Prerequisites:** Node.js ≥ 22, an SSH key, and either an SSH-reachable Linux/macOS host or a
|
|
106
|
+
cloud account with CLI credentials configured (`aws configure`, `gcloud auth login`, or `az login`).
|
|
107
|
+
You do not need Pulumi. The first cloud deployment installs the CLI clawops drives into
|
|
108
|
+
`~/.clawops/.pulumi-cli` and says so while it does.
|
|
210
109
|
|
|
211
|
-
|
|
212
|
-
mounted none — so stopping first destroys what the migration came to save.
|
|
110
|
+
For a full narrated walkthrough with example output, see [`docs/demo-script.md`](docs/demo-script.md).
|
|
213
111
|
|
|
214
|
-
|
|
215
|
-
clawops mounted was read by nothing. Your old settings are reported as *intent to review*,
|
|
216
|
-
never applied blindly — their channel blocks would not validate against 2.0 anyway.
|
|
112
|
+
---
|
|
217
113
|
|
|
218
|
-
|
|
219
|
-
it as pending. `migrate` waits for the second rather than declaring success early.
|
|
114
|
+
### Manual setup, existing server
|
|
220
115
|
|
|
221
|
-
If you
|
|
222
|
-
state is already gone — nothing was mounted to survive the container replacement. `migrate`
|
|
223
|
-
says so plainly rather than pretending to rescue it.
|
|
116
|
+
If you prefer step-by-step control, or are adding clawops to an already-running deployment:
|
|
224
117
|
|
|
225
|
-
|
|
118
|
+
```bash
|
|
119
|
+
npm install -g @clawops/cli
|
|
226
120
|
|
|
227
|
-
|
|
228
|
-
restore subcommand to call. 2.0 does, and clawops delegates to it:
|
|
121
|
+
clawops doctor # verify environment
|
|
229
122
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
B --> C["openclaw backup restore --target <staging>"]
|
|
234
|
-
C -- "target not empty" --> C1["refused by OpenClaw"]
|
|
235
|
-
C --> D["archive verified, expanded<br/>into a fresh directory"]
|
|
236
|
-
D --> E["warnings printed verbatim<br/>time travel, channel relink,<br/>approvals, plugins"]
|
|
237
|
-
E --> F["nothing activated"]
|
|
238
|
-
F --> G["you stop the gateway, swap the<br/>state dir, restart, re-apply"]
|
|
123
|
+
clawops init --provider local --host 192.168.1.50 --user ubuntu --key-path ~/.ssh/id_ed25519
|
|
124
|
+
clawops up # installs Docker + OpenClaw over SSH
|
|
125
|
+
clawops status
|
|
239
126
|
```
|
|
240
127
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
`node_modules`, so a restored deployment starts without its model providers — looking
|
|
244
|
-
healthy while doing it.
|
|
128
|
+
See [`docs/examples/local-vm.md`](docs/examples/local-vm.md) for SSH prerequisites, firewall
|
|
129
|
+
setup, and troubleshooting.
|
|
245
130
|
|
|
246
|
-
|
|
247
|
-
`secret_store_entries`, `worker_environment_credentials`, `device_auth_tokens` — unencrypted.
|
|
248
|
-
clawops now writes it `0600` locally; it previously used the default `0644`.
|
|
131
|
+
### Manual setup, cloud (AWS)
|
|
249
132
|
|
|
250
|
-
|
|
133
|
+
```bash
|
|
134
|
+
npm install -g @clawops/cli
|
|
251
135
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
Configuring one that is not bundled, without installing it, produces a gateway that starts,
|
|
255
|
-
reports healthy, and has no model backend.
|
|
136
|
+
# Requires AWS credentials in your environment (AWS_PROFILE or ~/.aws/credentials)
|
|
137
|
+
clawops init --provider aws
|
|
256
138
|
|
|
257
|
-
|
|
139
|
+
# Edit ~/.clawops/config.json: set stateUrl to your S3 bucket
|
|
258
140
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
Downloading plugin @openclaw/deepseek-provider@2026.9.2 from ClawHub…
|
|
262
|
-
Installed plugin: deepseek
|
|
141
|
+
clawops plan --provider aws --stack default --ssh-cidr auto --out /tmp/plan.json
|
|
142
|
+
clawops apply /tmp/plan.json
|
|
263
143
|
```
|
|
264
144
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
145
|
+
`--ssh-cidr auto` allows SSH from this machine's public IP, resolved while the plan is
|
|
146
|
+
generated and written into it. Without it the plan allows no ingress at all and nothing,
|
|
147
|
+
including clawops, will be able to connect.
|
|
268
148
|
|
|
269
|
-
|
|
270
|
-
fetch failed | getaddrinfo EAI_AGAIN clawhub.ai | EAI_AGAIN
|
|
271
|
-
```
|
|
149
|
+
---
|
|
272
150
|
|
|
273
|
-
|
|
274
|
-
while a configured provider is missing. [Required outbound access](docs/security/egress.md)
|
|
275
|
-
lists every destination and when it is needed.
|
|
151
|
+
## Connect an AI editor
|
|
276
152
|
|
|
277
|
-
|
|
153
|
+
The `setup` wizard handles this automatically (Step 4). To wire or re-wire editors at any time:
|
|
278
154
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
they are really installed:
|
|
282
|
-
|
|
283
|
-
```
|
|
284
|
-
[clawops] warning: the gateway is running, but these configured channels are not installed:
|
|
285
|
-
discord. They will never connect.
|
|
155
|
+
```bash
|
|
156
|
+
clawops mcp install
|
|
286
157
|
```
|
|
287
158
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
Channel plugins are pinned to the supported runtime. The current `latest` does not install on
|
|
293
|
-
it: `plugin "discord" requires plugin API >=2026.9.3, but this OpenClaw runtime exposes
|
|
294
|
-
2026.9.2` — the same drift that forced version pins on model providers.
|
|
295
|
-
|
|
296
|
-
Telegram needs nothing installed: it ships in the image.
|
|
159
|
+
This opens the same interactive checkbox used in the wizard, select Claude Desktop, Claude Code,
|
|
160
|
+
Cursor, Windsurf, VS Code, or Zed and clawops writes the MCP entry into each app's config using
|
|
161
|
+
the correct absolute binary path.
|
|
297
162
|
|
|
298
|
-
|
|
163
|
+
To add the entry manually instead, paste this into your editor's MCP config:
|
|
299
164
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"mcpServers": {
|
|
168
|
+
"clawops": {
|
|
169
|
+
"command": "npx",
|
|
170
|
+
"args": ["-y", "@clawops/cli", "mcp", "serve", "--read-only"]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
304
175
|
|
|
305
|
-
|
|
306
|
-
|
|
176
|
+
That form needs nothing on `$PATH` and is what a directory or an installer will copy. If you
|
|
177
|
+
would rather point at the binary you already have, use its absolute path — the output of
|
|
178
|
+
`which clawops` — with the same arguments:
|
|
307
179
|
|
|
308
|
-
|
|
309
|
-
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"mcpServers": {
|
|
183
|
+
"clawops": {
|
|
184
|
+
"command": "/path/to/clawops",
|
|
185
|
+
"args": ["mcp", "serve", "--read-only"]
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
310
190
|
|
|
311
|
-
|
|
191
|
+
Either way, pass the arguments. `mcp serve` is what speaks the protocol, and an explicit config
|
|
192
|
+
is one that still reads clearly a year later. clawops does not strand a client that omits them:
|
|
193
|
+
run with no command at all and a pipe on stdin — how every MCP client starts a server — and it
|
|
194
|
+
starts `mcp serve`, saying so on stderr. Typed at a terminal, `clawops` still prints help.
|
|
195
|
+
Config file locations:
|
|
312
196
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
197
|
+
| App | Path |
|
|
198
|
+
|---|---|
|
|
199
|
+
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
200
|
+
| Claude Desktop (Linux) | `~/.config/Claude/claude_desktop_config.json` |
|
|
201
|
+
| Claude Code | `~/.claude.json` |
|
|
202
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
203
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
|
|
204
|
+
| VS Code (macOS) | `~/Library/Application Support/Code/User/mcp.json` |
|
|
205
|
+
| VS Code (Linux) | `~/.config/Code/User/mcp.json` |
|
|
206
|
+
| Zed | `~/.config/zed/settings.json` (key: `context_servers`) |
|
|
316
207
|
|
|
317
|
-
|
|
208
|
+
**Start with `--read-only`**. It enables status, logs, config reads, and diagnostics while
|
|
209
|
+
blocking mutations. Remove it only after reviewing
|
|
210
|
+
[`docs/security/mcp-safety.md`](docs/security/mcp-safety.md).
|
|
318
211
|
|
|
319
|
-
`
|
|
320
|
-
|
|
321
|
-
restart changes neither the version nor who can reach it.
|
|
212
|
+
Destructive tools (`clawops_destroy`, `clawops_up`, `clawops_config_set`, etc.) require explicit
|
|
213
|
+
confirmation before executing, they will never run silently.
|
|
322
214
|
|
|
323
|
-
|
|
215
|
+
For HTTP mode setup see [`docs/mcp/`](docs/mcp/).
|
|
324
216
|
|
|
325
|
-
|
|
326
|
-
`clawops tunnel` or a reverse proxy on the host.
|
|
217
|
+
---
|
|
327
218
|
|
|
328
|
-
|
|
329
|
-
plaintext HTTP dashboard — token in the URL — was opened to your whole shell-access network
|
|
330
|
-
as a side effect of one unrelated answer. To bind all interfaces deliberately, set
|
|
331
|
-
`network.publishGateway: "all"`.
|
|
219
|
+
## Day-to-day operations
|
|
332
220
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
221
|
+
```bash
|
|
222
|
+
clawops status # Stack outputs: IP, gateway URL, SSH info
|
|
223
|
+
clawops logs -f # Tail OpenClaw logs over SSH
|
|
224
|
+
clawops ssh # Interactive SSH session
|
|
225
|
+
clawops ssh --command "docker ps"
|
|
336
226
|
|
|
337
|
-
|
|
227
|
+
clawops config get maxAgents
|
|
228
|
+
clawops config set maxAgents 8
|
|
338
229
|
|
|
339
|
-
|
|
340
|
-
with HTML**:
|
|
230
|
+
clawops tunnel # Port-forward gateway UI to localhost
|
|
341
231
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
/health-typo 200 text/html <!doctype html>…
|
|
232
|
+
clawops destroy --yes # Destroy cloud-provider stack
|
|
233
|
+
clawops down --yes # Destroy local-provider stack
|
|
345
234
|
```
|
|
346
235
|
|
|
347
|
-
|
|
348
|
-
was listening on the port, not that the gateway was healthy. Probes now read the response
|
|
349
|
-
body, and the restart gate uses `/startupz` rather than liveness — after a restart the
|
|
350
|
-
process listens long before startup finishes.
|
|
236
|
+
---
|
|
351
237
|
|
|
352
|
-
|
|
238
|
+
## Commands
|
|
353
239
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
`
|
|
357
|
-
|
|
358
|
-
|
|
240
|
+
| Command | Description |
|
|
241
|
+
|---|---|
|
|
242
|
+
| `setup` | First-run wizard: guided LLM, integrations, and deploy-plan generation |
|
|
243
|
+
| `init` | Register a stack in `~/.clawops/config.json` without provisioning. Additive, existing stacks are kept; `--force` is needed only to overwrite one |
|
|
244
|
+
| `up` | Provision or update stack (`--dry-run` for preview, `--gateway-port` for a non-default port) |
|
|
245
|
+
| `down` | Destroy local-provider stack (requires `--yes`; `--dry-run` shows current outputs) |
|
|
246
|
+
| `destroy` | Destroy cloud-provider stack with confirmation prompt (`--dry-run` shows current outputs) |
|
|
247
|
+
| `status` | Show stack outputs: IP, gateway URL, region, provisioned time |
|
|
248
|
+
| `plan` | Generate a deploy-plan JSON artifact (dry-run safe). `--ssh-cidr <list\|auto>` and `--gateway-cidr` decide who may connect; `--publish-gateway loopback\|all` decides what is listening; `--private-only` closes public access on a stack reached over its tailnet |
|
|
249
|
+
| `apply` | Apply a previously reviewed plan file (`--dry-run` validates and shows diff without applying) |
|
|
250
|
+
| `ssh` | Interactive SSH session or run a remote command |
|
|
251
|
+
| `logs` | Stream OpenClaw logs (`-f`, `--tail N`, `--since 5m`) |
|
|
252
|
+
| `tunnel` | Local port-forward to gateway UI over SSH |
|
|
253
|
+
| `config` | Get/set remote OpenClaw config values (`--dry-run` shows would-write JSON) |
|
|
254
|
+
| `agents` | List OpenClaw agents, or stream one agent's logs |
|
|
255
|
+
| `gateway` | Restart the OpenClaw gateway service |
|
|
256
|
+
| `backup` | Create and restore OpenClaw state backups (`restore` expands into a staging directory, never in place) |
|
|
257
|
+
| `stacks` | List named stacks and their state |
|
|
258
|
+
| `doctor` | Check the local machine; with `--stack`, the deployment's health too; with `--provider`, one cloud's credentials and account setup whether or not a stack exists; with `--instance-type`, account checks ask about that size rather than the provider default. `--json` for the report. Exits 1 on any failure |
|
|
259
|
+
| `secret` | Manage secrets: `list`, `set`, `delete`, `rotate`, `audit` |
|
|
260
|
+
| `monitor` | Live dashboard: gateway health, container stats, log tail, stack picker |
|
|
261
|
+
| `mcp serve` | Start the embedded MCP server (stdio, or HTTP with `--http <port> --token <t>`) |
|
|
262
|
+
| `mcp install` | Interactively wire clawops into AI editors |
|
|
263
|
+
| `mcp wire` | Wire the gateway's AI as an MCP client of clawops (verifies the connection before saving) |
|
|
264
|
+
| `help` | List all commands and global flags |
|
|
265
|
+
| `harden` | Apply security hardening to a deployed stack (SSH, UFW, fail2ban, unattended-upgrades, Docker socket; AWS: SG audit, SSM check, Flow Logs, GuardDuty). `--tailscale` joins the stack to your tailnet and moves clawops onto that address once it answers; `--tailscale-revert` undoes it |
|
|
266
|
+
| `bug` | Open a pre-filled GitHub issue with system context from `doctor` |
|
|
359
267
|
|
|
360
|
-
|
|
361
|
-
gateway, and reported: *"The gateway's AI can now run clawops commands."* It could not.
|
|
268
|
+
Full flag reference: `clawops <command> --help`
|
|
362
269
|
|
|
363
|
-
|
|
364
|
-
flowchart TD
|
|
365
|
-
A["clawops mcp wire"] --> B["openclaw mcp add --transport streamable-http"]
|
|
366
|
-
B --> C{"gateway connects<br/>to the URL?"}
|
|
367
|
-
C -- no --> C1["probe fails, nothing saved,<br/>clawops prints the reason"]
|
|
368
|
-
C -- yes --> D["saved to mcp.servers.clawops"]
|
|
369
|
-
D --> E["openclaw mcp reload"]
|
|
370
|
-
```
|
|
270
|
+
---
|
|
371
271
|
|
|
372
|
-
|
|
373
|
-
"wired" means the gateway connected, not that a file was written.
|
|
272
|
+
## Plan → Apply workflow
|
|
374
273
|
|
|
375
|
-
|
|
274
|
+
For non-local providers, clawops enforces a review-before-apply discipline:
|
|
376
275
|
|
|
377
276
|
```bash
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
277
|
+
# 1. Generate a plan: runs `pulumi preview` internally, produces JSON
|
|
278
|
+
# --ssh-cidr decides who may connect. `auto` means this machine; omit it and nobody can.
|
|
279
|
+
clawops plan --provider aws --region us-east-1 --ssh-cidr auto --out /tmp/plan.json
|
|
381
280
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
channel it is connected to. See `docs/security/threat-model.md` T11.
|
|
281
|
+
# 2. Review plan.json: the `diff` field shows projected changes at plan-generation time
|
|
282
|
+
cat /tmp/plan.json | jq .diff
|
|
385
283
|
|
|
386
|
-
|
|
284
|
+
# 3. Apply: reads and validates the plan file, then runs `pulumi up`
|
|
285
|
+
clawops apply /tmp/plan.json
|
|
387
286
|
|
|
388
|
-
|
|
287
|
+
# Without --yes, apply prompts: "Continue? (y/N)"
|
|
288
|
+
clawops apply /tmp/plan.json --yes # skip prompt in automation
|
|
289
|
+
```
|
|
389
290
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
`
|
|
291
|
+
The plan JSON conforms to `spec/deploy-plan.schema.json` (AJV-validated) and captures reviewed
|
|
292
|
+
intent: provider, region, instance type, CIDR ranges, and OpenClaw version. `apply` re-runs
|
|
293
|
+
`pulumi up` using those parameters against the current live state, it does not replay a locked
|
|
294
|
+
execution artifact. Review and apply in the same session to minimize drift risk.
|
|
393
295
|
|
|
394
|
-
|
|
395
|
-
takes a bearer token, compares it in constant time, and refuses to bind anywhere but loopback
|
|
396
|
-
without one.
|
|
296
|
+
See [`docs/plan-apply.md`](docs/plan-apply.md) for full semantics, drift guidance, and the safe CI pattern.
|
|
397
297
|
|
|
398
|
-
|
|
298
|
+
---
|
|
399
299
|
|
|
400
|
-
|
|
401
|
-
flowchart TD
|
|
402
|
-
A["clawops plan"] --> B{"publishGateway?"}
|
|
403
|
-
B -- "loopback (default)" --> C{"allowedGatewayCidrs empty?"}
|
|
404
|
-
C -- no --> C1["refuse — those rules would admit<br/>traffic to a closed port"]
|
|
405
|
-
C -- yes --> D["SSH rules only"]
|
|
406
|
-
B -- all --> E["SSH rules + gateway rules<br/>on spec.network.gatewayPort"]
|
|
407
|
-
D --> F["clawops harden"]
|
|
408
|
-
E --> F
|
|
409
|
-
F --> G["read the container's port bindings"]
|
|
410
|
-
G --> H{"published to the network?"}
|
|
411
|
-
H -- no --> H1["ufw: SSH only"]
|
|
412
|
-
H -- yes --> H2["ufw: SSH + the published port"]
|
|
413
|
-
```
|
|
300
|
+
## MCP server
|
|
414
301
|
|
|
415
|
-
|
|
302
|
+
clawops ships an embedded [MCP](https://modelcontextprotocol.io/) server. Claude Code, Cursor, and
|
|
303
|
+
any MCP-compatible agent can drive deployments without leaving the chat interface.
|
|
416
304
|
|
|
417
|
-
|
|
418
|
-
`ufw allow 18789/tcp` unconditionally. Since the gateway publishes on `127.0.0.1`, that
|
|
419
|
-
opened a port nothing was listening on — a hardening step widening the firewall past what the
|
|
420
|
-
deployment exposes. It now reads the running container's port bindings and adds the rule only
|
|
421
|
-
when the gateway is really published, on whatever port it is published on.
|
|
305
|
+
### Wire your editor
|
|
422
306
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
was invisible.
|
|
307
|
+
```bash
|
|
308
|
+
clawops mcp install # interactive checkbox: writes config for selected apps
|
|
309
|
+
```
|
|
427
310
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
311
|
+
The wizard resolves the absolute binary path automatically so app launchers can find `clawops`
|
|
312
|
+
without inheriting your shell's `PATH`. See [Connect an AI editor](#connect-an-ai-editor) above
|
|
313
|
+
for manual config paths.
|
|
431
314
|
|
|
432
|
-
|
|
433
|
-
Both are fixed in 2.0.1 — see the list at the top of this section.
|
|
315
|
+
### Wire the gateway AI
|
|
434
316
|
|
|
435
|
-
|
|
317
|
+
The OpenClaw gateway runs its own AI agent. Once wired, that agent can call clawops directly
|
|
318
|
+
instead of guessing at infrastructure state:
|
|
436
319
|
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
"allowedSshCidrs": ["203.0.113.4/32"],
|
|
440
|
-
"allowedGatewayCidrs": [],
|
|
441
|
-
"publishGateway": "loopback",
|
|
442
|
-
"gatewayPort": 9443
|
|
443
|
-
}
|
|
320
|
+
```bash
|
|
321
|
+
clawops mcp wire --stack prod # write MCP client entry into gateway config + restart
|
|
444
322
|
```
|
|
445
323
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
changing it meant finding all of them — and missing one produced a container publishing one
|
|
449
|
-
port, a gateway listening on another, and a firewall opening a third.
|
|
324
|
+
Requires OpenClaw ≥ 2026.4 on the gateway. The `clawops setup` wizard offers this step
|
|
325
|
+
automatically after a successful deploy.
|
|
450
326
|
|
|
451
|
-
|
|
327
|
+
### Stdio mode (Claude Code / Cursor / VS Code)
|
|
452
328
|
|
|
453
|
-
|
|
329
|
+
Start the server manually or confirm your config is correct:
|
|
454
330
|
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
B --> C{"--stack given?"}
|
|
459
|
-
C -- no --> Z["report"]
|
|
460
|
-
C -- yes --> D["container state"]
|
|
461
|
-
D --> E["deployed OpenClaw version"]
|
|
462
|
-
E --> F["probe /startupz<br/>and read the body"]
|
|
463
|
-
F --> G["published scope, disk,<br/>log rotation, hardening drift"]
|
|
464
|
-
G --> Z
|
|
465
|
-
Z --> Y{"any check failed?"}
|
|
466
|
-
Y -- no --> Y1["exit 0"]
|
|
467
|
-
Y -- yes --> Y2["exit 1"]
|
|
331
|
+
```bash
|
|
332
|
+
clawops mcp serve --read-only # safe for first evaluation
|
|
333
|
+
clawops mcp serve # full mode: enables provisioning, config write, ssh exec
|
|
468
334
|
```
|
|
469
335
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
**It asks the gateway.** `doctor` used to read `docker inspect`'s healthcheck field, which
|
|
473
|
-
the OpenClaw image does not set — so it reported "no healthcheck configured" and moved on. A
|
|
474
|
-
running container means the process started, not that it serves. It now probes `/startupz`
|
|
475
|
-
and reads the body.
|
|
336
|
+
### HTTP mode (remote / multi-client)
|
|
476
337
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
338
|
+
```bash
|
|
339
|
+
clawops mcp serve --http 3333 --bind 127.0.0.1
|
|
340
|
+
# MCP HTTP server listening on 127.0.0.1:3333
|
|
341
|
+
```
|
|
481
342
|
|
|
482
|
-
|
|
483
|
-
agent that hits a failure can find out why. It reports only; it never runs `openclaw doctor
|
|
484
|
-
--fix`. `--json` gives the CLI the same report.
|
|
343
|
+
Do not bind to a non-loopback address without additional authentication controls in front of it.
|
|
485
344
|
|
|
486
|
-
###
|
|
345
|
+
### Available tools
|
|
487
346
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
347
|
+
| Tool | Toolset | Description |
|
|
348
|
+
|---|---|---|
|
|
349
|
+
| `clawops_status` | cli | Show stack outputs (what is deployed, not whether it works) |
|
|
350
|
+
| `clawops_doctor` | cli | Run diagnostics: local prerequisites, and with a stack, remote health |
|
|
351
|
+
| `clawops_logs_tail` | cli | Tail OpenClaw logs |
|
|
352
|
+
| `clawops_monitor` | cli | Sample gateway and host metrics |
|
|
353
|
+
| `clawops_stacks_list` | admin | List all stacks and their state |
|
|
354
|
+
| `clawops_config_get` | cli | Read a remote config value |
|
|
355
|
+
| `clawops_agents_list` | cli | List running agents |
|
|
356
|
+
| `clawops_up` | cli | Provision or update a stack |
|
|
357
|
+
| `clawops_destroy` | cli | Destroy a stack (elicits confirmation) |
|
|
358
|
+
| `clawops_apply` | cli | Apply a plan file |
|
|
359
|
+
| `clawops_plan` | cli | Generate a deploy plan |
|
|
360
|
+
| `clawops_config_set` | cli | Write a remote config value |
|
|
361
|
+
| `clawops_config_unset` | cli | Remove a remote config key |
|
|
362
|
+
| `clawops_config_validate` | cli | Validate the deployed config against the OpenClaw schema |
|
|
363
|
+
| `clawops_gateway_restart` | cli | Restart the gateway (elicits confirmation) |
|
|
364
|
+
| `clawops_harden` | cli | Apply hardening modules; join or leave a tailnet (elicits confirmation) |
|
|
365
|
+
| `clawops_workflow_deploy_app` | workflow | End-to-end deploy: plan → confirm → apply → status |
|
|
366
|
+
| `clawops_workflow_recover` | workflow | Diagnostic workflow for an unhealthy stack |
|
|
367
|
+
| `clawops_task_status` | cli | Poll a long-running task |
|
|
491
368
|
|
|
492
|
-
|
|
369
|
+
Tools in the `read` toolset are also available in `--read-only` mode; the table's Toolset column shows the primary toolset. All other toolsets require full mode.
|
|
370
|
+
Destructive tools require explicit confirmation (elicitation) unless `yes: true` is passed.
|
|
493
371
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
only put `clawops` in the docker group, so every Docker command failed with `permission
|
|
497
|
-
denied`. GCP and Azure connect as `clawops`, which is why only AWS was affected.
|
|
372
|
+
See [`docs/security/tool-risk-matrix.md`](docs/security/tool-risk-matrix.md) for the full risk
|
|
373
|
+
classification of every tool.
|
|
498
374
|
|
|
499
|
-
|
|
375
|
+
---
|
|
500
376
|
|
|
501
|
-
|
|
502
|
-
per-agent restart — only `gateway restart` and `daemon restart`, both of which interrupt
|
|
503
|
-
every agent on the host. Use `clawops gateway restart`, or stay on `@clawops/cli@legacy`.
|
|
377
|
+
## Configuration
|
|
504
378
|
|
|
505
|
-
|
|
379
|
+
Config lives at `~/.clawops/config.json` (override with `$CLAWOPS_HOME`).
|
|
506
380
|
|
|
507
|
-
|
|
381
|
+
```json
|
|
382
|
+
{
|
|
383
|
+
"version": 1,
|
|
384
|
+
"defaults": {
|
|
385
|
+
"provider": "aws",
|
|
386
|
+
"stack": "default"
|
|
387
|
+
},
|
|
388
|
+
"stacks": {
|
|
389
|
+
"default": {
|
|
390
|
+
"provider": "aws",
|
|
391
|
+
"region": "us-east-1",
|
|
392
|
+
"stateUrl": "s3://my-clawops-state"
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
"ssh": {
|
|
396
|
+
"keyPath": "~/.clawops/id_ed25519",
|
|
397
|
+
"knownHostsPath": "~/.clawops/known_hosts"
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
508
401
|
|
|
509
|
-
|
|
402
|
+
**Cloud credentials are never stored in config**. Clawops reads them from the environment:
|
|
510
403
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
committing to Kubernetes, a managed SaaS platform, or a single cloud provider.
|
|
404
|
+
| Provider | Credential source |
|
|
405
|
+
|---|---|
|
|
406
|
+
| AWS | `AWS_PROFILE` or standard AWS credential chain (`~/.aws/credentials`) |
|
|
407
|
+
| GCP | `GOOGLE_APPLICATION_CREDENTIALS` or `gcloud auth application-default login` |
|
|
408
|
+
| Azure | `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` or `az login` |
|
|
409
|
+
| Local | SSH host + key configured in `stacks[name].localOpts` |
|
|
518
410
|
|
|
519
411
|
---
|
|
520
412
|
|
|
521
|
-
##
|
|
522
|
-
|
|
523
|
-
- Provisions and tears down OpenClaw infrastructure on **AWS, GCP, Azure, and local VMs** using
|
|
524
|
-
the Pulumi Automation API — you do not install Pulumi; clawops installs the CLI it needs into
|
|
525
|
-
`~/.clawops/.pulumi-cli` on first use.
|
|
526
|
-
- Manages day-to-day operations: status, logs, SSH, tunnels, config, agents, gateway, backups.
|
|
527
|
-
- Exposes every operation as a **typed MCP tool** so AI agents can drive ops safely.
|
|
528
|
-
- Enforces a **plan → review → apply** discipline for cloud deployments.
|
|
529
|
-
- Emits **JSON output everywhere** (`--json`) for scripting and automation.
|
|
530
|
-
- Never stores cloud credentials — reads them from your environment's existing CLI profiles.
|
|
413
|
+
## Known limitations
|
|
531
414
|
|
|
532
|
-
|
|
415
|
+
See [`docs/limitations.md`](docs/limitations.md) for the full list. Key points:
|
|
533
416
|
|
|
534
|
-
- **
|
|
535
|
-
-
|
|
536
|
-
- **No
|
|
537
|
-
|
|
538
|
-
- **No TLS or domain automation** (yet). Bring your own reverse proxy or see
|
|
539
|
-
[`docs/limitations.md`](docs/limitations.md) for the manual path.
|
|
540
|
-
- **No credential storage.** Cloud credentials must be configured in your environment before using
|
|
541
|
-
clawops. They are never written to `~/.clawops/config.json`.
|
|
542
|
-
- **No native Windows.** WSL2 is fully supported; see [`docs/support-matrix.md`](docs/support-matrix.md).
|
|
417
|
+
- **Single-node deployments only**, not a high-availability or clustering platform.
|
|
418
|
+
- **`clawops apply` is not an immutable plan execution**. See [`docs/plan-apply.md`](docs/plan-apply.md).
|
|
419
|
+
- **No TLS/domain automation** in the current release.
|
|
420
|
+
- **MCP tools execute privileged operations**, use `--read-only` for first evaluation.
|
|
543
421
|
|
|
544
422
|
---
|
|
545
423
|
|
|
546
|
-
##
|
|
424
|
+
## Architecture
|
|
547
425
|
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
|
|
426
|
+
```
|
|
427
|
+
clawops
|
|
428
|
+
├── src/cli/ citty-based commands (one file per verb)
|
|
429
|
+
├── src/config/ ~/.clawops/config.json management
|
|
430
|
+
├── src/providers/ Cloud adapters (AWS, GCP, Azure, local)
|
|
431
|
+
│ ├── aws/ Pulumi inline program + ProviderAdapter
|
|
432
|
+
│ ├── gcp/
|
|
433
|
+
│ ├── azure/
|
|
434
|
+
│ └── local/ SSH bootstrap (no Pulumi)
|
|
435
|
+
├── src/pulumi/ Pulumi Automation API wrapper + output helpers
|
|
436
|
+
├── src/transport/ SSH client (ssh2) + connection pool + tunnels
|
|
437
|
+
├── src/mcp/ MCP server, tool handlers, progress tracking
|
|
438
|
+
├── src/plan/ Maker plan generation, AJV validation, apply
|
|
439
|
+
├── src/output/ ASCII table, spinner, JSON, human-readable output
|
|
440
|
+
├── src/errors/ Typed error hierarchy with exit codes
|
|
441
|
+
└── spec/ Machine-readable ground truth (JSON Schema, YAML)
|
|
551
442
|
```
|
|
552
443
|
|
|
553
|
-
|
|
554
|
-
handles everything in one flow — no config files to write by hand, no commands to memorize.
|
|
444
|
+
Key design decisions:
|
|
555
445
|
|
|
556
|
-
|
|
446
|
+
- **Pulumi Automation API:** the user installs no Pulumi. Clawops installs the CLI the API drives into `~/.clawops/.pulumi-cli`, pinned to the bundled SDK, without editing `$PATH` (ADR 0010); Pulumi home is sandboxed to `~/.clawops/.pulumi`; stack programs are inline TypeScript closures
|
|
447
|
+
- **State in cloud blob storage:** GCS (`gs://`), S3 (`s3://`), Azure Blob, no local state files, no `pulumi.yaml`
|
|
448
|
+
- **SSH via `ssh2`:** never shells out to `/usr/bin/ssh`; TOFU host verification against `~/.clawops/known_hosts`; connection pool with 5-min idle TTL
|
|
449
|
+
- **Plan → apply discipline:** every non-local deployment goes through `generatePlan()` → review → `applyPlan()`; destructive changes always require human review of the plan JSON
|
|
450
|
+
- **MCP-first:** every CLI operation has a typed MCP tool; schemas generated from `spec/mcp-tools.yaml`; all destructive tools use elicitation
|
|
557
451
|
|
|
558
|
-
|
|
452
|
+
See [`docs/architecture.md`](docs/architecture.md) for a full narrative, and [`docs/decisions/`](docs/decisions/) for ADRs.
|
|
559
453
|
|
|
560
|
-
|
|
561
|
-
Azure. Cloud deployments walk you through authenticating with the provider CLI if you aren't
|
|
562
|
-
already signed in.
|
|
454
|
+
### Cloud provider stacks
|
|
563
455
|
|
|
564
|
-
|
|
456
|
+
Each cloud provider is an inline Pulumi program that creates the resources below. All three share the same outputs (`publicIp`, `gatewayUrl`, `sshHost`, `sshPort`, `sshUser`) consumed by the SSH and config-overlay layers.
|
|
565
457
|
|
|
566
|
-
|
|
567
|
-
API key and saves it locally (in `~/.clawops/secrets/`, chmod 600) — it is never sent anywhere
|
|
568
|
-
except to OpenClaw on the target host when the config is applied.
|
|
458
|
+
#### AWS
|
|
569
459
|
|
|
570
|
-
|
|
460
|
+
```mermaid
|
|
461
|
+
flowchart LR
|
|
462
|
+
subgraph NET["Networking"]
|
|
463
|
+
VPC["VPC (10.0.0.0/16)"]
|
|
464
|
+
IGW[Internet Gateway]
|
|
465
|
+
SUBNET["Subnet (10.0.1.0/24)"]
|
|
466
|
+
RT[Route Table]
|
|
467
|
+
SG["Security Group (ports 22, 18789)"]
|
|
468
|
+
end
|
|
469
|
+
subgraph IAM["IAM"]
|
|
470
|
+
ROLE[IAM Role]
|
|
471
|
+
SSM[SSM Policy Attachment]
|
|
472
|
+
BED["Bedrock Policy Attachment (optional)"]
|
|
473
|
+
IP[Instance Profile]
|
|
474
|
+
end
|
|
475
|
+
subgraph COMPUTE["Compute"]
|
|
476
|
+
KP[EC2 Key Pair]
|
|
477
|
+
EC2["EC2 Instance (Ubuntu 22.04, IMDSv2)"]
|
|
478
|
+
EIP[Elastic IP]
|
|
479
|
+
end
|
|
480
|
+
```
|
|
571
481
|
|
|
572
|
-
|
|
573
|
-
integration's bot token the same way as the API key — paste it in, reference an env var, or point
|
|
574
|
-
to a file.
|
|
482
|
+
[Detailed diagram →](docs/providers/aws.md#stack-diagram)
|
|
575
483
|
|
|
576
|
-
|
|
484
|
+
#### GCP
|
|
577
485
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
486
|
+
```mermaid
|
|
487
|
+
flowchart LR
|
|
488
|
+
subgraph NET["Networking"]
|
|
489
|
+
NW[VPC Network]
|
|
490
|
+
SN["Subnetwork (10.0.0.0/24)"]
|
|
491
|
+
FW1["Firewall: SSH port 22 (conditional)"]
|
|
492
|
+
FW2["Firewall: Gateway port 18789 (conditional)"]
|
|
493
|
+
ADDR[Static External IP]
|
|
494
|
+
end
|
|
495
|
+
subgraph COMPUTE["Compute"]
|
|
496
|
+
VM["Compute Instance (Debian 12, 20 GB)"]
|
|
497
|
+
end
|
|
498
|
+
```
|
|
581
499
|
|
|
582
|
-
|
|
500
|
+
[Detailed diagram →](docs/providers/gcp.md#stack-diagram)
|
|
583
501
|
|
|
584
|
-
|
|
585
|
-
starts the container), applies your LLM and integration config, generates a gateway auth token,
|
|
586
|
-
and prints a direct dashboard URL:
|
|
502
|
+
#### Azure
|
|
587
503
|
|
|
504
|
+
```mermaid
|
|
505
|
+
flowchart LR
|
|
506
|
+
RG[Resource Group]
|
|
507
|
+
subgraph NET["Networking"]
|
|
508
|
+
VNET["Virtual Network (10.0.0.0/16)"]
|
|
509
|
+
SUBNET["Subnet (10.0.1.0/24)"]
|
|
510
|
+
NSG["Network Security Group (ports 22, 18789)"]
|
|
511
|
+
PIP["Public IP Address (Static)"]
|
|
512
|
+
NIC[Network Interface]
|
|
513
|
+
end
|
|
514
|
+
subgraph COMPUTE["Compute"]
|
|
515
|
+
VM["VM (Ubuntu 22.04, managed identity)"]
|
|
516
|
+
end
|
|
517
|
+
subgraph KV["Key Vault (optional)"]
|
|
518
|
+
VAULT["Key Vault (RBAC, name max 24 chars)"]
|
|
519
|
+
RA["Role Assignment (Secrets User)"]
|
|
520
|
+
SECRET["Secret: gateway-token"]
|
|
521
|
+
end
|
|
588
522
|
```
|
|
589
|
-
✔ All done! OpenClaw is running.
|
|
590
|
-
ℹ Open dashboard: http://192.168.1.50:18789?token=<your-token>
|
|
591
|
-
ℹ Token saved to ~/.clawops/secrets/GATEWAY_TOKEN_my-stack
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
**Prerequisites:** Node.js ≥ 22, an SSH key, and either an SSH-reachable Linux/macOS host or a
|
|
595
|
-
cloud account with CLI credentials configured (`aws configure`, `gcloud auth login`, or `az login`).
|
|
596
|
-
You do not need Pulumi — the first cloud deployment installs the CLI clawops drives into
|
|
597
|
-
`~/.clawops/.pulumi-cli` and says so while it does.
|
|
598
523
|
|
|
599
|
-
|
|
524
|
+
[Detailed diagram →](docs/providers/azure.md#stack-diagram)
|
|
600
525
|
|
|
601
526
|
---
|
|
602
527
|
|
|
603
|
-
|
|
528
|
+
## Development
|
|
604
529
|
|
|
605
|
-
|
|
530
|
+
### Setup
|
|
606
531
|
|
|
607
532
|
```bash
|
|
608
|
-
|
|
533
|
+
git clone https://github.com/dfridkin/clawops.git
|
|
534
|
+
cd clawops
|
|
535
|
+
# Node 22+ required; use nvm: nvm use
|
|
536
|
+
pnpm install
|
|
537
|
+
pnpm dev doctor # verify toolchain
|
|
538
|
+
```
|
|
609
539
|
|
|
610
|
-
|
|
540
|
+
### Scripts
|
|
611
541
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
542
|
+
```bash
|
|
543
|
+
pnpm dev # run CLI from src/ via tsx
|
|
544
|
+
pnpm build # tsup → dist/
|
|
545
|
+
pnpm test # vitest (1855 tests, ~13s)
|
|
546
|
+
pnpm test:changed # vitest --changed (fast edit loop)
|
|
547
|
+
pnpm test:integration # Docker-based SSH integration tests
|
|
548
|
+
pnpm typecheck # tsc --noEmit
|
|
549
|
+
pnpm lint # eslint src/ tests/ scripts/ (--max-warnings=0)
|
|
550
|
+
pnpm gen:schemas # regenerate src/providers/types.ts + src/mcp/tools/_generated.ts
|
|
551
|
+
pnpm gen:schemas --check # CI guard: committed generated files match spec
|
|
552
|
+
pnpm graph # local coupling report (--base <ref> for this branch's delta)
|
|
553
|
+
pnpm verify:pack # install the packed tarball elsewhere and run it (CI gate)
|
|
554
|
+
pnpm sync:server-json # write package.json's version into server.json
|
|
555
|
+
pnpm changeset # record a release note before merging
|
|
615
556
|
```
|
|
616
557
|
|
|
617
|
-
|
|
618
|
-
setup, and troubleshooting.
|
|
558
|
+
### Project layout
|
|
619
559
|
|
|
620
|
-
|
|
560
|
+
| Path | Purpose |
|
|
561
|
+
|---|---|
|
|
562
|
+
| `spec/` | Machine-readable ground truth: JSON Schema, YAML. **Treat as source of truth.** |
|
|
563
|
+
| `SPEC.md` | Full technical specification (milestones, rules, schemas) |
|
|
564
|
+
| `DESIGN_RULES.md` | 25 normative rules (R1–R25) referenced throughout the codebase |
|
|
565
|
+
| `docs/architecture.md` | Narrative system overview |
|
|
566
|
+
| `docs/plan-apply.md` | Plan/apply semantics, drift guidance, CI pattern |
|
|
567
|
+
| `docs/ci.md` | CI integration guide: OIDC, env vars, plan → apply in CI |
|
|
568
|
+
| `docs/security/` | MCP safety model, tool risk matrix, redaction, audit logs |
|
|
569
|
+
| `docs/providers/matrix.md` | Per-provider capability matrix |
|
|
570
|
+
| `docs/decisions/` | Architecture Decision Records |
|
|
571
|
+
| `.claude/skills/` | Invokable procedures: `/add-provider`, `/release`, `/tdd`, `/mcp-tool` |
|
|
572
|
+
| `.claude/rules/` | Path-scoped lint rules loaded by Claude Code |
|
|
621
573
|
|
|
622
|
-
|
|
623
|
-
npm install -g @clawops/cli
|
|
574
|
+
### Code generation
|
|
624
575
|
|
|
625
|
-
|
|
626
|
-
clawops init --provider aws
|
|
576
|
+
Two files are generated from `spec/` and must not be hand-edited:
|
|
627
577
|
|
|
628
|
-
|
|
578
|
+
- `src/providers/types.ts`. `ProviderAdapter` interface from `spec/providers.schema.json`
|
|
579
|
+
- `src/mcp/tools/_generated.ts`. Zod schemas and type exports from `spec/mcp-tools.yaml`
|
|
629
580
|
|
|
630
|
-
|
|
631
|
-
clawops apply /tmp/plan.json
|
|
632
|
-
```
|
|
581
|
+
Run `pnpm gen:schemas` after modifying either spec file. CI enforces this with `--check`.
|
|
633
582
|
|
|
634
|
-
|
|
635
|
-
generated and written into it. Without it the plan allows no ingress at all and nothing —
|
|
636
|
-
including clawops — will be able to connect.
|
|
583
|
+
### Adding a provider
|
|
637
584
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
## Connect an AI editor
|
|
641
|
-
|
|
642
|
-
The `setup` wizard handles this automatically (Step 4). To wire or re-wire editors at any time:
|
|
585
|
+
Use the `/add-provider` skill in Claude Code, or follow [`src/providers/CLAUDE.md`](src/providers/CLAUDE.md). Every adapter must satisfy `ProviderAdapter` in `src/providers/types.ts`. Do not relax the schema to fit the adapter.
|
|
643
586
|
|
|
644
|
-
|
|
645
|
-
clawops mcp install
|
|
646
|
-
```
|
|
587
|
+
### Adding an MCP tool
|
|
647
588
|
|
|
648
|
-
|
|
649
|
-
Cursor, Windsurf, VS Code, or Zed and clawops writes the MCP entry into each app's config using
|
|
650
|
-
the correct absolute binary path.
|
|
589
|
+
Use the `/mcp-tool` skill. The skill adds the tool to `spec/mcp-tools.yaml`, runs `pnpm gen:schemas`, creates the handler in `src/mcp/tools/<toolset>/<name>.ts`, and wires it into the registry. All four annotation hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) are required on every tool.
|
|
651
590
|
|
|
652
|
-
|
|
591
|
+
### Conventional commits
|
|
653
592
|
|
|
654
|
-
```
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
"command": "/path/to/clawops",
|
|
659
|
-
"args": ["mcp", "serve", "--read-only"]
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
593
|
+
```
|
|
594
|
+
feat(scope): description
|
|
595
|
+
fix(scope): description
|
|
596
|
+
docs / refactor / chore / test / perf / ci
|
|
663
597
|
```
|
|
664
598
|
|
|
665
|
-
|
|
599
|
+
Use `pnpm changeset` to record a release note before merging a `feat` or `fix`.
|
|
666
600
|
|
|
667
|
-
|
|
668
|
-
|---|---|
|
|
669
|
-
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
670
|
-
| Claude Desktop (Linux) | `~/.config/Claude/claude_desktop_config.json` |
|
|
671
|
-
| Claude Code | `~/.claude.json` |
|
|
672
|
-
| Cursor | `~/.cursor/mcp.json` |
|
|
673
|
-
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
|
|
674
|
-
| VS Code (macOS) | `~/Library/Application Support/Code/User/mcp.json` |
|
|
675
|
-
| VS Code (Linux) | `~/.config/Code/User/mcp.json` |
|
|
676
|
-
| Zed | `~/.config/zed/settings.json` (key: `context_servers`) |
|
|
601
|
+
---
|
|
677
602
|
|
|
678
|
-
|
|
679
|
-
blocking mutations. Remove it only after reviewing
|
|
680
|
-
[`docs/security/mcp-safety.md`](docs/security/mcp-safety.md).
|
|
603
|
+
## What's new in 2.1
|
|
681
604
|
|
|
682
|
-
|
|
683
|
-
confirmation before executing — they will never run silently.
|
|
605
|
+
Private networking, hardening on every cloud, and two fixes to commands that could not start.
|
|
684
606
|
|
|
685
|
-
|
|
607
|
+
### Reach a stack over your tailnet
|
|
686
608
|
|
|
687
|
-
|
|
609
|
+
- `clawops harden --tailscale` installs Tailscale on a stack, joins it to your tailnet as
|
|
610
|
+
`clawops-<stack>`, and reports the address it was given.
|
|
611
|
+
- The same command then moves clawops onto that address, but only after opening an SSH session
|
|
612
|
+
to it — against host keys pinned over the public connection it already trusts.
|
|
613
|
+
- The Tailscale auth key comes from `clawops secret set TAILSCALE_AUTH_KEY`, and reaches the host
|
|
614
|
+
over the SSH data channel. It never appears in a command line, a process list or a log.
|
|
615
|
+
- `clawops plan --private-only` → `clawops apply` closes public SSH and gateway access on a stack
|
|
616
|
+
reached over its tailnet. Both refuse unless that address answers SSH at that moment
|
|
617
|
+
([ADR 0013](docs/decisions/0013-private-only-through-the-plan.md)).
|
|
618
|
+
- `clawops harden --tailscale-revert` takes a host off the tailnet and returns clawops to its
|
|
619
|
+
public address. On a private-only stack it refuses, and prints the commands that reopen SSH.
|
|
620
|
+
- `clawops destroy` forgets the host keys for both addresses of a stack on its tailnet, instead
|
|
621
|
+
of leaving the public one pinned for an instance that no longer exists.
|
|
688
622
|
|
|
689
|
-
|
|
623
|
+
### Hardening covers all three clouds
|
|
690
624
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
625
|
+
- **Azure**: NSG audit, disk encryption, Defender for Cloud and JIT VM access, all check-only.
|
|
626
|
+
- **GCP**: VPC firewall audit, Shielded VM and OS Login, all check-only.
|
|
627
|
+
- GCP instances boot with Secure Boot on. Existing stacks get it as an update that keeps the boot
|
|
628
|
+
disk and all OpenClaw state.
|
|
629
|
+
- A check that could not run reports as skipped, naming what was missing, rather than as a pass.
|
|
696
630
|
|
|
697
|
-
|
|
698
|
-
clawops config set maxAgents 8
|
|
631
|
+
### Plans say what they will disturb
|
|
699
632
|
|
|
700
|
-
clawops
|
|
633
|
+
- `clawops plan` counts and lists resources that would be **replaced**. It used to summarise a
|
|
634
|
+
preview that would destroy the instance and its boot disk as "0 to create, 0 to update".
|
|
635
|
+
- A plan that changes a live deployment warns before you apply it: a replacement names what goes
|
|
636
|
+
with it and points at `clawops backup create`; an update says the gateway goes down.
|
|
701
637
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
638
|
+
### Fixes
|
|
639
|
+
|
|
640
|
+
- `clawops mcp serve` could not start at all when installed from npm — it died on import before
|
|
641
|
+
emitting any protocol, so every MCP client got nothing. `pnpm verify:pack` now speaks MCP to
|
|
642
|
+
the packed tarball, so this class of failure cannot ship again.
|
|
643
|
+
- `server.json`, the MCP registry manifest, is versioned with the package rather than rewritten
|
|
644
|
+
at publish time. The committed file had read `1.7.3` against a published `2.0.2`.
|
|
645
|
+
- The published package carries its license, keywords and issue tracker, so it is findable on npm
|
|
646
|
+
and its listing is complete.
|
|
705
647
|
|
|
706
648
|
---
|
|
707
649
|
|
|
708
|
-
##
|
|
650
|
+
## What's new in 2.0.1
|
|
709
651
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
| `init` | Register a stack in `~/.clawops/config.json` without provisioning. Additive — existing stacks are kept; `--force` is needed only to overwrite one |
|
|
714
|
-
| `up` | Provision or update stack (`--dry-run` for preview, `--gateway-port` for a non-default port) |
|
|
715
|
-
| `down` | Destroy local-provider stack (requires `--yes`; `--dry-run` shows current outputs) |
|
|
716
|
-
| `destroy` | Destroy cloud-provider stack with confirmation prompt (`--dry-run` shows current outputs) |
|
|
717
|
-
| `status` | Show stack outputs: IP, gateway URL, region, provisioned time |
|
|
718
|
-
| `plan` | Generate a deploy-plan JSON artifact (dry-run safe). `--ssh-cidr <list\|auto>` and `--gateway-cidr` decide who may connect; `--publish-gateway loopback\|all` decides what is listening |
|
|
719
|
-
| `apply` | Apply a previously reviewed plan file (`--dry-run` validates and shows diff without applying) |
|
|
720
|
-
| `ssh` | Interactive SSH session or run a remote command |
|
|
721
|
-
| `logs` | Stream OpenClaw logs (`-f`, `--tail N`, `--since 5m`) |
|
|
722
|
-
| `tunnel` | Local port-forward to gateway UI over SSH |
|
|
723
|
-
| `config` | Get/set remote OpenClaw config values (`--dry-run` shows would-write JSON) |
|
|
724
|
-
| `agents` | List OpenClaw agents, or stream one agent's logs |
|
|
725
|
-
| `gateway` | Restart the OpenClaw gateway service |
|
|
726
|
-
| `backup` | Create and restore OpenClaw state backups (`restore` expands into a staging directory, never in place) |
|
|
727
|
-
| `stacks` | List named stacks and their state |
|
|
728
|
-
| `doctor` | Check the local machine; with `--stack`, the deployment's health too; with `--provider`, one cloud's credentials and account setup whether or not a stack exists; with `--instance-type`, account checks ask about that size rather than the provider default. `--json` for the report. Exits 1 on any failure |
|
|
729
|
-
| `secret` | Manage secrets: `list`, `set`, `delete`, `rotate`, `audit` |
|
|
730
|
-
| `monitor` | Live dashboard: gateway health, container stats, log tail, stack picker |
|
|
731
|
-
| `mcp serve` | Start the embedded MCP server (stdio, or HTTP with `--http <port> --token <t>`) |
|
|
732
|
-
| `mcp install` | Interactively wire clawops into AI editors |
|
|
733
|
-
| `mcp wire` | Wire the gateway's AI as an MCP client of clawops (verifies the connection before saving) |
|
|
734
|
-
| `help` | List all commands and global flags |
|
|
735
|
-
| `harden` | Apply security hardening to a deployed stack (SSH, UFW, fail2ban, unattended-upgrades, Docker socket; AWS: SG audit, SSM check, Flow Logs, GuardDuty) |
|
|
736
|
-
| `bug` | Open a pre-filled GitHub issue with system context from `doctor` |
|
|
652
|
+
A patch release, and a large one: in 2.0.0 no cloud deploy succeeded by any path. Every item
|
|
653
|
+
below is a fix or an addition in 2.0.1. The reasoning behind each one is in its commit message,
|
|
654
|
+
and the decisions that came out of them are in [`docs/decisions/`](docs/decisions/).
|
|
737
655
|
|
|
738
|
-
|
|
656
|
+
### Deploying to a cloud
|
|
739
657
|
|
|
740
|
-
|
|
658
|
+
- `clawops plan` → `clawops apply` provisions a cloud stack and deploys OpenClaw onto it.
|
|
659
|
+
- `clawops up` deploys to AWS, GCP and Azure, running the same path as `plan` → `apply`.
|
|
660
|
+
- clawops installs the Pulumi CLI it needs into `~/.clawops/.pulumi-cli`, or uses a compatible
|
|
661
|
+
one already on `$PATH` ([ADR 0010](docs/decisions/0010-pulumi-cli-bootstrap.md)).
|
|
662
|
+
- clawops creates and stores the passphrase its state backend requires
|
|
663
|
+
([ADR 0011](docs/decisions/0011-state-passphrase.md)).
|
|
664
|
+
- `clawops plan` takes `--ssh-cidr`, `--gateway-cidr` and `--publish-gateway`, and `apply`
|
|
665
|
+
passes them to the cloud firewall. `auto` resolves this machine's address.
|
|
666
|
+
- `clawops plan` stops, and names the cause, when it cannot open the state backend.
|
|
667
|
+
- `--instance-type` takes a clawops alias (`micro`–`gpu`) or a machine type your cloud names
|
|
668
|
+
itself, and the plan records the concrete type.
|
|
669
|
+
- Deploys pin the account they were planned against: `gcp:project` on GCP,
|
|
670
|
+
`azure-native:subscriptionId` on Azure.
|
|
741
671
|
|
|
742
|
-
|
|
672
|
+
### Checking the account before you spend
|
|
743
673
|
|
|
744
|
-
|
|
674
|
+
- `clawops doctor --provider <cloud>` checks one cloud's credentials and account setup, with or
|
|
675
|
+
without a stack. `--instance-type` points the size check at the size you are deploying.
|
|
676
|
+
- **AWS**. The account the credentials resolve to, the state bucket, and whether the instance
|
|
677
|
+
type is offered in the region.
|
|
678
|
+
- **GCP**. The project, the APIs a deploy needs, and the state bucket.
|
|
679
|
+
- **Azure**. The subscription, the resource providers, the VM size, and the azblob credentials
|
|
680
|
+
Pulumi authenticates with.
|
|
681
|
+
- `clawops setup` runs the same checks and offers to fix what it safely can, enabling an API,
|
|
682
|
+
creating a state bucket with versioning on and public access blocked, naming the change
|
|
683
|
+
before making it.
|
|
684
|
+
- A check clawops could not perform reports as a warning naming the error, rather than as a
|
|
685
|
+
pass or a failure.
|
|
686
|
+
- Azure accepts your `az login`; a service principal is no longer required.
|
|
745
687
|
|
|
746
|
-
|
|
747
|
-
# 1. Generate a plan — runs `pulumi preview` internally, produces JSON
|
|
748
|
-
# --ssh-cidr decides who may connect. `auto` means this machine; omit it and nobody can.
|
|
749
|
-
clawops plan --provider aws --region us-east-1 --ssh-cidr auto --out /tmp/plan.json
|
|
688
|
+
### Naming, config and setup
|
|
750
689
|
|
|
751
|
-
|
|
752
|
-
|
|
690
|
+
- clawops names the state backend after the account it is deploying into, instead of asking you
|
|
691
|
+
for a name or writing a placeholder
|
|
692
|
+
([ADR 0012](docs/decisions/0012-state-bucket-naming.md)).
|
|
693
|
+
- A name you type instead is checked against the rules of the cloud that has to accept it.
|
|
694
|
+
- `clawops init` keeps the stacks already in your config.
|
|
695
|
+
- `clawops init` generates an SSH key that clawops can read. If you ran `init` before this
|
|
696
|
+
release, `clawops doctor` will tell you whether yours is usable.
|
|
697
|
+
- `gcloud config set project` is honoured.
|
|
698
|
+
- The setup wizard writes model configuration that OpenClaw accepts, and installs the plugin
|
|
699
|
+
your chosen provider needs.
|
|
700
|
+
- Amazon Bedrock works: the right transport, and an inference profile resolved against your
|
|
701
|
+
deployment region and recorded in the plan. Needs `bedrock:ListInferenceProfiles`.
|
|
753
702
|
|
|
754
|
-
|
|
755
|
-
clawops apply /tmp/plan.json
|
|
703
|
+
### While a deploy is running
|
|
756
704
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
705
|
+
- `apply` waits for SSH, then waits for the gateway to answer, before reporting success.
|
|
706
|
+
- `apply` reports progress as it goes instead of going quiet for minutes.
|
|
707
|
+
- A deploy that times out prints what the host was doing, from its bootstrap log.
|
|
708
|
+
- A host still installing Docker is treated as still booting rather than as a failed deploy.
|
|
760
709
|
|
|
761
|
-
|
|
762
|
-
intent: provider, region, instance type, CIDR ranges, and OpenClaw version. `apply` re-runs
|
|
763
|
-
`pulumi up` using those parameters against the current live state — it does not replay a locked
|
|
764
|
-
execution artifact. Review and apply in the same session to minimize drift risk.
|
|
710
|
+
### Day-two commands
|
|
765
711
|
|
|
766
|
-
|
|
712
|
+
- `clawops logs` reads from the gateway on AWS.
|
|
713
|
+
- `doctor --stack`, `ssh`, `logs`, `gateway`, `config` and `agents` work against a freshly
|
|
714
|
+
deployed stack.
|
|
715
|
+
- `clawops doctor` validates cloud credentials.
|
|
716
|
+
- clawops tells a refused Docker socket from a missing container, and says which it found.
|
|
717
|
+
- `clawops destroy` forgets the instance's host key, so redeploying onto an address the cloud
|
|
718
|
+
has recycled no longer fails verification.
|
|
767
719
|
|
|
768
|
-
|
|
720
|
+
### Documentation
|
|
769
721
|
|
|
770
|
-
|
|
722
|
+
- The GCP guide names the credential source clawops actually reads, and describes 2.0
|
|
723
|
+
firewall behaviour.
|
|
724
|
+
- The smoke-test plan covers 2.0, and `pnpm test:cloud aws|gcp|azure` runs it against a real
|
|
725
|
+
deployment and destroys it afterwards.
|
|
771
726
|
|
|
772
|
-
|
|
773
|
-
any MCP-compatible agent can drive deployments without leaving the chat interface.
|
|
727
|
+
## What's new in 2.0
|
|
774
728
|
|
|
775
|
-
|
|
729
|
+
clawops 2.x targets **OpenClaw >= 2026.9.2**. The 1.x line continues for OpenClaw
|
|
730
|
+
`<= 2026.7.1-2` under the `legacy` dist-tag until **2027-03-31**:
|
|
776
731
|
|
|
777
732
|
```bash
|
|
778
|
-
|
|
733
|
+
npm install -g @clawops/cli # 2.x
|
|
734
|
+
npm install -g @clawops/cli@legacy # 1.x maintenance
|
|
779
735
|
```
|
|
780
736
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
737
|
+
Pin the tag in CI. `latest` moves to 2.x, so an unpinned pipeline will change lines.
|
|
738
|
+
[`CHANGELOG.md`](CHANGELOG.md) carries the full history; this section covers what changed
|
|
739
|
+
about *how clawops behaves*.
|
|
784
740
|
|
|
785
|
-
###
|
|
741
|
+
### Your deployment keeps its state
|
|
786
742
|
|
|
787
|
-
|
|
788
|
-
|
|
743
|
+
OpenClaw 2.0 stores sessions, transcripts and credentials in SQLite. clawops mounted no
|
|
744
|
+
state at all, so **every restart destroyed them**, and a restart is what `gateway restart`,
|
|
745
|
+
`gateway update` and `config set` all do.
|
|
789
746
|
|
|
790
|
-
|
|
791
|
-
|
|
747
|
+
One host directory (`/var/lib/clawops/openclaw`) is now bind-mounted at OpenClaw's own
|
|
748
|
+
default location, holding the config, the database and any provider plugins. Existing
|
|
749
|
+
deployments migrate on the next `up`/`apply`.
|
|
750
|
+
|
|
751
|
+
### `clawops up` / `clawops apply`
|
|
752
|
+
|
|
753
|
+
```mermaid
|
|
754
|
+
flowchart TD
|
|
755
|
+
A["clawops plan"] --> B{"config valid<br/>against OpenClaw schema?"}
|
|
756
|
+
B -- no --> B1["refuse: plan is still<br/>a file you can edit"]
|
|
757
|
+
B -- yes --> C["clawops apply"]
|
|
758
|
+
C --> D{"OpenClaw version<br/>in supported range?"}
|
|
759
|
+
D -- no --> D1["refuse: names<br/>@clawops/cli@legacy"]
|
|
760
|
+
D -- yes --> E["provision host"]
|
|
761
|
+
E --> F["state dir, owned 1000:1000<br/>migrate any pre-2.0 config"]
|
|
762
|
+
F --> G["write config<br/>validated before writing"]
|
|
763
|
+
G --> H["install provider plugins<br/>while egress exists"]
|
|
764
|
+
H --> I["start gateway"]
|
|
765
|
+
I --> J{"/startupz says started?"}
|
|
766
|
+
J -- no --> J1["fail with the reason"]
|
|
767
|
+
J -- yes --> K{"configured providers<br/>all loaded?"}
|
|
768
|
+
K -- no --> K1["warn: healthy gateway,<br/>missing model backend"]
|
|
769
|
+
K -- yes --> L["done"]
|
|
792
770
|
```
|
|
793
771
|
|
|
794
|
-
|
|
795
|
-
|
|
772
|
+
Three of those steps are new, and each exists because the old flow could report success
|
|
773
|
+
while something was wrong: the config was never validated before being written, provider
|
|
774
|
+
plugins were left to be fetched at boot (or silently missing on a deny-all host), and
|
|
775
|
+
"started" was inferred from `docker run` exiting 0.
|
|
796
776
|
|
|
797
|
-
###
|
|
777
|
+
### `clawops gateway update`
|
|
798
778
|
|
|
799
|
-
|
|
779
|
+
Previously: pull, run, report success. `docker run` exiting 0 means the container was
|
|
780
|
+
*created*, and the container it replaced is already gone.
|
|
800
781
|
|
|
801
|
-
```
|
|
802
|
-
|
|
803
|
-
clawops
|
|
782
|
+
```mermaid
|
|
783
|
+
flowchart TD
|
|
784
|
+
A["clawops gateway update X"] --> B{"X in supported range?"}
|
|
785
|
+
B -- no --> B1["refuse before pulling"]
|
|
786
|
+
B -- yes --> C["docker pull X"]
|
|
787
|
+
C --> D["snapshot state database"]
|
|
788
|
+
D -- cannot snapshot --> D1["refuse: no rollback point"]
|
|
789
|
+
D --> E{"target release understands<br/>this schema?"}
|
|
790
|
+
E -- no --> E1["refuse: downgrade across<br/>a schema boundary"]
|
|
791
|
+
E -- yes --> F["swap container"]
|
|
792
|
+
F --> G{"/startupz says started?"}
|
|
793
|
+
G -- yes --> H["done"]
|
|
794
|
+
G -- no --> I["one-shot doctor --fix<br/>in a throwaway container"]
|
|
795
|
+
I --> J["re-run, re-gate"]
|
|
796
|
+
J -- started --> K["done: reported as repaired"]
|
|
797
|
+
J -- still not --> L["roll back to previous image"]
|
|
798
|
+
L -- started --> M["rolled back, reason reported"]
|
|
799
|
+
L -- still not --> N["failed: snapshot path named"]
|
|
804
800
|
```
|
|
805
801
|
|
|
806
|
-
|
|
802
|
+
The snapshot is not only a rollback point: `database preflight` refuses a live database
|
|
803
|
+
because the schema version sits in the WAL until checkpointed, so the consolidated snapshot
|
|
804
|
+
is what makes the compatibility check possible at all.
|
|
807
805
|
|
|
808
|
-
|
|
809
|
-
clawops mcp serve --http 3333 --bind 127.0.0.1
|
|
810
|
-
# MCP HTTP server listening on 127.0.0.1:3333
|
|
811
|
-
```
|
|
806
|
+
### `clawops gateway restart`
|
|
812
807
|
|
|
813
|
-
|
|
808
|
+
A restart changes neither the deployed version nor who can reach the gateway. Both are read
|
|
809
|
+
back from the running container rather than guessed:
|
|
814
810
|
|
|
815
|
-
|
|
811
|
+
```mermaid
|
|
812
|
+
flowchart LR
|
|
813
|
+
A["gateway restart"] --> B["read current image"]
|
|
814
|
+
B -- no container --> B1["refuse: nothing to reuse.<br/>latest and stable point at 2.0"]
|
|
815
|
+
B --> C["read current publish scope"]
|
|
816
|
+
C --> D["recreate with the same<br/>version and reachability"]
|
|
817
|
+
D --> E{"/startupz says started?"}
|
|
818
|
+
E -- no --> E1["fail with the reason"]
|
|
819
|
+
E -- yes --> F["done"]
|
|
820
|
+
```
|
|
816
821
|
|
|
817
|
-
|
|
818
|
-
|---|---|---|
|
|
819
|
-
| `clawops_status` | cli | Show stack outputs (what is deployed, not whether it works) |
|
|
820
|
-
| `clawops_doctor` | cli | Run diagnostics: local prerequisites, and with a stack, remote health |
|
|
821
|
-
| `clawops_logs_tail` | cli | Tail OpenClaw logs |
|
|
822
|
-
| `clawops_monitor` | cli | Sample gateway and host metrics |
|
|
823
|
-
| `clawops_stacks_list` | admin | List all stacks and their state |
|
|
824
|
-
| `clawops_config_get` | cli | Read a remote config value |
|
|
825
|
-
| `clawops_agents_list` | cli | List running agents |
|
|
826
|
-
| `clawops_up` | cli | Provision or update a stack |
|
|
827
|
-
| `clawops_destroy` | cli | Destroy a stack (elicits confirmation) |
|
|
828
|
-
| `clawops_apply` | cli | Apply a plan file |
|
|
829
|
-
| `clawops_plan` | cli | Generate a deploy plan |
|
|
830
|
-
| `clawops_config_set` | cli | Write a remote config value |
|
|
831
|
-
| `clawops_config_unset` | cli | Remove a remote config key |
|
|
832
|
-
| `clawops_config_validate` | cli | Validate the deployed config against the OpenClaw schema |
|
|
833
|
-
| `clawops_gateway_restart` | cli | Restart the gateway (elicits confirmation) |
|
|
834
|
-
| `clawops_workflow_deploy_app` | workflow | End-to-end deploy: plan → confirm → apply → status |
|
|
835
|
-
| `clawops_workflow_recover` | workflow | Diagnostic workflow for an unhealthy stack |
|
|
836
|
-
| `clawops_task_status` | cli | Poll a long-running task |
|
|
822
|
+
### Migrating an existing 1.x deployment
|
|
837
823
|
|
|
838
|
-
|
|
839
|
-
|
|
824
|
+
```mermaid
|
|
825
|
+
flowchart TD
|
|
826
|
+
A["clawops migrate"] --> B{"1.x container running?"}
|
|
827
|
+
B -- no --> B1["nothing to rescue: state was<br/>already lost to an earlier restart"]
|
|
828
|
+
B -- yes --> C["verified backup, inside the running container"]
|
|
829
|
+
C -- "backup fails" --> C1["refused: nothing touched"]
|
|
830
|
+
C --> D["extract state from the RUNNING container"]
|
|
831
|
+
D --> E["chown 1000:1000"]
|
|
832
|
+
E --> F["stop and remove 1.x"]
|
|
833
|
+
F --> G["synthesise a valid 2.0 config"]
|
|
834
|
+
G --> H["start 2.0 with the state directory"]
|
|
835
|
+
H --> I{"/startupz started?"}
|
|
836
|
+
I -- "no: schema still migrating" --> J["restart once"]
|
|
837
|
+
J --> K{"started?"}
|
|
838
|
+
K -- no --> K1["failed: points at the backup"]
|
|
839
|
+
K --> L["report"]
|
|
840
|
+
I -- yes --> L
|
|
841
|
+
L --> M["what carried over,<br/>device identity, config to review"]
|
|
842
|
+
```
|
|
840
843
|
|
|
841
|
-
|
|
842
|
-
classification of every tool.
|
|
844
|
+
Two things about that shape are not obvious, and both came from running a real migration:
|
|
843
845
|
|
|
844
|
-
|
|
846
|
+
**State is extracted from the *running* container.** All 1.x state lived inside it, clawops
|
|
847
|
+
mounted none, so stopping first destroys what the migration came to save.
|
|
845
848
|
|
|
846
|
-
|
|
849
|
+
**The config is synthesised, not carried forward.** 1.x never had one that applied; the file
|
|
850
|
+
clawops mounted was read by nothing. Your old settings are reported as *intent to review*,
|
|
851
|
+
never applied blindly. Their channel blocks would not validate against 2.0 anyway.
|
|
847
852
|
|
|
848
|
-
|
|
853
|
+
The gateway also needs two starts: the first performs the state-schema migration and reports
|
|
854
|
+
it as pending. `migrate` waits for the second rather than declaring success early.
|
|
849
855
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
"
|
|
866
|
-
"
|
|
867
|
-
|
|
868
|
-
}
|
|
856
|
+
If you ran `gateway restart`, `gateway update` or `config set` on a clawops before 2.0, your
|
|
857
|
+
state is already gone, nothing was mounted to survive the container replacement. `migrate`
|
|
858
|
+
says so plainly rather than pretending to rescue it.
|
|
859
|
+
|
|
860
|
+
### `clawops backup restore` works again, and never in place
|
|
861
|
+
|
|
862
|
+
v1.7.5 made restore fail with an explanation, because the OpenClaw it supported had no
|
|
863
|
+
restore subcommand to call. 2.0 does, and clawops delegates to it:
|
|
864
|
+
|
|
865
|
+
```mermaid
|
|
866
|
+
flowchart TD
|
|
867
|
+
A["clawops backup restore --file X"] --> B["upload archive to the host"]
|
|
868
|
+
B --> C["openclaw backup restore --target <staging>"]
|
|
869
|
+
C -- "target not empty" --> C1["refused by OpenClaw"]
|
|
870
|
+
C --> D["archive verified, expanded<br/>into a fresh directory"]
|
|
871
|
+
D --> E["warnings printed verbatim<br/>time travel, channel relink,<br/>approvals, plugins"]
|
|
872
|
+
E --> F["nothing activated"]
|
|
873
|
+
F --> G["you stop the gateway, swap the<br/>state dir, restart, re-apply"]
|
|
869
874
|
```
|
|
870
875
|
|
|
871
|
-
|
|
876
|
+
clawops does not extract archives itself and does not restore in place. The final step is
|
|
877
|
+
manual on purpose, and re-applying matters: the archive does not carry plugin
|
|
878
|
+
`node_modules`, so a restored deployment starts without its model providers, looking
|
|
879
|
+
healthy while doing it.
|
|
872
880
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
| GCP | `GOOGLE_APPLICATION_CREDENTIALS` or `gcloud auth application-default login` |
|
|
877
|
-
| Azure | `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` or `az login` |
|
|
878
|
-
| Local | SSH host + key configured in `stacks[name].localOpts` |
|
|
881
|
+
**The archive is a credential.** It carries the state database, `mcp_oauth_stores`,
|
|
882
|
+
`secret_store_entries`, `worker_environment_credentials`, `device_auth_tokens`, unencrypted.
|
|
883
|
+
clawops now writes it `0600` locally; it previously used the default `0644`.
|
|
879
884
|
|
|
880
|
-
|
|
885
|
+
### Model providers that need a plugin are installed for you
|
|
881
886
|
|
|
882
|
-
|
|
887
|
+
OpenClaw 2.0 made model providers **install-gated plugins**. Twenty-four ship in the image,
|
|
888
|
+
`anthropic`, `openai`, `google`, `ollama`, `openrouter` among them, but not all of them.
|
|
889
|
+
Configuring one that is not bundled, without installing it, produces a gateway that starts,
|
|
890
|
+
reports healthy, and has no model backend.
|
|
883
891
|
|
|
884
|
-
|
|
892
|
+
clawops installs what your config needs, pinned to an exact version, **during `apply`**:
|
|
885
893
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
894
|
+
```
|
|
895
|
+
Resolving clawhub:@openclaw/deepseek-provider@2026.9.2…
|
|
896
|
+
Downloading plugin @openclaw/deepseek-provider@2026.9.2 from ClawHub…
|
|
897
|
+
Installed plugin: deepseek
|
|
898
|
+
```
|
|
890
899
|
|
|
891
|
-
|
|
900
|
+
**This adds an outbound dependency the 1.x line did not have: `clawhub.ai`.** It is needed
|
|
901
|
+
while `apply` is running, not at boot. Deliberately, so a failure reaches the person running
|
|
902
|
+
the command rather than a locked-down host at 3am. Blocked, it looks like this:
|
|
892
903
|
|
|
893
|
-
|
|
904
|
+
```
|
|
905
|
+
fetch failed | getaddrinfo EAI_AGAIN clawhub.ai | EAI_AGAIN
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
clawops checks the installed provider IDs afterwards and will not call the deploy finished
|
|
909
|
+
while a configured provider is missing. [Required outbound access](docs/security/egress.md)
|
|
910
|
+
lists every destination and when it is needed.
|
|
911
|
+
|
|
912
|
+
### Chat channels are installed for you too
|
|
913
|
+
|
|
914
|
+
Every channel in OpenClaw 2.0 is an install-gated plugin. `clawops apply` installs the ones
|
|
915
|
+
your config names, during the deploy while egress exists, and then asks the gateway whether
|
|
916
|
+
they are really installed:
|
|
894
917
|
|
|
895
918
|
```
|
|
896
|
-
clawops
|
|
897
|
-
|
|
898
|
-
├── src/config/ ~/.clawops/config.json management
|
|
899
|
-
├── src/providers/ Cloud adapters (AWS, GCP, Azure, local)
|
|
900
|
-
│ ├── aws/ Pulumi inline program + ProviderAdapter
|
|
901
|
-
│ ├── gcp/
|
|
902
|
-
│ ├── azure/
|
|
903
|
-
│ └── local/ SSH bootstrap (no Pulumi)
|
|
904
|
-
├── src/pulumi/ Pulumi Automation API wrapper + output helpers
|
|
905
|
-
├── src/transport/ SSH client (ssh2) + connection pool + tunnels
|
|
906
|
-
├── src/mcp/ MCP server, tool handlers, progress tracking
|
|
907
|
-
├── src/plan/ Maker plan generation, AJV validation, apply
|
|
908
|
-
├── src/output/ ASCII table, spinner, JSON, human-readable output
|
|
909
|
-
├── src/errors/ Typed error hierarchy with exit codes
|
|
910
|
-
└── spec/ Machine-readable ground truth (JSON Schema, YAML)
|
|
919
|
+
[clawops] warning: the gateway is running, but these configured channels are not installed:
|
|
920
|
+
discord. They will never connect.
|
|
911
921
|
```
|
|
912
922
|
|
|
913
|
-
|
|
923
|
+
It has to ask. `openclaw channels add`. The obvious command, returns success even when the
|
|
924
|
+
plugin install fails, so clawops uses `openclaw plugins install` and verifies against
|
|
925
|
+
`channels list --all --json`.
|
|
914
926
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
- **Plan → apply discipline:** every non-local deployment goes through `generatePlan()` → review → `applyPlan()`; destructive changes always require human review of the plan JSON
|
|
919
|
-
- **MCP-first:** every CLI operation has a typed MCP tool; schemas generated from `spec/mcp-tools.yaml`; all destructive tools use elicitation
|
|
927
|
+
Channel plugins are pinned to the supported runtime. The current `latest` does not install on
|
|
928
|
+
it: `plugin "discord" requires plugin API >=2026.9.3, but this OpenClaw runtime exposes
|
|
929
|
+
2026.9.2`. The same drift that forced version pins on model providers.
|
|
920
930
|
|
|
921
|
-
|
|
931
|
+
Telegram needs nothing installed: it ships in the image.
|
|
922
932
|
|
|
923
|
-
###
|
|
933
|
+
### Bad config is caught before it is written
|
|
924
934
|
|
|
925
|
-
|
|
935
|
+
Config is validated against **OpenClaw's own schema**, captured from the image, not
|
|
936
|
+
hand-written, before anything is sent to the host, and again before a write replaces a
|
|
937
|
+
working file. `clawops plan` refuses a plan whose config the gateway would reject, while the
|
|
938
|
+
plan is still a file you can edit.
|
|
926
939
|
|
|
927
|
-
|
|
940
|
+
A rejected config is kept at `<path>.rejected.<timestamp>` and the live one is left alone, so
|
|
941
|
+
a validation failure never costs you what you were trying to write.
|
|
928
942
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
subgraph NET["Networking"]
|
|
932
|
-
VPC["VPC (10.0.0.0/16)"]
|
|
933
|
-
IGW[Internet Gateway]
|
|
934
|
-
SUBNET["Subnet (10.0.1.0/24)"]
|
|
935
|
-
RT[Route Table]
|
|
936
|
-
SG["Security Group (ports 22, 18789)"]
|
|
937
|
-
end
|
|
938
|
-
subgraph IAM["IAM"]
|
|
939
|
-
ROLE[IAM Role]
|
|
940
|
-
SSM[SSM Policy Attachment]
|
|
941
|
-
BED["Bedrock Policy Attachment (optional)"]
|
|
942
|
-
IP[Instance Profile]
|
|
943
|
-
end
|
|
944
|
-
subgraph COMPUTE["Compute"]
|
|
945
|
-
KP[EC2 Key Pair]
|
|
946
|
-
EC2["EC2 Instance (Ubuntu 22.04, IMDSv2)"]
|
|
947
|
-
EIP[Elastic IP]
|
|
948
|
-
end
|
|
949
|
-
```
|
|
943
|
+
One rule is clawops's own: `gateway.mode` is optional in the schema and **mandatory in
|
|
944
|
+
practice**. A config without it passes `openclaw config validate` and then exits 78.
|
|
950
945
|
|
|
951
|
-
|
|
946
|
+
### Containers are hardened
|
|
952
947
|
|
|
953
|
-
|
|
948
|
+
The gateway runs with `--cap-drop=ALL`, `--security-opt no-new-privileges`, `--init` and
|
|
949
|
+
`--pids-limit 512`. State is owned numerically by `1000:1000`, matching the container's user
|
|
950
|
+
rather than a host account that may not have that uid.
|
|
954
951
|
|
|
955
|
-
|
|
956
|
-
flowchart LR
|
|
957
|
-
subgraph NET["Networking"]
|
|
958
|
-
NW[VPC Network]
|
|
959
|
-
SN["Subnetwork (10.0.0.0/24)"]
|
|
960
|
-
FW1["Firewall: SSH port 22 (conditional)"]
|
|
961
|
-
FW2["Firewall: Gateway port 18789 (conditional)"]
|
|
962
|
-
ADDR[Static External IP]
|
|
963
|
-
end
|
|
964
|
-
subgraph COMPUTE["Compute"]
|
|
965
|
-
VM["Compute Instance (Debian 12, 20 GB)"]
|
|
966
|
-
end
|
|
967
|
-
```
|
|
952
|
+
### The version pin is enforced everywhere it can change
|
|
968
953
|
|
|
969
|
-
|
|
954
|
+
`doctor`, `plan`, `up` and `apply` refuse an OpenClaw release outside the supported range, and
|
|
955
|
+
`gateway restart` reuses the version already deployed rather than resolving a moving tag. A
|
|
956
|
+
restart changes neither the version nor who can reach it.
|
|
970
957
|
|
|
971
|
-
|
|
958
|
+
### The gateway is no longer exposed to your network
|
|
959
|
+
|
|
960
|
+
The container publishes on `127.0.0.1:18789` instead of `0.0.0.0:18789`. Reach it with
|
|
961
|
+
`clawops tunnel` or a reverse proxy on the host.
|
|
962
|
+
|
|
963
|
+
Previously the wizard set `allowedGatewayCidrs` from the CIDR you gave for **SSH**, so a
|
|
964
|
+
plaintext HTTP dashboard. Token in the URL. Was opened to your whole shell-access network
|
|
965
|
+
as a side effect of one unrelated answer. To bind all interfaces deliberately, set
|
|
966
|
+
`network.publishGateway: "all"`.
|
|
967
|
+
|
|
968
|
+
**You must act if** a client or reverse proxy on another machine reaches the gateway
|
|
969
|
+
directly, or external monitoring hits `/health`. A proxy on the host is unaffected; one in a
|
|
970
|
+
*container* on the host needs `--network host`.
|
|
971
|
+
|
|
972
|
+
### Health checks can actually fail
|
|
973
|
+
|
|
974
|
+
The gateway serves its Control UI on a catch-all route, so **any unmatched path answers 200
|
|
975
|
+
with HTML**:
|
|
972
976
|
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
subgraph NET["Networking"]
|
|
977
|
-
VNET["Virtual Network (10.0.0.0/16)"]
|
|
978
|
-
SUBNET["Subnet (10.0.1.0/24)"]
|
|
979
|
-
NSG["Network Security Group (ports 22, 18789)"]
|
|
980
|
-
PIP["Public IP Address (Static)"]
|
|
981
|
-
NIC[Network Interface]
|
|
982
|
-
end
|
|
983
|
-
subgraph COMPUTE["Compute"]
|
|
984
|
-
VM["VM (Ubuntu 22.04, managed identity)"]
|
|
985
|
-
end
|
|
986
|
-
subgraph KV["Key Vault (optional)"]
|
|
987
|
-
VAULT["Key Vault (RBAC, name max 24 chars)"]
|
|
988
|
-
RA["Role Assignment (Secrets User)"]
|
|
989
|
-
SECRET["Secret: gateway-token"]
|
|
990
|
-
end
|
|
977
|
+
```
|
|
978
|
+
/healthz 200 application/json {"ok":true,"status":"live"}
|
|
979
|
+
/health-typo 200 text/html <!doctype html>…
|
|
991
980
|
```
|
|
992
981
|
|
|
993
|
-
|
|
982
|
+
clawops probed with `curl -fsS … >/dev/null`, which succeeds on a typo. It proved something
|
|
983
|
+
was listening on the port, not that the gateway was healthy. Probes now read the response
|
|
984
|
+
body, and the restart gate uses `/startupz` rather than liveness, after a restart the
|
|
985
|
+
process listens long before startup finishes.
|
|
994
986
|
|
|
995
|
-
|
|
987
|
+
### `clawops mcp wire` actually wires something now
|
|
996
988
|
|
|
997
|
-
|
|
989
|
+
It has never worked, not on 2.0, not on any 1.x release. It wrote `gateway.mcpClients`,
|
|
990
|
+
which is **not a key OpenClaw has**: checked against the config schemas of `2026.4.5`,
|
|
991
|
+
`2026.7.1-2` and `2026.9.2`. The real key is top-level `mcp.servers`. And the entry it wrote
|
|
992
|
+
was `command: "clawops"` over stdio, which spawns *inside the gateway container*, where
|
|
993
|
+
clawops is not installed and nothing installs it.
|
|
998
994
|
|
|
999
|
-
|
|
995
|
+
On 1.x nothing validated the write, so clawops stored a key nothing read, restarted your
|
|
996
|
+
gateway, and reported: *"The gateway's AI can now run clawops commands."* It could not.
|
|
1000
997
|
|
|
1001
|
-
```
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
998
|
+
```mermaid
|
|
999
|
+
flowchart TD
|
|
1000
|
+
A["clawops mcp wire"] --> B["openclaw mcp add --transport streamable-http"]
|
|
1001
|
+
B --> C{"gateway connects<br/>to the URL?"}
|
|
1002
|
+
C -- no --> C1["probe fails, nothing saved,<br/>clawops prints the reason"]
|
|
1003
|
+
C -- yes --> D["saved to mcp.servers.clawops"]
|
|
1004
|
+
D --> E["openclaw mcp reload"]
|
|
1007
1005
|
```
|
|
1008
1006
|
|
|
1009
|
-
|
|
1007
|
+
It delegates to `openclaw mcp add` now, which **probes the server before saving**, so
|
|
1008
|
+
"wired" means the gateway connected, not that a file was written.
|
|
1009
|
+
|
|
1010
|
+
**You have to run the server yourself.** clawops is not installed on the gateway host:
|
|
1010
1011
|
|
|
1011
1012
|
```bash
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
pnpm test # vitest (1669 tests, ~12s)
|
|
1015
|
-
pnpm test:changed # vitest --changed (fast edit loop)
|
|
1016
|
-
pnpm test:integration # Docker-based SSH integration tests
|
|
1017
|
-
pnpm typecheck # tsc --noEmit
|
|
1018
|
-
pnpm lint # eslint src/ tests/ scripts/ (--max-warnings=0)
|
|
1019
|
-
pnpm gen:schemas # regenerate src/providers/types.ts + src/mcp/tools/_generated.ts
|
|
1020
|
-
pnpm gen:schemas --check # CI guard: committed generated files match spec
|
|
1021
|
-
pnpm graph # local coupling report (--base <ref> for this branch's delta)
|
|
1022
|
-
pnpm verify:pack # install the packed tarball elsewhere and run it (CI gate)
|
|
1023
|
-
pnpm changeset # record a release note before merging
|
|
1013
|
+
clawops mcp serve --http 18790 --bind 0.0.0.0 --token "$(openssl rand -hex 16)"
|
|
1014
|
+
clawops mcp wire --stack prod --token <same token>
|
|
1024
1015
|
```
|
|
1025
1016
|
|
|
1026
|
-
|
|
1017
|
+
Installing clawops on the gateway host is a deliberate follow-up, not part of 2.0: it puts
|
|
1018
|
+
deployment credentials on the deployed box, and the gateway's AI is reachable from every
|
|
1019
|
+
channel it is connected to. See `docs/security/threat-model.md` T11.
|
|
1027
1020
|
|
|
1028
|
-
|
|
1029
|
-
|---|---|
|
|
1030
|
-
| `spec/` | Machine-readable ground truth: JSON Schema, YAML. **Treat as source of truth.** |
|
|
1031
|
-
| `SPEC.md` | Full technical specification (milestones, rules, schemas) |
|
|
1032
|
-
| `DESIGN_RULES.md` | 25 normative rules (R1–R25) referenced throughout the codebase |
|
|
1033
|
-
| `docs/architecture.md` | Narrative system overview |
|
|
1034
|
-
| `docs/plan-apply.md` | Plan/apply semantics, drift guidance, CI pattern |
|
|
1035
|
-
| `docs/ci.md` | CI integration guide: OIDC, env vars, plan → apply in CI |
|
|
1036
|
-
| `docs/security/` | MCP safety model, tool risk matrix, redaction, audit logs |
|
|
1037
|
-
| `docs/providers/matrix.md` | Per-provider capability matrix |
|
|
1038
|
-
| `docs/decisions/` | Architecture Decision Records |
|
|
1039
|
-
| `.claude/skills/` | Invokable procedures: `/add-provider`, `/release`, `/tdd`, `/mcp-tool` |
|
|
1040
|
-
| `.claude/rules/` | Path-scoped lint rules loaded by Claude Code |
|
|
1021
|
+
### `clawops mcp serve --http` serves more than one client, and asks who you are
|
|
1041
1022
|
|
|
1042
|
-
|
|
1023
|
+
Two bugs, found by testing against a real gateway rather than a mock.
|
|
1043
1024
|
|
|
1044
|
-
|
|
1025
|
+
It built **one transport for the whole process**, so the first client to connect claimed it
|
|
1026
|
+
and every later one. A second editor, a reconnect, the gateway's own probe, was answered
|
|
1027
|
+
`"Server already initialized"`. HTTP mode is the multi-client mode.
|
|
1028
|
+
|
|
1029
|
+
It had **no authentication**, while exposing every tool including `clawops_destroy`. It now
|
|
1030
|
+
takes a bearer token, compares it in constant time, and refuses to bind anywhere but loopback
|
|
1031
|
+
without one.
|
|
1045
1032
|
|
|
1046
|
-
|
|
1047
|
-
- `src/mcp/tools/_generated.ts` — Zod schemas and type exports from `spec/mcp-tools.yaml`
|
|
1033
|
+
### The firewall follows the deployment
|
|
1048
1034
|
|
|
1049
|
-
|
|
1035
|
+
```mermaid
|
|
1036
|
+
flowchart TD
|
|
1037
|
+
A["clawops plan"] --> B{"publishGateway?"}
|
|
1038
|
+
B -- "loopback (default)" --> C{"allowedGatewayCidrs empty?"}
|
|
1039
|
+
C -- no --> C1["refuse: those rules would admit<br/>traffic to a closed port"]
|
|
1040
|
+
C -- yes --> D["SSH rules only"]
|
|
1041
|
+
B -- all --> E["SSH rules + gateway rules<br/>on spec.network.gatewayPort"]
|
|
1042
|
+
D --> F["clawops harden"]
|
|
1043
|
+
E --> F
|
|
1044
|
+
F --> G["read the container's port bindings"]
|
|
1045
|
+
G --> H{"published to the network?"}
|
|
1046
|
+
H -- no --> H1["ufw: SSH only"]
|
|
1047
|
+
H -- yes --> H2["ufw: SSH + the published port"]
|
|
1048
|
+
```
|
|
1050
1049
|
|
|
1051
|
-
|
|
1050
|
+
Three security controls were doing the opposite of what they say.
|
|
1052
1051
|
|
|
1053
|
-
|
|
1052
|
+
**`clawops harden` opened the gateway port on every deployment.** The `ufw` module ran
|
|
1053
|
+
`ufw allow 18789/tcp` unconditionally. Since the gateway publishes on `127.0.0.1`, that
|
|
1054
|
+
opened a port nothing was listening on. A hardening step widening the firewall past what the
|
|
1055
|
+
deployment exposes. It now reads the running container's port bindings and adds the rule only
|
|
1056
|
+
when the gateway is really published, on whatever port it is published on.
|
|
1054
1057
|
|
|
1055
|
-
|
|
1058
|
+
**The AWS security-group audit exempted the two ports it exists to check.** Ports 22 and
|
|
1059
|
+
18789 were on an "expected" list, so a group opening SSH *or the gateway* to `0.0.0.0/0` came
|
|
1060
|
+
back as "No unexpected open ingress rules found". It also never read IPv6 rules, so `::/0`
|
|
1061
|
+
was invisible.
|
|
1056
1062
|
|
|
1057
|
-
|
|
1063
|
+
**The setup wizard defaulted SSH access to `0.0.0.0/0`.** Pressing Enter opened SSH to the
|
|
1064
|
+
whole internet, on the path most first-time users take. It offers your own IP as a `/32` now,
|
|
1065
|
+
and when that cannot be detected it offers no default and requires an answer.
|
|
1058
1066
|
|
|
1059
|
-
|
|
1067
|
+
**`clawops plan` could not express any of it, and `apply` never passed any of it to Pulumi.**
|
|
1068
|
+
Both are fixed in 2.0.1. See the list at the top of this section.
|
|
1060
1069
|
|
|
1070
|
+
### The gateway port comes from the plan
|
|
1071
|
+
|
|
1072
|
+
```jsonc
|
|
1073
|
+
"network": {
|
|
1074
|
+
"allowedSshCidrs": ["203.0.113.4/32"],
|
|
1075
|
+
"allowedGatewayCidrs": [],
|
|
1076
|
+
"publishGateway": "loopback",
|
|
1077
|
+
"gatewayPort": 9443
|
|
1078
|
+
}
|
|
1061
1079
|
```
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1080
|
+
|
|
1081
|
+
One value now reaches the security-group rules, the container publish flag, the default
|
|
1082
|
+
`gateway.port` and the gateway URL. It was a constant redeclared in eleven places, so
|
|
1083
|
+
changing it meant finding all of them, and missing one produced a container publishing one
|
|
1084
|
+
port, a gateway listening on another, and a firewall opening a third.
|
|
1085
|
+
|
|
1086
|
+
Local deployments use `clawops up --gateway-port 9443`.
|
|
1087
|
+
|
|
1088
|
+
### `clawops doctor` answers whether it works, and says so in its exit code
|
|
1089
|
+
|
|
1090
|
+
```mermaid
|
|
1091
|
+
flowchart TD
|
|
1092
|
+
A["clawops doctor"] --> B["local: Node, Pulumi CLI + home,<br/>config, SSH key, credentials"]
|
|
1093
|
+
B --> C{"--stack given?"}
|
|
1094
|
+
C -- no --> Z["report"]
|
|
1095
|
+
C -- yes --> D["container state"]
|
|
1096
|
+
D --> E["deployed OpenClaw version"]
|
|
1097
|
+
E --> F["probe /startupz<br/>and read the body"]
|
|
1098
|
+
F --> G["published scope, disk,<br/>log rotation, hardening drift"]
|
|
1099
|
+
G --> Z
|
|
1100
|
+
Z --> Y{"any check failed?"}
|
|
1101
|
+
Y -- no --> Y1["exit 0"]
|
|
1102
|
+
Y -- yes --> Y2["exit 1"]
|
|
1065
1103
|
```
|
|
1066
1104
|
|
|
1067
|
-
|
|
1105
|
+
Three changes:
|
|
1106
|
+
|
|
1107
|
+
**It asks the gateway.** `doctor` used to read `docker inspect`'s healthcheck field, which
|
|
1108
|
+
the OpenClaw image does not set, so it reported "no healthcheck configured" and moved on. A
|
|
1109
|
+
running container means the process started, not that it serves. It now probes `/startupz`
|
|
1110
|
+
and reads the body.
|
|
1111
|
+
|
|
1112
|
+
**It exits 1 when something failed.** Only an old Node.js used to do that; an unreadable SSH
|
|
1113
|
+
key or an unsupported gateway exited 0, so a CI step running `clawops doctor` read a broken
|
|
1114
|
+
deployment as success. Warnings still exit 0, a fresh machine with no stacks is
|
|
1115
|
+
unconfigured, not broken.
|
|
1116
|
+
|
|
1117
|
+
**It is an MCP tool.** `clawops_doctor` returns the same report as structured data, so an
|
|
1118
|
+
agent that hits a failure can find out why. It reports only; it never runs `openclaw doctor
|
|
1119
|
+
--fix`. `--json` gives the CLI the same report.
|
|
1120
|
+
|
|
1121
|
+
### `clawops agents list` stops inventing an empty list
|
|
1122
|
+
|
|
1123
|
+
The command ended in `|| echo '[]'`, so a stopped container, a gateway still starting, or a
|
|
1124
|
+
Docker permission error all produced **"No agents running."**, a wrong answer rather than an
|
|
1125
|
+
error. It now fails, and says which.
|
|
1126
|
+
|
|
1127
|
+
### Day-two commands work on AWS
|
|
1128
|
+
|
|
1129
|
+
`gateway restart`, `logs`, `monitor`, `backup`, `agents`, `config set` and `doctor`'s
|
|
1130
|
+
container checks were **all broken on AWS**: clawops connects as `ubuntu`, but provisioning
|
|
1131
|
+
only put `clawops` in the docker group, so every Docker command failed with `permission
|
|
1132
|
+
denied`. GCP and Azure connect as `clawops`, so only AWS was affected.
|
|
1133
|
+
|
|
1134
|
+
### Removed
|
|
1135
|
+
|
|
1136
|
+
**`clawops agents restart`** and the `clawops_agents_restart` MCP tool. OpenClaw 2.0 has no
|
|
1137
|
+
per-agent restart, only `gateway restart` and `daemon restart`, both of which interrupt
|
|
1138
|
+
every agent on the host. Use `clawops gateway restart`, or stay on `@clawops/cli@legacy`.
|
|
1139
|
+
|
|
1140
|
+
`clawops agents list` and `clawops agents logs` are unaffected.
|
|
1068
1141
|
|
|
1069
1142
|
---
|
|
1070
1143
|
|
|
@@ -1072,14 +1145,14 @@ Use `pnpm changeset` to record a release note before merging a `feat` or `fix`.
|
|
|
1072
1145
|
|
|
1073
1146
|
| Milestone | Status | What ships |
|
|
1074
1147
|
|---|---|---|
|
|
1075
|
-
| M0
|
|
1076
|
-
| M1
|
|
1077
|
-
| M2
|
|
1078
|
-
| M3
|
|
1079
|
-
| M4
|
|
1080
|
-
| M5
|
|
1081
|
-
| M6
|
|
1082
|
-
| M7
|
|
1148
|
+
| M0: Scaffold | ✅ | Tooling, CI, stubs, generated types |
|
|
1149
|
+
| M1: GCP MVP | ✅ | `init` / `up` / `down` / `status` / `ssh` / `logs` on GCP |
|
|
1150
|
+
| M2: Remote Mgmt | ✅ | `tunnel`, `config`, `agents`, `gateway`; SSH connection pool |
|
|
1151
|
+
| M3: AWS + Azure | ✅ | AWS EC2 + Azure VM adapters; `stacks list` |
|
|
1152
|
+
| M4: Local VM | ✅ | Local adapter (SSH bootstrap, no Pulumi); `doctor` |
|
|
1153
|
+
| M5: MCP Layer | ✅ | `mcp serve` (stdio), all CLI ops as MCP tools, progress tracking |
|
|
1154
|
+
| M6: Plan/Apply | ✅ | `plan` + `apply`; deploy-plan schema; MCP HTTP transport; `workflow_deploy_app` |
|
|
1155
|
+
| M7: v1.0 Polish | ✅ | Full `doctor` surface; `destroy` command; `--dry-run` across commands; CI guide |
|
|
1083
1156
|
|
|
1084
1157
|
See [`docs/roadmap.md`](docs/roadmap.md) for the public roadmap and upcoming work.
|
|
1085
1158
|
|
|
@@ -1087,4 +1160,4 @@ See [`docs/roadmap.md`](docs/roadmap.md) for the public roadmap and upcoming wor
|
|
|
1087
1160
|
|
|
1088
1161
|
## License
|
|
1089
1162
|
|
|
1090
|
-
MPL-2.0
|
|
1163
|
+
MPL-2.0, see [LICENSE](LICENSE).
|