@clawops/cli 2.0.2 → 2.1.1

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