@clawops/cli 1.7.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +412 -70
- package/dist/{apply-F3KY2HTB.js → apply-AM5HERE7.js} +8 -6
- package/dist/{aws-L4RR2LSE.js → aws-AMON7DA7.js} +20 -9
- package/dist/{azure-7OXQENQL.js → azure-T4G6OBJQ.js} +19 -8
- package/dist/{bootstrap-HL4DNUNG.js → bootstrap-DMRPWJGN.js} +28 -19
- package/dist/bootstrap.sh.tmpl +51 -31
- package/dist/channels-KBUR42IU.js +53 -0
- package/dist/{chunk-DQDAWNGB.js → chunk-3WYA2QDM.js} +1 -1
- package/dist/chunk-6TIYVHPX.js +152 -0
- package/dist/{chunk-3XK66DKK.js → chunk-AHAAEXWC.js} +41 -0
- package/dist/{chunk-GZN7PKP4.js → chunk-AX74VXTV.js} +292 -94
- package/dist/{chunk-C5X375RO.js → chunk-CKSPROD5.js} +59 -15
- package/dist/{chunk-UKTP332Z.js → chunk-GAZPPSKU.js} +6 -7
- package/dist/{chunk-FNS56UQY.js → chunk-GYV4KNW7.js} +319 -252
- package/dist/{chunk-PVUVI35Y.js → chunk-JW5A7E3X.js} +5 -1
- package/dist/chunk-LT4XMO5D.js +50 -0
- package/dist/{chunk-P5LKWRPL.js → chunk-LU2V3LXA.js} +3 -3
- package/dist/chunk-N44K3LHE.js +140 -0
- package/dist/chunk-NUDIHZEH.js +55 -0
- package/dist/chunk-SBLQH2TJ.js +56 -0
- package/dist/chunk-UFRO4H5W.js +228 -0
- package/dist/{chunk-A4QR4TXW.js → chunk-WHJMKDV7.js} +24 -4
- package/dist/chunk-YR3O2I3S.js +101 -0
- package/dist/cli.js +866 -577
- package/dist/config-validate-K2N6XLDX.js +76 -0
- package/dist/{context-5LCUG2QR.js → context-KHKWJJMC.js} +1 -1
- package/dist/{firewall-XQGO7HWK.js → firewall-T3NTPZJD.js} +10 -0
- package/dist/{gcp-UG6AWPIZ.js → gcp-2JBTD7ZM.js} +19 -8
- package/dist/{generate-OAGYT3VH.js → generate-2XV2GSP2.js} +4 -4
- package/dist/{harden-URPXLSVF.js → harden-PCDK3UQ5.js} +48 -11
- package/dist/health-5CCOXYGG.js +11 -0
- package/dist/logs-P7YCKVF4.js +15 -0
- package/dist/{mcp-wire-7E5JQY47.js → mcp-wire-BFEY2Y6H.js} +23 -17
- package/dist/{package-5AR4ZPK4.js → package-3HUW57XH.js} +3 -3
- package/dist/plugins-DTST3OER.js +49 -0
- package/dist/{pool-QGFQRMGU.js → pool-EX4ZTA5P.js} +2 -2
- package/dist/privileged-JFA6LLTV.js +17 -0
- package/dist/{remote-config-QDLN4UMG.js → remote-config-Z6IIZR3D.js} +3 -3
- package/dist/runtime-LZ7NA53V.js +42 -0
- package/dist/{server-FIZCOLHL.js → server-A5AB67GQ.js} +16 -12
- package/dist/spec-path-KQOOIQCA.js +9 -0
- package/dist/{ssh-CNJNLM3E.js → ssh-B3NZAU4I.js} +1 -1
- package/dist/upgrade-ULD4KFYC.js +88 -0
- package/dist/validate-BOQRWX4F.js +14 -0
- package/dist/{version-guard-VUCNZXQS.js → version-guard-GPSTCPT7.js} +3 -3
- package/dist/{versions-2UVAT6PQ.js → versions-W5LRQCAJ.js} +2 -2
- package/package.json +3 -3
- package/spec/deploy-plan.schema.json +114 -25
- package/spec/integrations.yaml +135 -28
- package/spec/mcp-tools.yaml +68 -35
- package/spec/models.yaml +9 -0
- package/spec/openclaw-2.0.config.schema.json +1 -0
- package/spec/openclaw-versions.yaml +116 -27
- package/dist/chunk-35WVQY3X.js +0 -122
- package/dist/chunk-6X45DDGH.js +0 -42
- package/dist/chunk-SEZMVVAS.js +0 -156
- package/dist/validate-CFQ4FH2T.js +0 -10
package/README.md
CHANGED
|
@@ -12,85 +12,422 @@ and Cursor drive them through typed MCP tools with explicit safety controls.
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
## What's new in
|
|
15
|
+
## What's new in 2.0
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
The real key is top-level `mcp.servers`. Nothing on this line validated the write, so clawops
|
|
20
|
-
stored a key nothing read, restarted your gateway, and reported success. The entry also used
|
|
21
|
-
stdio with `command: "clawops"`, which spawns inside the gateway container where clawops is
|
|
22
|
-
not installed — so even the right key would not have worked.
|
|
17
|
+
clawops 2.x targets **OpenClaw >= 2026.9.2**. The 1.x line continues for OpenClaw
|
|
18
|
+
`<= 2026.7.1-2` under the `legacy` dist-tag until **2027-03-31**:
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @clawops/cli # 2.x
|
|
22
|
+
npm install -g @clawops/cli@legacy # 1.x maintenance
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Pin the tag in CI — `latest` moves to 2.x, so an unpinned pipeline will change lines.
|
|
26
|
+
[`CHANGELOG.md`](CHANGELOG.md) carries the full history; this section covers what changed
|
|
27
|
+
about *how clawops behaves*.
|
|
28
|
+
|
|
29
|
+
> **Release in progress.** 2.0 has not shipped yet. This section is written as the work
|
|
30
|
+
> lands so the flows stay documented while they are being changed, not reconstructed after.
|
|
31
|
+
|
|
32
|
+
### Your deployment keeps its state
|
|
33
|
+
|
|
34
|
+
OpenClaw 2.0 stores sessions, transcripts and credentials in SQLite. clawops mounted no
|
|
35
|
+
state at all, so **every restart destroyed them** — and a restart is what `gateway restart`,
|
|
36
|
+
`gateway update` and `config set` all do.
|
|
37
|
+
|
|
38
|
+
One host directory (`/var/lib/clawops/openclaw`) is now bind-mounted at OpenClaw's own
|
|
39
|
+
default location, holding the config, the database and any provider plugins. Existing
|
|
40
|
+
deployments migrate on the next `up`/`apply`.
|
|
41
|
+
|
|
42
|
+
### `clawops up` / `clawops apply`
|
|
43
|
+
|
|
44
|
+
```mermaid
|
|
45
|
+
flowchart TD
|
|
46
|
+
A["clawops plan"] --> B{"config valid<br/>against OpenClaw schema?"}
|
|
47
|
+
B -- no --> B1["refuse — plan is still<br/>a file you can edit"]
|
|
48
|
+
B -- yes --> C["clawops apply"]
|
|
49
|
+
C --> D{"OpenClaw version<br/>in supported range?"}
|
|
50
|
+
D -- no --> D1["refuse — names<br/>@clawops/cli@legacy"]
|
|
51
|
+
D -- yes --> E["provision host"]
|
|
52
|
+
E --> F["state dir, owned 1000:1000<br/>migrate any pre-2.0 config"]
|
|
53
|
+
F --> G["write config<br/>validated before writing"]
|
|
54
|
+
G --> H["install provider plugins<br/>while egress exists"]
|
|
55
|
+
H --> I["start gateway"]
|
|
56
|
+
I --> J{"/startupz says started?"}
|
|
57
|
+
J -- no --> J1["fail with the reason"]
|
|
58
|
+
J -- yes --> K{"configured providers<br/>all loaded?"}
|
|
59
|
+
K -- no --> K1["warn — healthy gateway,<br/>missing model backend"]
|
|
60
|
+
K -- yes --> L["done"]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Three of those steps are new, and each exists because the old flow could report success
|
|
64
|
+
while something was wrong: the config was never validated before being written, provider
|
|
65
|
+
plugins were left to be fetched at boot (or silently missing on a deny-all host), and
|
|
66
|
+
"started" was inferred from `docker run` exiting 0.
|
|
67
|
+
|
|
68
|
+
### `clawops gateway update`
|
|
69
|
+
|
|
70
|
+
Previously: pull, run, report success. `docker run` exiting 0 means the container was
|
|
71
|
+
*created* — and the container it replaced is already gone.
|
|
72
|
+
|
|
73
|
+
```mermaid
|
|
74
|
+
flowchart TD
|
|
75
|
+
A["clawops gateway update X"] --> B{"X in supported range?"}
|
|
76
|
+
B -- no --> B1["refuse before pulling"]
|
|
77
|
+
B -- yes --> C["docker pull X"]
|
|
78
|
+
C --> D["snapshot state database"]
|
|
79
|
+
D -- cannot snapshot --> D1["refuse — no rollback point"]
|
|
80
|
+
D --> E{"target release understands<br/>this schema?"}
|
|
81
|
+
E -- no --> E1["refuse — downgrade across<br/>a schema boundary"]
|
|
82
|
+
E -- yes --> F["swap container"]
|
|
83
|
+
F --> G{"/startupz says started?"}
|
|
84
|
+
G -- yes --> H["done"]
|
|
85
|
+
G -- no --> I["one-shot doctor --fix<br/>in a throwaway container"]
|
|
86
|
+
I --> J["re-run, re-gate"]
|
|
87
|
+
J -- started --> K["done — reported as repaired"]
|
|
88
|
+
J -- still not --> L["roll back to previous image"]
|
|
89
|
+
L -- started --> M["rolled back, reason reported"]
|
|
90
|
+
L -- still not --> N["failed — snapshot path named"]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The snapshot is not only a rollback point: `database preflight` refuses a live database
|
|
94
|
+
because the schema version sits in the WAL until checkpointed, so the consolidated snapshot
|
|
95
|
+
is what makes the compatibility check possible at all.
|
|
96
|
+
|
|
97
|
+
### `clawops gateway restart`
|
|
98
|
+
|
|
99
|
+
A restart changes neither the deployed version nor who can reach the gateway. Both are read
|
|
100
|
+
back from the running container rather than guessed:
|
|
101
|
+
|
|
102
|
+
```mermaid
|
|
103
|
+
flowchart LR
|
|
104
|
+
A["gateway restart"] --> B["read current image"]
|
|
105
|
+
B -- no container --> B1["refuse — nothing to reuse.<br/>latest and stable point at 2.0"]
|
|
106
|
+
B --> C["read current publish scope"]
|
|
107
|
+
C --> D["recreate with the same<br/>version and reachability"]
|
|
108
|
+
D --> E{"/startupz says started?"}
|
|
109
|
+
E -- no --> E1["fail with the reason"]
|
|
110
|
+
E -- yes --> F["done"]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Migrating an existing 1.x deployment
|
|
114
|
+
|
|
115
|
+
```mermaid
|
|
116
|
+
flowchart TD
|
|
117
|
+
A["clawops migrate"] --> B{"1.x container running?"}
|
|
118
|
+
B -- no --> B1["nothing to rescue — state was<br/>already lost to an earlier restart"]
|
|
119
|
+
B -- yes --> C["verified backup, inside the running container"]
|
|
120
|
+
C -- "backup fails" --> C1["refused — nothing touched"]
|
|
121
|
+
C --> D["extract state from the RUNNING container"]
|
|
122
|
+
D --> E["chown 1000:1000"]
|
|
123
|
+
E --> F["stop and remove 1.x"]
|
|
124
|
+
F --> G["synthesise a valid 2.0 config"]
|
|
125
|
+
G --> H["start 2.0 with the state directory"]
|
|
126
|
+
H --> I{"/startupz started?"}
|
|
127
|
+
I -- "no — schema still migrating" --> J["restart once"]
|
|
128
|
+
J --> K{"started?"}
|
|
129
|
+
K -- no --> K1["failed — points at the backup"]
|
|
130
|
+
K --> L["report"]
|
|
131
|
+
I -- yes --> L
|
|
132
|
+
L --> M["what carried over,<br/>device identity, config to review"]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Two things about that shape are not obvious, and both came from running a real migration:
|
|
136
|
+
|
|
137
|
+
**State is extracted from the *running* container.** All 1.x state lived inside it — clawops
|
|
138
|
+
mounted none — so stopping first destroys what the migration came to save.
|
|
139
|
+
|
|
140
|
+
**The config is synthesised, not carried forward.** 1.x never had one that applied; the file
|
|
141
|
+
clawops mounted was read by nothing. Your old settings are reported as *intent to review*,
|
|
142
|
+
never applied blindly — their channel blocks would not validate against 2.0 anyway.
|
|
143
|
+
|
|
144
|
+
The gateway also needs two starts: the first performs the state-schema migration and reports
|
|
145
|
+
it as pending. `migrate` waits for the second rather than declaring success early.
|
|
146
|
+
|
|
147
|
+
If you ran `gateway restart`, `gateway update` or `config set` on a clawops before 2.0, your
|
|
148
|
+
state is already gone — nothing was mounted to survive the container replacement. `migrate`
|
|
149
|
+
says so plainly rather than pretending to rescue it.
|
|
150
|
+
|
|
151
|
+
### `clawops backup restore` works again, and never in place
|
|
152
|
+
|
|
153
|
+
v1.7.5 made restore fail with an explanation, because the OpenClaw it supported had no
|
|
154
|
+
restore subcommand to call. 2.0 does, and clawops delegates to it:
|
|
155
|
+
|
|
156
|
+
```mermaid
|
|
157
|
+
flowchart TD
|
|
158
|
+
A["clawops backup restore --file X"] --> B["upload archive to the host"]
|
|
159
|
+
B --> C["openclaw backup restore --target <staging>"]
|
|
160
|
+
C -- "target not empty" --> C1["refused by OpenClaw"]
|
|
161
|
+
C --> D["archive verified, expanded<br/>into a fresh directory"]
|
|
162
|
+
D --> E["warnings printed verbatim<br/>time travel, channel relink,<br/>approvals, plugins"]
|
|
163
|
+
E --> F["nothing activated"]
|
|
164
|
+
F --> G["you stop the gateway, swap the<br/>state dir, restart, re-apply"]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
clawops does not extract archives itself and does not restore in place. The final step is
|
|
168
|
+
manual on purpose, and re-applying matters: the archive does not carry plugin
|
|
169
|
+
`node_modules`, so a restored deployment starts without its model providers — looking
|
|
170
|
+
healthy while doing it.
|
|
171
|
+
|
|
172
|
+
**The archive is a credential.** It carries the state database — `mcp_oauth_stores`,
|
|
173
|
+
`secret_store_entries`, `worker_environment_credentials`, `device_auth_tokens` — unencrypted.
|
|
174
|
+
clawops now writes it `0600` locally; it previously used the default `0644`.
|
|
175
|
+
|
|
176
|
+
### Model providers that need a plugin are installed for you
|
|
177
|
+
|
|
178
|
+
OpenClaw 2.0 made model providers **install-gated plugins**. Twenty-four ship in the image —
|
|
179
|
+
`anthropic`, `openai`, `google`, `ollama`, `openrouter` among them — but not all of them.
|
|
180
|
+
Configuring one that is not bundled, without installing it, produces a gateway that starts,
|
|
181
|
+
reports healthy, and has no model backend.
|
|
182
|
+
|
|
183
|
+
clawops installs what your config needs, pinned to an exact version, **during `apply`**:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
Resolving clawhub:@openclaw/deepseek-provider@2026.9.2…
|
|
187
|
+
Downloading plugin @openclaw/deepseek-provider@2026.9.2 from ClawHub…
|
|
188
|
+
Installed plugin: deepseek
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**This adds an outbound dependency the 1.x line did not have: `clawhub.ai`.** It is needed
|
|
192
|
+
while `apply` is running, not at boot — deliberately, so a failure reaches the person running
|
|
193
|
+
the command rather than a locked-down host at 3am. Blocked, it looks like this:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
fetch failed | getaddrinfo EAI_AGAIN clawhub.ai | EAI_AGAIN
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
clawops checks the installed provider IDs afterwards and will not call the deploy finished
|
|
200
|
+
while a configured provider is missing. [Required outbound access](docs/security/egress.md)
|
|
201
|
+
lists every destination and when it is needed.
|
|
202
|
+
|
|
203
|
+
### Chat channels are installed for you too
|
|
204
|
+
|
|
205
|
+
Every channel in OpenClaw 2.0 is an install-gated plugin. `clawops apply` installs the ones
|
|
206
|
+
your config names, during the deploy while egress exists, and then asks the gateway whether
|
|
207
|
+
they are really installed:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
[clawops] warning: the gateway is running, but these configured channels are not installed:
|
|
211
|
+
discord. They will never connect.
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
It has to ask. `openclaw channels add` — the obvious command — returns success even when the
|
|
215
|
+
plugin install fails, so clawops uses `openclaw plugins install` and verifies against
|
|
216
|
+
`channels list --all --json`.
|
|
217
|
+
|
|
218
|
+
Channel plugins are pinned to the supported runtime. The current `latest` does not install on
|
|
219
|
+
it: `plugin "discord" requires plugin API >=2026.9.3, but this OpenClaw runtime exposes
|
|
220
|
+
2026.9.2` — the same drift that forced version pins on model providers.
|
|
221
|
+
|
|
222
|
+
Telegram needs nothing installed: it ships in the image.
|
|
223
|
+
|
|
224
|
+
### Bad config is caught before it is written
|
|
225
|
+
|
|
226
|
+
Config is validated against **OpenClaw's own schema** — captured from the image, not
|
|
227
|
+
hand-written — before anything is sent to the host, and again before a write replaces a
|
|
228
|
+
working file. `clawops plan` refuses a plan whose config the gateway would reject, while the
|
|
229
|
+
plan is still a file you can edit.
|
|
230
|
+
|
|
231
|
+
A rejected config is kept at `<path>.rejected.<timestamp>` and the live one is left alone, so
|
|
232
|
+
a validation failure never costs you what you were trying to write.
|
|
233
|
+
|
|
234
|
+
One rule is clawops's own: `gateway.mode` is optional in the schema and **mandatory in
|
|
235
|
+
practice** — a config without it passes `openclaw config validate` and then exits 78.
|
|
236
|
+
|
|
237
|
+
### Containers are hardened
|
|
238
|
+
|
|
239
|
+
The gateway runs with `--cap-drop=ALL`, `--security-opt no-new-privileges`, `--init` and
|
|
240
|
+
`--pids-limit 512`. State is owned numerically by `1000:1000`, matching the container's user
|
|
241
|
+
rather than a host account that may not have that uid.
|
|
242
|
+
|
|
243
|
+
### The version pin is enforced everywhere it can change
|
|
244
|
+
|
|
245
|
+
`doctor`, `plan`, `up` and `apply` refuse an OpenClaw release outside the supported range, and
|
|
246
|
+
`gateway restart` reuses the version already deployed rather than resolving a moving tag. A
|
|
247
|
+
restart changes neither the version nor who can reach it.
|
|
248
|
+
|
|
249
|
+
### The gateway is no longer exposed to your network
|
|
250
|
+
|
|
251
|
+
The container publishes on `127.0.0.1:18789` instead of `0.0.0.0:18789`. Reach it with
|
|
252
|
+
`clawops tunnel` or a reverse proxy on the host.
|
|
253
|
+
|
|
254
|
+
Previously the wizard set `allowedGatewayCidrs` from the CIDR you gave for **SSH**, so a
|
|
255
|
+
plaintext HTTP dashboard — token in the URL — was opened to your whole shell-access network
|
|
256
|
+
as a side effect of one unrelated answer. To bind all interfaces deliberately, set
|
|
257
|
+
`network.publishGateway: "all"`.
|
|
258
|
+
|
|
259
|
+
**You must act if** a client or reverse proxy on another machine reaches the gateway
|
|
260
|
+
directly, or external monitoring hits `/health`. A proxy on the host is unaffected; one in a
|
|
261
|
+
*container* on the host needs `--network host`.
|
|
262
|
+
|
|
263
|
+
### Health checks can actually fail
|
|
264
|
+
|
|
265
|
+
The gateway serves its Control UI on a catch-all route, so **any unmatched path answers 200
|
|
266
|
+
with HTML**:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
/healthz 200 application/json {"ok":true,"status":"live"}
|
|
270
|
+
/health-typo 200 text/html <!doctype html>…
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
clawops probed with `curl -fsS … >/dev/null`, which succeeds on a typo. It proved something
|
|
274
|
+
was listening on the port, not that the gateway was healthy. Probes now read the response
|
|
275
|
+
body, and the restart gate uses `/startupz` rather than liveness — after a restart the
|
|
276
|
+
process listens long before startup finishes.
|
|
277
|
+
|
|
278
|
+
### `clawops mcp wire` actually wires something now
|
|
279
|
+
|
|
280
|
+
It has never worked — not on 2.0, not on any 1.x release. It wrote `gateway.mcpClients`,
|
|
281
|
+
which is **not a key OpenClaw has**: checked against the config schemas of `2026.4.5`,
|
|
282
|
+
`2026.7.1-2` and `2026.9.2`. The real key is top-level `mcp.servers`. And the entry it wrote
|
|
283
|
+
was `command: "clawops"` over stdio, which spawns *inside the gateway container* — where
|
|
284
|
+
clawops is not installed and nothing installs it.
|
|
285
|
+
|
|
286
|
+
On 1.x nothing validated the write, so clawops stored a key nothing read, restarted your
|
|
287
|
+
gateway, and reported: *"The gateway's AI can now run clawops commands."* It could not.
|
|
288
|
+
|
|
289
|
+
```mermaid
|
|
290
|
+
flowchart TD
|
|
291
|
+
A["clawops mcp wire"] --> B["openclaw mcp add --transport streamable-http"]
|
|
292
|
+
B --> C{"gateway connects<br/>to the URL?"}
|
|
293
|
+
C -- no --> C1["probe fails, nothing saved,<br/>clawops prints the reason"]
|
|
294
|
+
C -- yes --> D["saved to mcp.servers.clawops"]
|
|
295
|
+
D --> E["openclaw mcp reload"]
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
It delegates to `openclaw mcp add` now, which **probes the server before saving** — so
|
|
299
|
+
"wired" means the gateway connected, not that a file was written.
|
|
300
|
+
|
|
301
|
+
**You have to run the server yourself.** clawops is not installed on the gateway host:
|
|
26
302
|
|
|
27
303
|
```bash
|
|
28
304
|
clawops mcp serve --http 18790 --bind 0.0.0.0 --token "$(openssl rand -hex 16)"
|
|
29
305
|
clawops mcp wire --stack prod --token <same token>
|
|
30
306
|
```
|
|
31
307
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
it with `--bind 0.0.0.0`, pass `--token` (or set `CLAWOPS_MCP_TOKEN`) before upgrading.
|
|
308
|
+
Installing clawops on the gateway host is a deliberate follow-up, not part of 2.0: it puts
|
|
309
|
+
deployment credentials on the deployed box, and the gateway's AI is reachable from every
|
|
310
|
+
channel it is connected to. See `docs/security/threat-model.md` T11.
|
|
36
311
|
|
|
37
|
-
|
|
38
|
-
client to connect claimed it; every later one got `"Server already initialized"`.
|
|
312
|
+
### `clawops mcp serve --http` serves more than one client, and asks who you are
|
|
39
313
|
|
|
40
|
-
|
|
314
|
+
Two bugs, found by testing against a real gateway rather than a mock.
|
|
41
315
|
|
|
42
|
-
|
|
316
|
+
It built **one transport for the whole process**, so the first client to connect claimed it
|
|
317
|
+
and every later one — a second editor, a reconnect, the gateway's own probe — was answered
|
|
318
|
+
`"Server already initialized"`. HTTP mode is the multi-client mode.
|
|
43
319
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
320
|
+
It had **no authentication**, while exposing every tool including `clawops_destroy`. It now
|
|
321
|
+
takes a bearer token, compares it in constant time, and refuses to bind anywhere but loopback
|
|
322
|
+
without one.
|
|
47
323
|
|
|
48
|
-
|
|
324
|
+
### The firewall follows the deployment
|
|
325
|
+
|
|
326
|
+
```mermaid
|
|
327
|
+
flowchart TD
|
|
328
|
+
A["clawops plan"] --> B{"publishGateway?"}
|
|
329
|
+
B -- "loopback (default)" --> C{"allowedGatewayCidrs empty?"}
|
|
330
|
+
C -- no --> C1["refuse — those rules would admit<br/>traffic to a closed port"]
|
|
331
|
+
C -- yes --> D["SSH rules only"]
|
|
332
|
+
B -- all --> E["SSH rules + gateway rules<br/>on spec.network.gatewayPort"]
|
|
333
|
+
D --> F["clawops harden"]
|
|
334
|
+
E --> F
|
|
335
|
+
F --> G["read the container's port bindings"]
|
|
336
|
+
G --> H{"published to the network?"}
|
|
337
|
+
H -- no --> H1["ufw: SSH only"]
|
|
338
|
+
H -- yes --> H2["ufw: SSH + the published port"]
|
|
339
|
+
```
|
|
49
340
|
|
|
50
|
-
|
|
341
|
+
Three security controls were doing the opposite of what they say.
|
|
51
342
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
343
|
+
**`clawops harden` opened the gateway port on every deployment.** The `ufw` module ran
|
|
344
|
+
`ufw allow 18789/tcp` unconditionally. Since the gateway publishes on `127.0.0.1`, that
|
|
345
|
+
opened a port nothing was listening on — a hardening step widening the firewall past what the
|
|
346
|
+
deployment exposes. It now reads the running container's port bindings and adds the rule only
|
|
347
|
+
when the gateway is really published, on whatever port it is published on.
|
|
56
348
|
|
|
57
|
-
The
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
349
|
+
**The AWS security-group audit exempted the two ports it exists to check.** Ports 22 and
|
|
350
|
+
18789 were on an "expected" list, so a group opening SSH *or the gateway* to `0.0.0.0/0` came
|
|
351
|
+
back as "No unexpected open ingress rules found". It also never read IPv6 rules, so `::/0`
|
|
352
|
+
was invisible.
|
|
61
353
|
|
|
62
|
-
|
|
63
|
-
|
|
354
|
+
**The setup wizard defaulted SSH access to `0.0.0.0/0`.** Pressing Enter opened SSH to the
|
|
355
|
+
whole internet, on the path most first-time users take. It offers your own IP as a `/32` now,
|
|
356
|
+
and when that cannot be detected it offers no default and requires an answer.
|
|
64
357
|
|
|
65
|
-
|
|
66
|
-
on either OpenClaw line — models, channels and auth mode were silently discarded. Setting
|
|
67
|
-
`OPENCLAW_CONFIG_PATH` fixes it, with four guards: `gateway.port` normalisation (with a warning),
|
|
68
|
-
an argv `--port` pin, a parse check, and a post-restart health gate. The MCP `gateway restart`
|
|
69
|
-
tool, which dropped the config mount entirely, now matches the CLI path.
|
|
358
|
+
### The gateway port comes from the plan
|
|
70
359
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
360
|
+
```jsonc
|
|
361
|
+
"network": {
|
|
362
|
+
"allowedSshCidrs": ["203.0.113.4/32"],
|
|
363
|
+
"allowedGatewayCidrs": [],
|
|
364
|
+
"publishGateway": "loopback",
|
|
365
|
+
"gatewayPort": 9443
|
|
366
|
+
}
|
|
367
|
+
```
|
|
75
368
|
|
|
76
|
-
|
|
77
|
-
|
|
369
|
+
One value now reaches the security-group rules, the container publish flag, the default
|
|
370
|
+
`gateway.port` and the gateway URL. It was a constant redeclared in eleven places, so
|
|
371
|
+
changing it meant finding all of them — and missing one produced a container publishing one
|
|
372
|
+
port, a gateway listening on another, and a firewall opening a third.
|
|
78
373
|
|
|
79
|
-
|
|
80
|
-
`bootstrap.sh.tmpl` resolved from the bundle — so `clawops plan` and `clawops up --provider local`
|
|
81
|
-
failed from an npm install. Both are now shipped and resolved correctly.
|
|
374
|
+
Local deployments use `clawops up --gateway-port 9443`.
|
|
82
375
|
|
|
83
|
-
|
|
84
|
-
standard entry failed permanently with `Host denied`. Standard entries now parse, including
|
|
85
|
-
comma-separated host lists, `[host]:port`, hashed hostnames, `@revoked` / `@cert-authority`
|
|
86
|
-
markers, and wildcard and negated patterns.
|
|
376
|
+
### `clawops doctor` answers whether it works, and says so in its exit code
|
|
87
377
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
378
|
+
```mermaid
|
|
379
|
+
flowchart TD
|
|
380
|
+
A["clawops doctor"] --> B["local: Node, Pulumi home,<br/>config, SSH key, credentials"]
|
|
381
|
+
B --> C{"--stack given?"}
|
|
382
|
+
C -- no --> Z["report"]
|
|
383
|
+
C -- yes --> D["container state"]
|
|
384
|
+
D --> E["deployed OpenClaw version"]
|
|
385
|
+
E --> F["probe /startupz<br/>and read the body"]
|
|
386
|
+
F --> G["published scope, disk,<br/>log rotation, hardening drift"]
|
|
387
|
+
G --> Z
|
|
388
|
+
Z --> Y{"any check failed?"}
|
|
389
|
+
Y -- no --> Y1["exit 0"]
|
|
390
|
+
Y -- yes --> Y2["exit 1"]
|
|
391
|
+
```
|
|
91
392
|
|
|
92
|
-
|
|
393
|
+
Three changes worth knowing:
|
|
394
|
+
|
|
395
|
+
**It asks the gateway.** `doctor` used to read `docker inspect`'s healthcheck field, which
|
|
396
|
+
the OpenClaw image does not set — so it reported "no healthcheck configured" and moved on. A
|
|
397
|
+
running container means the process started, not that it serves. It now probes `/startupz`
|
|
398
|
+
and reads the body.
|
|
399
|
+
|
|
400
|
+
**It exits 1 when something failed.** Only an old Node.js used to do that; an unreadable SSH
|
|
401
|
+
key or an unsupported gateway exited 0, so a CI step running `clawops doctor` read a broken
|
|
402
|
+
deployment as success. Warnings still exit 0 — a fresh machine with no stacks is
|
|
403
|
+
unconfigured, not broken.
|
|
93
404
|
|
|
405
|
+
**It is an MCP tool.** `clawops_doctor` returns the same report as structured data, so an
|
|
406
|
+
agent that hits a failure can find out why. It reports only; it never runs `openclaw doctor
|
|
407
|
+
--fix`. `--json` gives the CLI the same report.
|
|
408
|
+
|
|
409
|
+
### `clawops agents list` stops inventing an empty list
|
|
410
|
+
|
|
411
|
+
The command ended in `|| echo '[]'`, so a stopped container, a gateway still starting, or a
|
|
412
|
+
Docker permission error all produced **"No agents running."** — a wrong answer rather than an
|
|
413
|
+
error. It now fails, and says which.
|
|
414
|
+
|
|
415
|
+
### Day-two commands work on AWS
|
|
416
|
+
|
|
417
|
+
`gateway restart`, `logs`, `monitor`, `backup`, `agents`, `config set` and `doctor`'s
|
|
418
|
+
container checks were **all broken on AWS**: clawops connects as `ubuntu`, but provisioning
|
|
419
|
+
only put `clawops` in the docker group, so every Docker command failed with `permission
|
|
420
|
+
denied`. GCP and Azure connect as `clawops`, which is why only AWS was affected.
|
|
421
|
+
|
|
422
|
+
### Removed
|
|
423
|
+
|
|
424
|
+
**`clawops agents restart`** and the `clawops_agents_restart` MCP tool. OpenClaw 2.0 has no
|
|
425
|
+
per-agent restart — only `gateway restart` and `daemon restart`, both of which interrupt
|
|
426
|
+
every agent on the host. Use `clawops gateway restart`, or stay on `@clawops/cli@legacy`.
|
|
427
|
+
|
|
428
|
+
`clawops agents list` and `clawops agents logs` are unaffected.
|
|
429
|
+
|
|
430
|
+
---
|
|
94
431
|
|
|
95
432
|
## Who this is for
|
|
96
433
|
|
|
@@ -290,7 +627,7 @@ clawops down --yes # Destroy local-provider stack
|
|
|
290
627
|
|---|---|
|
|
291
628
|
| `setup` | First-run wizard — guided LLM, integrations, and deploy-plan generation |
|
|
292
629
|
| `init` | Register a stack in `~/.clawops/config.json` without provisioning |
|
|
293
|
-
| `up` | Provision or update stack (`--dry-run` for preview) |
|
|
630
|
+
| `up` | Provision or update stack (`--dry-run` for preview, `--gateway-port` for a non-default port) |
|
|
294
631
|
| `down` | Destroy local-provider stack (requires `--yes`; `--dry-run` shows current outputs) |
|
|
295
632
|
| `destroy` | Destroy cloud-provider stack with confirmation prompt (`--dry-run` shows current outputs) |
|
|
296
633
|
| `status` | Show stack outputs: IP, gateway URL, region, provisioned time |
|
|
@@ -300,16 +637,16 @@ clawops down --yes # Destroy local-provider stack
|
|
|
300
637
|
| `logs` | Stream OpenClaw logs (`-f`, `--tail N`, `--since 5m`) |
|
|
301
638
|
| `tunnel` | Local port-forward to gateway UI over SSH |
|
|
302
639
|
| `config` | Get/set remote OpenClaw config values (`--dry-run` shows would-write JSON) |
|
|
303
|
-
| `agents` | List or
|
|
640
|
+
| `agents` | List OpenClaw agents, or stream one agent's logs |
|
|
304
641
|
| `gateway` | Restart the OpenClaw gateway service |
|
|
305
|
-
| `backup` | Create
|
|
642
|
+
| `backup` | Create and restore OpenClaw state backups (`restore` expands into a staging directory, never in place) |
|
|
306
643
|
| `stacks` | List named stacks and their state |
|
|
307
|
-
| `doctor` | Check
|
|
644
|
+
| `doctor` | Check the local machine; with `--stack`, the deployment's health too. `--json` for the report. Exits 1 on any failure |
|
|
308
645
|
| `secret` | Manage secrets: `list`, `set`, `delete`, `rotate`, `audit` |
|
|
309
646
|
| `monitor` | Live dashboard: gateway health, container stats, log tail, stack picker |
|
|
310
|
-
| `mcp serve` | Start the embedded MCP server (stdio or HTTP) |
|
|
647
|
+
| `mcp serve` | Start the embedded MCP server (stdio, or HTTP with `--http <port> --token <t>`) |
|
|
311
648
|
| `mcp install` | Interactively wire clawops into AI editors |
|
|
312
|
-
| `mcp wire` | Wire the gateway's AI as an MCP client of clawops |
|
|
649
|
+
| `mcp wire` | Wire the gateway's AI as an MCP client of clawops (verifies the connection before saving) |
|
|
313
650
|
| `help` | List all commands and global flags |
|
|
314
651
|
| `harden` | Apply security hardening to a deployed stack (SSH, UFW, fail2ban, unattended-upgrades, Docker socket; AWS: SG audit, SSM check, Flow Logs, GuardDuty) |
|
|
315
652
|
| `bug` | Open a pre-filled GitHub issue with system context from `doctor` |
|
|
@@ -394,21 +731,26 @@ Do not bind to a non-loopback address without additional authentication controls
|
|
|
394
731
|
|
|
395
732
|
| Tool | Toolset | Description |
|
|
396
733
|
|---|---|---|
|
|
397
|
-
| `clawops_status` |
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
400
|
-
| `
|
|
401
|
-
| `clawops_task_status` | read | Poll a long-running task |
|
|
734
|
+
| `clawops_status` | cli | Show stack outputs (what is deployed, not whether it works) |
|
|
735
|
+
| `clawops_doctor` | cli | Run diagnostics: local prerequisites, and with a stack, remote health |
|
|
736
|
+
| `clawops_logs_tail` | cli | Tail OpenClaw logs |
|
|
737
|
+
| `clawops_monitor` | cli | Sample gateway and host metrics |
|
|
402
738
|
| `clawops_stacks_list` | admin | List all stacks and their state |
|
|
739
|
+
| `clawops_config_get` | cli | Read a remote config value |
|
|
740
|
+
| `clawops_agents_list` | cli | List running agents |
|
|
403
741
|
| `clawops_up` | cli | Provision or update a stack |
|
|
404
|
-
| `
|
|
742
|
+
| `clawops_destroy` | cli | Destroy a stack (elicits confirmation) |
|
|
405
743
|
| `clawops_apply` | cli | Apply a plan file |
|
|
406
|
-
| `
|
|
744
|
+
| `clawops_plan` | cli | Generate a deploy plan |
|
|
407
745
|
| `clawops_config_set` | cli | Write a remote config value |
|
|
408
|
-
| `
|
|
746
|
+
| `clawops_config_unset` | cli | Remove a remote config key |
|
|
747
|
+
| `clawops_config_validate` | cli | Validate the deployed config against the OpenClaw schema |
|
|
748
|
+
| `clawops_gateway_restart` | cli | Restart the gateway (elicits confirmation) |
|
|
409
749
|
| `clawops_workflow_deploy_app` | workflow | End-to-end deploy: plan → confirm → apply → status |
|
|
750
|
+
| `clawops_workflow_recover` | workflow | Diagnostic workflow for an unhealthy stack |
|
|
751
|
+
| `clawops_task_status` | cli | Poll a long-running task |
|
|
410
752
|
|
|
411
|
-
`read` toolset
|
|
753
|
+
Tools in the `read` toolset are also available in `--read-only` mode; the table's Toolset column shows the primary toolset. All other toolsets require full mode.
|
|
412
754
|
Destructive tools require explicit confirmation (elicitation) unless `yes: true` is passed.
|
|
413
755
|
|
|
414
756
|
See [`docs/security/tool-risk-matrix.md`](docs/security/tool-risk-matrix.md) for the full risk
|
|
@@ -584,7 +926,7 @@ pnpm dev doctor # verify toolchain
|
|
|
584
926
|
```bash
|
|
585
927
|
pnpm dev # run CLI from src/ via tsx
|
|
586
928
|
pnpm build # tsup → dist/
|
|
587
|
-
pnpm test # vitest (
|
|
929
|
+
pnpm test # vitest (1210 tests, ~13s)
|
|
588
930
|
pnpm test:changed # vitest --changed (fast edit loop)
|
|
589
931
|
pnpm test:integration # Docker-based SSH integration tests
|
|
590
932
|
pnpm typecheck # tsc --noEmit
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
applyPlan
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-UFRO4H5W.js";
|
|
5
|
+
import "./chunk-N44K3LHE.js";
|
|
6
|
+
import "./chunk-YR3O2I3S.js";
|
|
5
7
|
import "./chunk-6ZFIFDBJ.js";
|
|
6
8
|
import "./chunk-BOPSG2LI.js";
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-EI7CQQ7D.js";
|
|
11
|
-
import "./chunk-PVUVI35Y.js";
|
|
9
|
+
import "./chunk-LU2V3LXA.js";
|
|
10
|
+
import "./chunk-NUDIHZEH.js";
|
|
11
|
+
import "./chunk-JW5A7E3X.js";
|
|
12
12
|
import "./chunk-A2I76FTA.js";
|
|
13
13
|
import "./chunk-CX5SL5HP.js";
|
|
14
14
|
import "./chunk-KGXPLI7W.js";
|
|
15
|
+
import "./chunk-6TIYVHPX.js";
|
|
16
|
+
import "./chunk-EI7CQQ7D.js";
|
|
15
17
|
export {
|
|
16
18
|
applyPlan
|
|
17
19
|
};
|