@clawops/cli 2.0.1 → 2.1.0

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