@clawops/cli 1.7.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +504 -76
  2. package/dist/apply-KYFPWI5U.js +27 -0
  3. package/dist/automation-RMAIBNVC.js +44 -0
  4. package/dist/bedrock-XVASJX2K.js +61 -0
  5. package/dist/{bootstrap-HL4DNUNG.js → bootstrap-HNGKY6WW.js} +31 -19
  6. package/dist/bootstrap.sh.tmpl +51 -31
  7. package/dist/channels-KBUR42IU.js +53 -0
  8. package/dist/chunk-2I7KCMXZ.js +142 -0
  9. package/dist/{chunk-DQDAWNGB.js → chunk-2Z7J3ERQ.js} +1 -1
  10. package/dist/chunk-3OLUJPCB.js +36 -0
  11. package/dist/chunk-5XT5IAJK.js +69 -0
  12. package/dist/chunk-AEEMRFZV.js +24 -0
  13. package/dist/chunk-BSLIYB5Q.js +106 -0
  14. package/dist/{chunk-3XK66DKK.js → chunk-E4NJSJ54.js} +57 -93
  15. package/dist/chunk-FEK7VJAY.js +16 -0
  16. package/dist/{chunk-UKTP332Z.js → chunk-GAZPPSKU.js} +6 -7
  17. package/dist/chunk-GIEVAGEK.js +126 -0
  18. package/dist/chunk-HSPHXG6W.js +67 -0
  19. package/dist/chunk-IVWP4QUJ.js +901 -0
  20. package/dist/{chunk-PVUVI35Y.js → chunk-JW5A7E3X.js} +5 -1
  21. package/dist/{chunk-GZN7PKP4.js → chunk-KC3GC3PE.js} +328 -121
  22. package/dist/chunk-LBE6COC4.js +244 -0
  23. package/dist/chunk-LEVIW2PV.js +890 -0
  24. package/dist/chunk-LT4XMO5D.js +50 -0
  25. package/dist/chunk-MC5ATCXS.js +108 -0
  26. package/dist/chunk-N4WZLWJ4.js +16 -0
  27. package/dist/chunk-NUDIHZEH.js +55 -0
  28. package/dist/chunk-NUL5F6YV.js +56 -0
  29. package/dist/chunk-RHOWYIWT.js +52 -0
  30. package/dist/chunk-STLRHLLV.js +193 -0
  31. package/dist/chunk-V25QRZRB.js +45 -0
  32. package/dist/chunk-VOEJYRIK.js +33 -0
  33. package/dist/{providers-2OABPW2E.js → chunk-VUFMZEIM.js} +3 -2
  34. package/dist/chunk-W3OLNNJW.js +51 -0
  35. package/dist/cli-WLD3EBEM.js +11 -0
  36. package/dist/cli-auth-GD5SK7JU.js +13 -0
  37. package/dist/cli.js +1296 -695
  38. package/dist/config-validate-K2N6XLDX.js +76 -0
  39. package/dist/context-7EVC6W45.js +20 -0
  40. package/dist/docker-3PDO4TS7.js +12 -0
  41. package/dist/{firewall-XQGO7HWK.js → firewall-5VKZ4WBH.js} +32 -1
  42. package/dist/generate-I6YQ5V2T.js +29 -0
  43. package/dist/{harden-URPXLSVF.js → harden-72JMRT4E.js} +51 -11
  44. package/dist/health-5CCOXYGG.js +11 -0
  45. package/dist/index.d.ts +56 -0
  46. package/dist/known-hosts-file-I3Q3NBYQ.js +22 -0
  47. package/dist/logs-5A7YYA53.js +15 -0
  48. package/dist/{mcp-wire-7E5JQY47.js → mcp-wire-3SWBM7D6.js} +32 -16
  49. package/dist/models-AGTQWYDL.js +32 -0
  50. package/dist/network-args-PYJRJBYO.js +64 -0
  51. package/dist/{package-5AR4ZPK4.js → package-5BCMEBTE.js} +11 -4
  52. package/dist/passphrase-MJBE45N7.js +15 -0
  53. package/dist/plugins-DTST3OER.js +49 -0
  54. package/dist/{pool-QGFQRMGU.js → pool-PIIKJ4L2.js} +3 -2
  55. package/dist/preflight-EF52S3WW.js +162 -0
  56. package/dist/preflight-MWWM5JT3.js +181 -0
  57. package/dist/preflight-TFD4N5ZU.js +141 -0
  58. package/dist/privileged-JFA6LLTV.js +17 -0
  59. package/dist/providers-FW6H6OPF.js +11 -0
  60. package/dist/ready-KDNRBNSU.js +95 -0
  61. package/dist/{remote-config-QDLN4UMG.js → remote-config-3HZLUWEA.js} +4 -3
  62. package/dist/runtime-X3KW5TCP.js +44 -0
  63. package/dist/server-GBA7BJGK.js +46 -0
  64. package/dist/spec-path-KQOOIQCA.js +9 -0
  65. package/dist/ssh-VAI5DU5M.js +11 -0
  66. package/dist/ssh-key-DUO6OKOI.js +7 -0
  67. package/dist/stack-config-R4SPSUQZ.js +12 -0
  68. package/dist/upgrade-ULD4KFYC.js +88 -0
  69. package/dist/validate-YVI3BJ55.js +14 -0
  70. package/dist/{version-guard-VUCNZXQS.js → version-guard-XVYW5WFJ.js} +2 -2
  71. package/dist/{versions-2UVAT6PQ.js → versions-W5LRQCAJ.js} +2 -2
  72. package/dist/wait-WO72TRRQ.js +79 -0
  73. package/package.json +11 -4
  74. package/spec/deploy-plan.schema.json +114 -25
  75. package/spec/integrations.yaml +135 -28
  76. package/spec/mcp-tools.yaml +89 -37
  77. package/spec/models.yaml +23 -3
  78. package/spec/openclaw-2.0.config.schema.json +1 -0
  79. package/spec/openclaw-versions.yaml +116 -27
  80. package/spec/providers.schema.json +163 -28
  81. package/dist/apply-F3KY2HTB.js +0 -17
  82. package/dist/automation-RULUSJBW.js +0 -30
  83. package/dist/aws-L4RR2LSE.js +0 -254
  84. package/dist/azure-7OXQENQL.js +0 -262
  85. package/dist/chunk-35WVQY3X.js +0 -122
  86. package/dist/chunk-6X45DDGH.js +0 -42
  87. package/dist/chunk-A4QR4TXW.js +0 -120
  88. package/dist/chunk-C5X375RO.js +0 -85
  89. package/dist/chunk-FNS56UQY.js +0 -741
  90. package/dist/chunk-P5LKWRPL.js +0 -114
  91. package/dist/chunk-SEZMVVAS.js +0 -156
  92. package/dist/context-5LCUG2QR.js +0 -10
  93. package/dist/gcp-UG6AWPIZ.js +0 -193
  94. package/dist/generate-OAGYT3VH.js +0 -15
  95. package/dist/local-DXBEVZ5C.js +0 -47
  96. package/dist/server-FIZCOLHL.js +0 -32
  97. package/dist/ssh-CNJNLM3E.js +0 -8
  98. package/dist/validate-CFQ4FH2T.js +0 -10
package/README.md CHANGED
@@ -12,85 +12,499 @@ and Cursor drive them through typed MCP tools with explicit safety controls.
12
12
 
13
13
  ---
14
14
 
15
- ## What's new in v1.7.8
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.
52
+
53
+ ### Naming, config and setup
54
+
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`.
67
+
68
+ ### While a deploy is running
69
+
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.
74
+
75
+ ### Day-two commands
76
+
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.
84
+
85
+ ### Documentation
86
+
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.
91
+
92
+ ## What's new in 2.0
93
+
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**:
16
96
 
17
- **`clawops mcp wire` never wired anything.** It wrote `gateway.mcpClients` — a key OpenClaw
18
- does not have, checked against the config schemas of `2026.4.5`, `2026.7.1-2` and `2026.9.2`.
19
- The real key is top-level `mcp.servers`. Nothing on this line validated the write, so clawops
20
- stored a key nothing read, restarted your gateway, and reported success. The entry also used
21
- stdio with `command: "clawops"`, which spawns inside the gateway container where clawops is
22
- not installed — so even the right key would not have worked.
97
+ ```bash
98
+ npm install -g @clawops/cli # 2.x
99
+ npm install -g @clawops/cli@legacy # 1.x maintenance
100
+ ```
101
+
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
107
+
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.
111
+
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`.
115
+
116
+ ### `clawops up` / `clawops apply`
117
+
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
+ ```
136
+
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.
141
+
142
+ ### `clawops gateway update`
143
+
144
+ Previously: pull, run, report success. `docker run` exiting 0 means the container was
145
+ *created* — and the container it replaced is already gone.
146
+
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
+ ```
166
+
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.
170
+
171
+ ### `clawops gateway restart`
172
+
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:
175
+
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
+ ```
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"]
207
+ ```
208
+
209
+ Two things about that shape are not obvious, and both came from running a real migration:
210
+
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.
213
+
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.
217
+
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.
220
+
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.
224
+
225
+ ### `clawops backup restore` works again, and never in place
226
+
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:
229
+
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"]
239
+ ```
240
+
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.
245
+
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`.
249
+
250
+ ### Model providers that need a plugin are installed for you
251
+
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.
256
+
257
+ clawops installs what your config needs, pinned to an exact version, **during `apply`**:
258
+
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
263
+ ```
264
+
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:
268
+
269
+ ```
270
+ fetch failed | getaddrinfo EAI_AGAIN clawhub.ai | EAI_AGAIN
271
+ ```
272
+
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.
276
+
277
+ ### Chat channels are installed for you too
278
+
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.
286
+ ```
23
287
 
24
- It delegates to `openclaw mcp add` now, which probes the server before saving: "wired" means
25
- the gateway connected. You run the server yourself, since clawops is not on the gateway host:
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.
304
+
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.
307
+
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.
310
+
311
+ ### Containers are hardened
312
+
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.
316
+
317
+ ### The version pin is enforced everywhere it can change
318
+
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.
322
+
323
+ ### The gateway is no longer exposed to your network
324
+
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.
327
+
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"`.
332
+
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`.
336
+
337
+ ### Health checks can actually fail
338
+
339
+ The gateway serves its Control UI on a catch-all route, so **any unmatched path answers 200
340
+ with HTML**:
341
+
342
+ ```
343
+ /healthz 200 application/json {"ok":true,"status":"live"}
344
+ /health-typo 200 text/html <!doctype html>…
345
+ ```
346
+
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.
351
+
352
+ ### `clawops mcp wire` actually wires something now
353
+
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.
359
+
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.
362
+
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
+ ```
371
+
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.
374
+
375
+ **You have to run the server yourself.** clawops is not installed on the gateway host:
26
376
 
27
377
  ```bash
28
378
  clawops mcp serve --http 18790 --bind 0.0.0.0 --token "$(openssl rand -hex 16)"
29
379
  clawops mcp wire --stack prod --token <same token>
30
380
  ```
31
381
 
32
- **`clawops mcp serve --http` now requires a token off loopback — this may stop your server.**
33
- It had no authentication at all while exposing every tool, `clawops_destroy` included. It now
34
- requires a bearer token and refuses to start when bound off-loopback without one. If you run
35
- it with `--bind 0.0.0.0`, pass `--token` (or set `CLAWOPS_MCP_TOKEN`) before upgrading.
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.
36
385
 
37
- **And it only ever served one client.** One transport for the whole process meant the first
38
- client to connect claimed it; every later one got `"Server already initialized"`.
386
+ ### `clawops mcp serve --http` serves more than one client, and asks who you are
39
387
 
40
- ---
388
+ Two bugs, found by testing against a real gateway rather than a mock.
41
389
 
42
- ## What's new in v1.7.3
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.
43
393
 
44
- **README maintenance.** The release-notes section had grown to four versions and was missing
45
- v1.7.2 entirely. It now carries the current line only; [`CHANGELOG.md`](CHANGELOG.md) remains the
46
- full history.
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.
47
397
 
48
- ---
398
+ ### The firewall follows the deployment
49
399
 
50
- ## What's new in v1.7.2
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"]
413
+ ```
51
414
 
52
- **Refuses OpenClaw 2.0.** OpenClaw `2026.8.1` changed the container runtime contract — state moved
53
- into SQLite, config moved to a writable path, and model providers became install-gated plugins.
54
- Deploying it from this release line produces a crash-looping gateway. `doctor`, `plan`, `up` and
55
- `apply` now refuse anything above `2026.7.1-2`.
415
+ Three security controls were doing the opposite of what they say.
56
416
 
57
- The support range in `spec/openclaw-versions.yaml` was previously unbounded *and read by no code*,
58
- so any OpenClaw release was accepted. Moving tags are now resolved to a concrete version **before**
59
- the range check, and an unresolved tag is refused rather than assumed safe — `latest` and `stable`
60
- both point at 2.0 today. The default is now a concrete pin.
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.
61
422
 
62
- `clawops doctor --stack <name>` reports the version a deployed gateway is actually running, so an
63
- existing deployment that already picked up 2.0 through a moving tag can be identified.
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.
64
427
 
65
- **`clawops config set` now applies.** It never has. The config clawops mounted was read by nothing
66
- on either OpenClaw line — models, channels and auth mode were silently discarded. Setting
67
- `OPENCLAW_CONFIG_PATH` fixes it, with four guards: `gateway.port` normalisation (with a warning),
68
- an argv `--port` pin, a parse check, and a post-restart health gate. The MCP `gateway restart`
69
- tool, which dropped the config mount entirely, now matches the CLI path.
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.
70
431
 
71
- **A fresh local deployment starts.** OpenClaw refuses a non-loopback bind without auth and always
72
- binds `0.0.0.0` in a container, and the bootstrap never supplied a token — so the gateway exited 78
73
- and systemd restart-looped. A token is now generated once, kept in a `0600` env file, and passed
74
- via `--env-file`, never on the command line.
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.
75
434
 
76
- **Ollama is reachable.** `localhost` inside the container is the container. clawops now passes
77
- `--add-host=host.docker.internal:host-gateway` and defaults the Ollama address to match.
435
+ ### The gateway port comes from the plan
78
436
 
79
- **Packaging.** `spec/` was missing from the published files, and neither it nor
80
- `bootstrap.sh.tmpl` resolved from the bundle — so `clawops plan` and `clawops up --provider local`
81
- failed from an npm install. Both are now shipped and resolved correctly.
437
+ ```jsonc
438
+ "network": {
439
+ "allowedSshCidrs": ["203.0.113.4/32"],
440
+ "allowedGatewayCidrs": [],
441
+ "publishGateway": "loopback",
442
+ "gatewayPort": 9443
443
+ }
444
+ ```
82
445
 
83
- **SSH host keys.** The verifier read each `known_hosts` line's *key type* as the key, so any
84
- standard entry failed permanently with `Host denied`. Standard entries now parse, including
85
- comma-separated host lists, `[host]:port`, hashed hostnames, `@revoked` / `@cert-authority`
86
- markers, and wildcard and negated patterns.
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.
87
450
 
88
- > **Behaviour change:** a host covered by a wildcard whose key does not match is now refused where
89
- > it previously connected. Ignoring wildcards meant trust-on-first-use accepted a key your own
90
- > `known_hosts` contradicted. This matches OpenSSH.
451
+ Local deployments use `clawops up --gateway-port 9443`.
91
452
 
92
- ---
453
+ ### `clawops doctor` answers whether it works, and says so in its exit code
454
+
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"]
468
+ ```
469
+
470
+ Three changes worth knowing:
471
+
472
+ **It asks the gateway.** `doctor` used to read `docker inspect`'s healthcheck field, which
473
+ the OpenClaw image does not set — so it reported "no healthcheck configured" and moved on. A
474
+ running container means the process started, not that it serves. It now probes `/startupz`
475
+ and reads the body.
476
+
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.
481
+
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.
93
485
 
486
+ ### `clawops agents list` stops inventing an empty list
487
+
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.
491
+
492
+ ### Day-two commands work on AWS
493
+
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.
498
+
499
+ ### Removed
500
+
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`.
504
+
505
+ `clawops agents list` and `clawops agents logs` are unaffected.
506
+
507
+ ---
94
508
 
95
509
  ## Who this is for
96
510
 
@@ -107,7 +521,8 @@ markers, and wildcard and negated patterns.
107
521
  ## What clawops does
108
522
 
109
523
  - Provisions and tears down OpenClaw infrastructure on **AWS, GCP, Azure, and local VMs** using
110
- the Pulumi Automation API (embedded — no `pulumi` binary required).
524
+ the Pulumi Automation API — you do not install Pulumi; clawops installs the CLI it needs into
525
+ `~/.clawops/.pulumi-cli` on first use.
111
526
  - Manages day-to-day operations: status, logs, SSH, tunnels, config, agents, gateway, backups.
112
527
  - Exposes every operation as a **typed MCP tool** so AI agents can drive ops safely.
113
528
  - Enforces a **plan → review → apply** discipline for cloud deployments.
@@ -178,6 +593,8 @@ and prints a direct dashboard URL:
178
593
 
179
594
  **Prerequisites:** Node.js ≥ 22, an SSH key, and either an SSH-reachable Linux/macOS host or a
180
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.
181
598
 
182
599
  For a full narrated walkthrough with example output, see [`docs/demo-script.md`](docs/demo-script.md).
183
600
 
@@ -210,10 +627,14 @@ clawops init --provider aws
210
627
 
211
628
  # Edit ~/.clawops/config.json — set stateUrl to your S3 bucket
212
629
 
213
- clawops plan --provider aws --stack default --out /tmp/plan.json
630
+ clawops plan --provider aws --stack default --ssh-cidr auto --out /tmp/plan.json
214
631
  clawops apply /tmp/plan.json
215
632
  ```
216
633
 
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.
637
+
217
638
  ---
218
639
 
219
640
  ## Connect an AI editor
@@ -289,27 +710,27 @@ clawops down --yes # Destroy local-provider stack
289
710
  | Command | Description |
290
711
  |---|---|
291
712
  | `setup` | First-run wizard — guided LLM, integrations, and deploy-plan generation |
292
- | `init` | Register a stack in `~/.clawops/config.json` without provisioning |
293
- | `up` | Provision or update stack (`--dry-run` for preview) |
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) |
294
715
  | `down` | Destroy local-provider stack (requires `--yes`; `--dry-run` shows current outputs) |
295
716
  | `destroy` | Destroy cloud-provider stack with confirmation prompt (`--dry-run` shows current outputs) |
296
717
  | `status` | Show stack outputs: IP, gateway URL, region, provisioned time |
297
- | `plan` | Generate a deploy-plan JSON artifact (dry-run safe) |
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 |
298
719
  | `apply` | Apply a previously reviewed plan file (`--dry-run` validates and shows diff without applying) |
299
720
  | `ssh` | Interactive SSH session or run a remote command |
300
721
  | `logs` | Stream OpenClaw logs (`-f`, `--tail N`, `--since 5m`) |
301
722
  | `tunnel` | Local port-forward to gateway UI over SSH |
302
723
  | `config` | Get/set remote OpenClaw config values (`--dry-run` shows would-write JSON) |
303
- | `agents` | List or restart OpenClaw agents |
724
+ | `agents` | List OpenClaw agents, or stream one agent's logs |
304
725
  | `gateway` | Restart the OpenClaw gateway service |
305
- | `backup` | Create an OpenClaw state backup (`restore` returns in clawops 2.x — see [limitations](docs/limitations.md#backup-and-restore)) |
726
+ | `backup` | Create and restore OpenClaw state backups (`restore` expands into a staging directory, never in place) |
306
727
  | `stacks` | List named stacks and their state |
307
- | `doctor` | Check Node version, config, SSH key, provider credentials, and Pulumi home |
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 |
308
729
  | `secret` | Manage secrets: `list`, `set`, `delete`, `rotate`, `audit` |
309
730
  | `monitor` | Live dashboard: gateway health, container stats, log tail, stack picker |
310
- | `mcp serve` | Start the embedded MCP server (stdio or HTTP) |
731
+ | `mcp serve` | Start the embedded MCP server (stdio, or HTTP with `--http <port> --token <t>`) |
311
732
  | `mcp install` | Interactively wire clawops into AI editors |
312
- | `mcp wire` | Wire the gateway's AI as an MCP client of clawops |
733
+ | `mcp wire` | Wire the gateway's AI as an MCP client of clawops (verifies the connection before saving) |
313
734
  | `help` | List all commands and global flags |
314
735
  | `harden` | Apply security hardening to a deployed stack (SSH, UFW, fail2ban, unattended-upgrades, Docker socket; AWS: SG audit, SSM check, Flow Logs, GuardDuty) |
315
736
  | `bug` | Open a pre-filled GitHub issue with system context from `doctor` |
@@ -324,7 +745,8 @@ For non-local providers, clawops enforces a review-before-apply discipline:
324
745
 
325
746
  ```bash
326
747
  # 1. Generate a plan — runs `pulumi preview` internally, produces JSON
327
- clawops plan --provider aws --region us-east-1 --out /tmp/plan.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
328
750
 
329
751
  # 2. Review plan.json — the `diff` field shows projected changes at plan-generation time
330
752
  cat /tmp/plan.json | jq .diff
@@ -394,21 +816,26 @@ Do not bind to a non-loopback address without additional authentication controls
394
816
 
395
817
  | Tool | Toolset | Description |
396
818
  |---|---|---|
397
- | `clawops_status` | read | Show stack outputs |
398
- | `clawops_logs_tail` | read | Tail OpenClaw logs |
399
- | `clawops_config_get` | read | Read a remote config value |
400
- | `clawops_agents_list` | read | List running agents |
401
- | `clawops_task_status` | read | Poll a long-running task |
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 |
402
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 |
403
826
  | `clawops_up` | cli | Provision or update a stack |
404
- | `clawops_plan` | cli | Generate a deploy plan |
827
+ | `clawops_destroy` | cli | Destroy a stack (elicits confirmation) |
405
828
  | `clawops_apply` | cli | Apply a plan file |
406
- | `clawops_ssh_exec` | cli | Run a command over SSH |
829
+ | `clawops_plan` | cli | Generate a deploy plan |
407
830
  | `clawops_config_set` | cli | Write a remote config value |
408
- | `clawops_destroy` | cli | Destroy a stack (elicits confirmation) |
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) |
409
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 |
410
837
 
411
- `read` toolset tools are available in `--read-only` mode. All other toolsets require full mode.
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.
412
839
  Destructive tools require explicit confirmation (elicitation) unless `yes: true` is passed.
413
840
 
414
841
  See [`docs/security/tool-risk-matrix.md`](docs/security/tool-risk-matrix.md) for the full risk
@@ -485,7 +912,7 @@ clawops
485
912
 
486
913
  Key design decisions:
487
914
 
488
- - **Pulumi Automation API (embedded):** no `pulumi` binary required; Pulumi home is sandboxed to `~/.clawops/.pulumi`; stack programs are inline TypeScript closures
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
489
916
  - **State in cloud blob storage:** GCS (`gs://`), S3 (`s3://`), Azure Blob — no local state files, no `pulumi.yaml`
490
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
491
918
  - **Plan → apply discipline:** every non-local deployment goes through `generatePlan()` → review → `applyPlan()`; destructive changes always require human review of the plan JSON
@@ -584,13 +1011,14 @@ pnpm dev doctor # verify toolchain
584
1011
  ```bash
585
1012
  pnpm dev # run CLI from src/ via tsx
586
1013
  pnpm build # tsup → dist/
587
- pnpm test # vitest (786 tests, ~4s)
1014
+ pnpm test # vitest (1669 tests, ~12s)
588
1015
  pnpm test:changed # vitest --changed (fast edit loop)
589
1016
  pnpm test:integration # Docker-based SSH integration tests
590
1017
  pnpm typecheck # tsc --noEmit
591
1018
  pnpm lint # eslint src/ tests/ scripts/ (--max-warnings=0)
592
1019
  pnpm gen:schemas # regenerate src/providers/types.ts + src/mcp/tools/_generated.ts
593
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)
594
1022
  pnpm changeset # record a release note before merging
595
1023
  ```
596
1024