@clawops/cli 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +862 -807
  2. package/dist/{apply-KYFPWI5U.js → apply-HJ5GAKHW.js} +7 -7
  3. package/dist/{automation-RMAIBNVC.js → automation-MKCJ46I6.js} +3 -3
  4. package/dist/{bootstrap-HNGKY6WW.js → bootstrap-NN647RCY.js} +2 -2
  5. package/dist/{chunk-HSPHXG6W.js → chunk-3Z5TDXCJ.js} +2 -2
  6. package/dist/{chunk-CX5SL5HP.js → chunk-64PONSA5.js} +10 -1
  7. package/dist/{chunk-STLRHLLV.js → chunk-A2DS5SB7.js} +12 -9
  8. package/dist/{chunk-KC3GC3PE.js → chunk-AYUX4FOF.js} +127 -13
  9. package/dist/{chunk-LEVIW2PV.js → chunk-BELTQGHT.js} +16 -2
  10. package/dist/{preflight-MWWM5JT3.js → chunk-BTW7NHOH.js} +3 -2
  11. package/dist/{chunk-A2I76FTA.js → chunk-ELU3XQEA.js} +1 -1
  12. package/dist/{chunk-IVWP4QUJ.js → chunk-GFUK6KDR.js} +10 -10
  13. package/dist/{chunk-W3OLNNJW.js → chunk-PSOSBJVO.js} +1 -1
  14. package/dist/{chunk-5XT5IAJK.js → chunk-QDNFADJI.js} +12 -5
  15. package/dist/{chunk-LBE6COC4.js → chunk-R3DTA35B.js} +22 -3
  16. package/dist/{preflight-EF52S3WW.js → chunk-TC2VYPLH.js} +3 -2
  17. package/dist/chunk-X56CQ6GB.js +157 -0
  18. package/dist/{chunk-BSLIYB5Q.js → chunk-XFN3YDZE.js} +1 -1
  19. package/dist/{cli-WLD3EBEM.js → cli-NH7VUNOU.js} +1 -1
  20. package/dist/cli.js +156 -82
  21. package/dist/{context-7EVC6W45.js → context-7VI3R6EK.js} +4 -4
  22. package/dist/flows-RI7FGHAM.js +120 -0
  23. package/dist/{generate-I6YQ5V2T.js → generate-YEM73KVB.js} +6 -6
  24. package/dist/{harden-72JMRT4E.js → harden-3KKCR7BL.js} +541 -3
  25. package/dist/{mcp-wire-3SWBM7D6.js → mcp-wire-TE2ZL6HD.js} +10 -10
  26. package/dist/{package-5BCMEBTE.js → package-JWDMWPV3.js} +47 -10
  27. package/dist/preflight-7W23GP4R.js +19 -0
  28. package/dist/preflight-IE2YWBAC.js +13 -0
  29. package/dist/private-only-DIA26I2E.js +74 -0
  30. package/dist/{server-GBA7BJGK.js → server-ZJPXPRZT.js} +10 -10
  31. package/dist/{stack-config-R4SPSUQZ.js → stack-config-LUTS2XAD.js} +2 -2
  32. package/dist/{state-PRBIIN7I.js → state-AOAYLYY5.js} +2 -2
  33. package/dist/{store-SDUR52Z5.js → store-OVB3DAJR.js} +1 -1
  34. package/dist/tailscale-cutover-6YP2QKFM.js +113 -0
  35. package/dist/{validate-YVI3BJ55.js → validate-JXKN5WD6.js} +1 -1
  36. package/package.json +39 -8
  37. package/spec/deploy-plan.schema.json +16 -1
  38. package/spec/mcp-tools.yaml +67 -9
@@ -2,8 +2,39 @@
2
2
 
3
3
  // package.json
4
4
  var name = "@clawops/cli";
5
- var version = "2.0.1";
6
- var description = "Deploy and manage self-hosted OpenClaw instances across clouds";
5
+ var version = "2.1.0";
6
+ var description = "Deploy and operate self-hosted OpenClaw across AWS, GCP, Azure and local VMs \u2014 a CLI and an MCP server in one.";
7
+ var keywords = [
8
+ "openclaw",
9
+ "mcp",
10
+ "mcp-server",
11
+ "model-context-protocol",
12
+ "pulumi",
13
+ "pulumi-automation-api",
14
+ "infrastructure-as-code",
15
+ "iac",
16
+ "devops",
17
+ "deployment",
18
+ "self-hosted",
19
+ "cli",
20
+ "aws",
21
+ "gcp",
22
+ "azure",
23
+ "ai-agent",
24
+ "agent-ops",
25
+ "claude-code",
26
+ "typescript",
27
+ "nodejs"
28
+ ];
29
+ var homepage = "https://clawops.fyi";
30
+ var bugs = {
31
+ url: "https://github.com/dfridkin/clawops/issues"
32
+ };
33
+ var repository = {
34
+ type: "git",
35
+ url: "git+https://github.com/dfridkin/clawops.git"
36
+ };
37
+ var license = "MPL-2.0";
7
38
  var type = "module";
8
39
  var bin = {
9
40
  clawops: "./dist/cli.js"
@@ -15,11 +46,6 @@ var files = [
15
46
  "dist",
16
47
  "spec"
17
48
  ];
18
- var repository = {
19
- type: "git",
20
- url: "git+https://github.com/dfridkin/clawops.git"
21
- };
22
- var homepage = "https://clawops.fyi";
23
49
  var mcpName = "io.github.dfridkin/clawops";
24
50
  var publishConfig = {
25
51
  access: "public",
@@ -43,7 +69,12 @@ var scripts = {
43
69
  changeset: "changeset",
44
70
  release: "pnpm build && changeset publish",
45
71
  "test:mutation": "node scripts/dev/mutation-check.mjs",
46
- graph: "node scripts/dev/graph.mjs"
72
+ graph: "node scripts/dev/graph.mjs",
73
+ "verify:pack": "node scripts/dev/verify-pack.mjs",
74
+ "verify:mcp": "node scripts/dev/mcp-probe.mjs",
75
+ "verify:docker": "node scripts/dev/verify-docker.mjs",
76
+ "version:packages": "changeset version && node scripts/sync-server-json.mjs",
77
+ "sync:server-json": "node scripts/sync-server-json.mjs"
47
78
  };
48
79
  var dependencies = {
49
80
  "@aws-sdk/client-bedrock": "^3.1063.0",
@@ -106,12 +137,15 @@ var package_default = {
106
137
  name,
107
138
  version,
108
139
  description,
140
+ keywords,
141
+ homepage,
142
+ bugs,
143
+ repository,
144
+ license,
109
145
  type,
110
146
  bin,
111
147
  exports,
112
148
  files,
113
- repository,
114
- homepage,
115
149
  mcpName,
116
150
  publishConfig,
117
151
  engines,
@@ -123,6 +157,7 @@ var package_default = {
123
157
  };
124
158
  export {
125
159
  bin,
160
+ bugs,
126
161
  package_default as default,
127
162
  dependencies,
128
163
  description,
@@ -131,6 +166,8 @@ export {
131
166
  exports,
132
167
  files,
133
168
  homepage,
169
+ keywords,
170
+ license,
134
171
  mcpName,
135
172
  name,
136
173
  packageManager,
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ REQUIRED_PROVIDERS,
4
+ availableSizes,
5
+ azurePreflight,
6
+ managementToken,
7
+ sizeCheck,
8
+ stateBackendCheck
9
+ } from "./chunk-BTW7NHOH.js";
10
+ import "./chunk-FEK7VJAY.js";
11
+ import "./chunk-RHOWYIWT.js";
12
+ export {
13
+ REQUIRED_PROVIDERS,
14
+ availableSizes,
15
+ azurePreflight,
16
+ managementToken,
17
+ sizeCheck,
18
+ stateBackendCheck
19
+ };
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ gcloudConfiguredProject,
4
+ gcpPreflight,
5
+ projectFromIni,
6
+ resolveProjectId
7
+ } from "./chunk-TC2VYPLH.js";
8
+ export {
9
+ gcloudConfiguredProject,
10
+ gcpPreflight,
11
+ projectFromIni,
12
+ resolveProjectId
13
+ };
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ UsageError
4
+ } from "./chunk-KGXPLI7W.js";
5
+
6
+ // src/plan/private-only.ts
7
+ function privateOnlyNetwork(flags, resolved, override, stackName) {
8
+ if (flags.sshCidr !== void 0 || flags.gatewayCidr !== void 0) {
9
+ throw new UsageError(
10
+ "--private-only closes public SSH and gateway access, so it cannot be combined with --ssh-cidr or --gateway-cidr. Drop those flags, or drop --private-only to keep a public rule."
11
+ );
12
+ }
13
+ const tailnet = requireOverride(override, stackName);
14
+ return {
15
+ ...resolved,
16
+ allowedSshCidrs: [],
17
+ allowedGatewayCidrs: [],
18
+ tailscale: { enabled: true, privateOnly: true, ip: tailnet.ip }
19
+ };
20
+ }
21
+ async function assertTailnetReachable(ctx, probe) {
22
+ const tailnet = requireOverride(ctx.config.stacks[ctx.stackName]?.tailscale, ctx.stackName);
23
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
24
+ let conn;
25
+ try {
26
+ const outputMap = await (await ctx.getStack()).outputs();
27
+ const raw = Object.fromEntries(Object.entries(outputMap).map(([k, v]) => [k, v.value]));
28
+ conn = ctx.adapter.getConnectionInfo({
29
+ ...extractBaseOutputs(raw),
30
+ privateKeyPath: ctx.config.ssh.keyPath,
31
+ knownHostsPath: ctx.config.ssh.knownHostsPath
32
+ });
33
+ } catch {
34
+ throw new UsageError(
35
+ `Stack "${ctx.stackName}" has no deployment to make private. Deploy it, then run \`clawops harden --tailscale\`, then plan with --private-only.`
36
+ );
37
+ }
38
+ if (!await probe(conn)) {
39
+ throw new UsageError(
40
+ `This machine cannot reach "${ctx.stackName}" at its tailnet address ${tailnet.ip}, so closing the public ports would leave no way in. Check that Tailscale is running here and signed in to the same tailnet, then try again. Nothing was changed.`
41
+ );
42
+ }
43
+ return tailnet.ip;
44
+ }
45
+ async function guardPrivateOnlyApply(plan, ctx, probe) {
46
+ const { network } = plan.spec;
47
+ if (network.tailscale?.privateOnly !== true) return;
48
+ if (network.allowedSshCidrs.length > 0 || network.allowedGatewayCidrs.length > 0) {
49
+ throw new UsageError(
50
+ `This plan is marked private-only but still opens public ports (SSH: ${network.allowedSshCidrs.join(", ") || "none"}; gateway: ${network.allowedGatewayCidrs.join(", ") || "none"}). Regenerate it with \`clawops plan --private-only\`.`
51
+ );
52
+ }
53
+ const override = ctx.config.stacks[ctx.stackName]?.tailscale;
54
+ const current = requireOverride(override, ctx.stackName);
55
+ if (network.tailscale.ip !== current.ip) {
56
+ throw new UsageError(
57
+ `This plan was made for the tailnet address ${network.tailscale.ip ?? "(none)"}, but "${ctx.stackName}" is now reached at ${current.ip}. Regenerate it with \`clawops plan --private-only\`.`
58
+ );
59
+ }
60
+ await assertTailnetReachable(ctx, probe);
61
+ }
62
+ function requireOverride(override, stackName) {
63
+ if (!override) {
64
+ throw new UsageError(
65
+ `Stack "${stackName}" has no verified tailnet address, so closing its public ports would leave no way in. Run \`clawops harden --tailscale\` first; it only records an address this machine has reached.`
66
+ );
67
+ }
68
+ return override;
69
+ }
70
+ export {
71
+ assertTailnetReachable,
72
+ guardPrivateOnlyApply,
73
+ privateOnlyNetwork
74
+ };
@@ -5,27 +5,27 @@ import {
5
5
  bearerFrom,
6
6
  isLoopbackBind,
7
7
  serveMcp
8
- } from "./chunk-KC3GC3PE.js";
9
- import "./chunk-LBE6COC4.js";
8
+ } from "./chunk-AYUX4FOF.js";
9
+ import "./chunk-R3DTA35B.js";
10
10
  import "./chunk-BOPSG2LI.js";
11
11
  import "./chunk-6ZFIFDBJ.js";
12
12
  import "./chunk-3QJBNAHW.js";
13
13
  import "./chunk-2I7KCMXZ.js";
14
- import "./chunk-HSPHXG6W.js";
14
+ import "./chunk-3Z5TDXCJ.js";
15
15
  import "./chunk-AEEMRFZV.js";
16
- import "./chunk-STLRHLLV.js";
17
- import "./chunk-BSLIYB5Q.js";
16
+ import "./chunk-A2DS5SB7.js";
17
+ import "./chunk-XFN3YDZE.js";
18
18
  import "./chunk-2Z7J3ERQ.js";
19
19
  import "./chunk-E4NJSJ54.js";
20
20
  import "./chunk-MC5ATCXS.js";
21
- import "./chunk-5XT5IAJK.js";
22
- import "./chunk-IVWP4QUJ.js";
21
+ import "./chunk-QDNFADJI.js";
22
+ import "./chunk-GFUK6KDR.js";
23
23
  import "./chunk-NUL5F6YV.js";
24
24
  import "./chunk-3OLUJPCB.js";
25
25
  import "./chunk-NUDIHZEH.js";
26
26
  import "./chunk-JW5A7E3X.js";
27
27
  import "./chunk-LT4XMO5D.js";
28
- import "./chunk-LEVIW2PV.js";
28
+ import "./chunk-BELTQGHT.js";
29
29
  import "./chunk-GIEVAGEK.js";
30
30
  import "./chunk-VOEJYRIK.js";
31
31
  import "./chunk-EI7CQQ7D.js";
@@ -33,8 +33,8 @@ import "./chunk-FEK7VJAY.js";
33
33
  import "./chunk-RHOWYIWT.js";
34
34
  import "./chunk-Q7NQY5HV.js";
35
35
  import "./chunk-N4WZLWJ4.js";
36
- import "./chunk-A2I76FTA.js";
37
- import "./chunk-CX5SL5HP.js";
36
+ import "./chunk-ELU3XQEA.js";
37
+ import "./chunk-64PONSA5.js";
38
38
  import "./chunk-KGXPLI7W.js";
39
39
  import "./chunk-VUFMZEIM.js";
40
40
  export {
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  writeStackConfig
4
- } from "./chunk-HSPHXG6W.js";
4
+ } from "./chunk-3Z5TDXCJ.js";
5
5
  import "./chunk-AEEMRFZV.js";
6
6
  import "./chunk-VOEJYRIK.js";
7
7
  import "./chunk-EI7CQQ7D.js";
8
- import "./chunk-CX5SL5HP.js";
8
+ import "./chunk-64PONSA5.js";
9
9
  import "./chunk-KGXPLI7W.js";
10
10
  export {
11
11
  writeStackConfig
@@ -5,8 +5,8 @@ import {
5
5
  stateDir,
6
6
  statePath,
7
7
  writeLocalState
8
- } from "./chunk-A2I76FTA.js";
9
- import "./chunk-CX5SL5HP.js";
8
+ } from "./chunk-ELU3XQEA.js";
9
+ import "./chunk-64PONSA5.js";
10
10
  import "./chunk-KGXPLI7W.js";
11
11
  export {
12
12
  localStateToConnectionInfo,
@@ -5,7 +5,7 @@ import {
5
5
  getConfigPath,
6
6
  requireConfig,
7
7
  setConfig
8
- } from "./chunk-CX5SL5HP.js";
8
+ } from "./chunk-64PONSA5.js";
9
9
  import "./chunk-KGXPLI7W.js";
10
10
  export {
11
11
  getConfig,
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isTailscaleIpv4,
4
+ parseStatus,
5
+ rootPrefix
6
+ } from "./chunk-X56CQ6GB.js";
7
+ import {
8
+ formatKnownHostsLine,
9
+ keyTypeFromBlob,
10
+ verifyAgainstKnownHosts
11
+ } from "./chunk-MC5ATCXS.js";
12
+ import "./chunk-64PONSA5.js";
13
+ import "./chunk-KGXPLI7W.js";
14
+
15
+ // src/harden/tailscale-cutover.ts
16
+ import { readFileSync, appendFileSync, mkdirSync } from "fs";
17
+ import path from "path";
18
+ function parseHostKeys(text) {
19
+ const out = [];
20
+ for (const line of text.split("\n")) {
21
+ const [declared, b64] = line.trim().split(/\s+/);
22
+ if (!declared || !b64) continue;
23
+ let blob;
24
+ try {
25
+ blob = Buffer.from(b64, "base64");
26
+ } catch {
27
+ continue;
28
+ }
29
+ const actual = keyTypeFromBlob(blob);
30
+ if (!actual || actual !== declared) continue;
31
+ out.push({ type: actual, blob });
32
+ }
33
+ return out;
34
+ }
35
+ function pinKeys(knownHostsPath, host, port, keys) {
36
+ let content = "";
37
+ try {
38
+ content = readFileSync(knownHostsPath, "utf-8");
39
+ } catch {
40
+ }
41
+ let added = 0;
42
+ mkdirSync(path.dirname(knownHostsPath), { recursive: true });
43
+ for (const k of keys) {
44
+ if (verifyAgainstKnownHosts(content, host, port, k.blob) === "match") continue;
45
+ const line = formatKnownHostsLine(host, port, k.type, k.blob);
46
+ appendFileSync(knownHostsPath, line, "utf-8");
47
+ content += line;
48
+ added += 1;
49
+ }
50
+ return added;
51
+ }
52
+ async function verifyTailnetAddress(publicConn, deps) {
53
+ const status = parseStatus((await deps.exec("tailscale status --json 2>/dev/null || true")).stdout);
54
+ if (status.state !== "Running" || !status.ipv4 || !isTailscaleIpv4(status.ipv4)) {
55
+ return {
56
+ ok: false,
57
+ reason: "The host is not on a tailnet yet, so there is no address to move to. Run `clawops harden --options tailscale` first."
58
+ };
59
+ }
60
+ const ip = status.ipv4;
61
+ const keys = parseHostKeys((await deps.exec("cat /etc/ssh/ssh_host_*_key.pub 2>/dev/null")).stdout);
62
+ if (keys.length === 0) {
63
+ return {
64
+ ok: false,
65
+ reason: "Could not read the host keys over the public connection, so the tailnet address cannot be tied to this host. Nothing was changed."
66
+ };
67
+ }
68
+ const pinned = pinKeys(publicConn.knownHostsPath, ip, publicConn.port, keys);
69
+ const reached = await deps.probe({ ...publicConn, host: ip });
70
+ if (!reached) {
71
+ return {
72
+ ok: false,
73
+ reason: `The host is on the tailnet as ${ip}, but this machine could not reach it there. The usual cause is that this machine is not on the same tailnet: install Tailscale here and sign in to the same account. clawops still uses the public address; nothing else was changed.`
74
+ };
75
+ }
76
+ return { ok: true, ip, ...status.hostname ? { hostname: status.hostname } : {}, pinned };
77
+ }
78
+ async function probeSsh(conn, signal) {
79
+ const { connect } = await import("./ssh-VAI5DU5M.js");
80
+ let session;
81
+ try {
82
+ session = await connect({
83
+ host: conn.host,
84
+ port: conn.port,
85
+ user: conn.user,
86
+ privateKeyPath: conn.privateKeyPath,
87
+ knownHostsPath: conn.knownHostsPath,
88
+ signal
89
+ });
90
+ return (await session.exec("true", signal)).code === 0;
91
+ } catch {
92
+ return false;
93
+ } finally {
94
+ session?.close();
95
+ }
96
+ }
97
+ async function leaveTailnet(exec) {
98
+ const sudo = await rootPrefix(exec);
99
+ const r = await exec(`${sudo}tailscale logout 2>&1`);
100
+ if (r.code === 0) return { ok: true };
101
+ if (/not logged in|NeedsLogin/i.test(r.stdout)) return { ok: true };
102
+ return {
103
+ ok: false,
104
+ reason: `tailscale logout failed on the host: ${r.stdout.trim().split("\n").slice(-1)[0] || `exit ${r.code}`}`
105
+ };
106
+ }
107
+ export {
108
+ leaveTailnet,
109
+ parseHostKeys,
110
+ pinKeys,
111
+ probeSsh,
112
+ verifyTailnetAddress
113
+ };
@@ -4,7 +4,7 @@ import {
4
4
  validatePlan,
5
5
  validatePlanConfig,
6
6
  validatePlanNetwork
7
- } from "./chunk-BSLIYB5Q.js";
7
+ } from "./chunk-XFN3YDZE.js";
8
8
  import "./chunk-JW5A7E3X.js";
9
9
  export {
10
10
  assertValidPlan,
package/package.json CHANGED
@@ -1,7 +1,38 @@
1
1
  {
2
2
  "name": "@clawops/cli",
3
- "version": "2.0.1",
4
- "description": "Deploy and manage self-hosted OpenClaw instances across clouds",
3
+ "version": "2.1.0",
4
+ "description": "Deploy and operate self-hosted OpenClaw across AWS, GCP, Azure and local VMs — a CLI and an MCP server in one.",
5
+ "keywords": [
6
+ "openclaw",
7
+ "mcp",
8
+ "mcp-server",
9
+ "model-context-protocol",
10
+ "pulumi",
11
+ "pulumi-automation-api",
12
+ "infrastructure-as-code",
13
+ "iac",
14
+ "devops",
15
+ "deployment",
16
+ "self-hosted",
17
+ "cli",
18
+ "aws",
19
+ "gcp",
20
+ "azure",
21
+ "ai-agent",
22
+ "agent-ops",
23
+ "claude-code",
24
+ "typescript",
25
+ "nodejs"
26
+ ],
27
+ "homepage": "https://clawops.fyi",
28
+ "bugs": {
29
+ "url": "https://github.com/dfridkin/clawops/issues"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/dfridkin/clawops.git"
34
+ },
35
+ "license": "MPL-2.0",
5
36
  "type": "module",
6
37
  "bin": {
7
38
  "clawops": "./dist/cli.js"
@@ -13,11 +44,6 @@
13
44
  "dist",
14
45
  "spec"
15
46
  ],
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/dfridkin/clawops.git"
19
- },
20
- "homepage": "https://clawops.fyi",
21
47
  "mcpName": "io.github.dfridkin/clawops",
22
48
  "publishConfig": {
23
49
  "access": "public",
@@ -40,7 +66,12 @@
40
66
  "changeset": "changeset",
41
67
  "release": "pnpm build && changeset publish",
42
68
  "test:mutation": "node scripts/dev/mutation-check.mjs",
43
- "graph": "node scripts/dev/graph.mjs"
69
+ "graph": "node scripts/dev/graph.mjs",
70
+ "verify:pack": "node scripts/dev/verify-pack.mjs",
71
+ "verify:mcp": "node scripts/dev/mcp-probe.mjs",
72
+ "verify:docker": "node scripts/dev/verify-docker.mjs",
73
+ "version:packages": "changeset version && node scripts/sync-server-json.mjs",
74
+ "sync:server-json": "node scripts/sync-server-json.mjs"
44
75
  },
45
76
  "dependencies": {
46
77
  "@aws-sdk/client-bedrock": "^3.1063.0",
@@ -162,7 +162,7 @@
162
162
  "type": "string",
163
163
  "format": "cidr"
164
164
  },
165
- "description": "CIDRs allowed to reach the gateway port. Only meaningful with publishGateway \"all\" — under \"loopback\" nothing listens on a routable interface, so these rules would admit traffic to a closed port. clawops refuses that combination rather than creating rules that do nothing."
165
+ "description": "CIDRs allowed to reach the gateway port. Only meaningful with publishGateway \"all\" \u2014 under \"loopback\" nothing listens on a routable interface, so these rules would admit traffic to a closed port. clawops refuses that combination rather than creating rules that do nothing."
166
166
  },
167
167
  "gatewayPort": {
168
168
  "type": "integer",
@@ -181,6 +181,14 @@
181
181
  "authKeyRef": {
182
182
  "type": "string",
183
183
  "description": "Reference to a secret containing the Tailscale auth key"
184
+ },
185
+ "privateOnly": {
186
+ "type": "boolean",
187
+ "description": "The plan closes public SSH and gateway ingress because the stack is reached over its tailnet (WO-34 step 6; ADR 0013). Apply refuses it unless the stack has a verified tailnet address, equal to `ip`, that answers SSH at apply time. Both CIDR lists must be empty."
188
+ },
189
+ "ip": {
190
+ "type": "string",
191
+ "description": "The tailnet address the plan relies on once public ingress is closed. Recorded so the reviewer sees which address remains, and so apply can refuse a plan made for an address the stack no longer has."
184
192
  }
185
193
  }
186
194
  },
@@ -253,6 +261,13 @@
253
261
  },
254
262
  "totalChanges": {
255
263
  "type": "integer"
264
+ },
265
+ "replace": {
266
+ "type": "array",
267
+ "description": "Resources Pulumi will destroy and recreate. A replaced instance loses its boot disk, and with it every byte of OpenClaw state on the host.",
268
+ "items": {
269
+ "$ref": "#/$defs/ResourceRef"
270
+ }
256
271
  }
257
272
  }
258
273
  }
@@ -86,8 +86,9 @@ tools:
86
86
  actually running.
87
87
 
88
88
  Do NOT use when: you already know the problem and want to fix it. This tool only
89
- reports — it changes nothing, and never runs `openclaw doctor --fix`. Repair is
90
- part of clawops_gateway_update, where it is gated on a verified backup.
89
+ reports — it changes nothing, and never runs `openclaw doctor --fix`. No tool
90
+ repairs a gateway: `clawops gateway update` is CLI-only, so tell the user to run
91
+ it themselves, after `clawops backup create`.
91
92
 
92
93
  Every check carries a status: fail (something is wrong that clawops can name),
93
94
  warn (worth knowing, not broken), info (did not apply). `ok` is false only when
@@ -125,8 +126,9 @@ tools:
125
126
 
126
127
  Do NOT use when: the user wants real-time streaming logs (those are not
127
128
  well-suited to tool calls; suggest the user run `clawops logs -f` directly
128
- in their terminal). Do NOT use for instance-level system logs (use
129
- clawops_ssh with `journalctl`).
129
+ in their terminal). Do NOT use for instance-level system logs: no tool runs
130
+ arbitrary remote commands, so tell the user to run `clawops ssh --command
131
+ 'journalctl ...'` themselves.
130
132
  annotations:
131
133
  title: Tail Gateway Logs
132
134
  readOnlyHint: true
@@ -238,7 +240,8 @@ tools:
238
240
  Use when: the user wants to see which agents are running, debug agent
239
241
  routing, or count active workspaces.
240
242
 
241
- Do NOT use when: the user wants agent logs (use clawops_agents_logs).
243
+ Do NOT use when: the user wants one agent's logs — no tool exposes those; tell
244
+ the user to run `clawops agents logs <name>`.
242
245
  annotations:
243
246
  title: List OpenClaw Agents
244
247
  readOnlyHint: true
@@ -332,9 +335,10 @@ tools:
332
335
  Use when: the user explicitly asks to destroy, tear down, delete, or
333
336
  remove a stack. Always confirm the stack name.
334
337
 
335
- Do NOT use when: the user wants to stop the gateway temporarily — use
336
- clawops_gateway_stop instead. Do NOT use when in doubt about which stack;
337
- list first with clawops_stacks_list.
338
+ Do NOT use when: the user wants to stop the gateway temporarily — that is not
339
+ a destroy, and no tool stops a gateway; clawops_gateway_restart is the only
340
+ gateway tool. Do NOT use when in doubt about which stack; list first with
341
+ clawops_stacks_list.
338
342
  annotations:
339
343
  title: Destroy Stack (DESTRUCTIVE)
340
344
  readOnlyHint: false
@@ -393,7 +397,12 @@ tools:
393
397
  stackName: { type: string, optional: true }
394
398
  provider: { type: enum, values: [aws, gcp, azure, local], optional: true }
395
399
  region: { type: string, optional: true }
396
- instanceType: { type: enum, values: [micro, small, medium, large, gpu], optional: true }
400
+ instanceType: { type: string, optional: true, description: "A clawops alias (micro|small|medium|large|gpu) or a machine type the cloud names itself, e.g. t3.small" }
401
+ openclawVersion: { type: string, optional: true, description: "semver, or 'stable'/'dev'" }
402
+ sshCidr: { type: string, optional: true, description: "CIDR(s) allowed to reach SSH, comma-separated, or 'auto' for this machine. Omitted = none, and nothing will be able to connect" }
403
+ gatewayCidr: { type: string, optional: true, description: "CIDR(s) allowed to reach the gateway port, or 'auto'. Requires publishGateway=all" }
404
+ publishGateway: { type: enum, values: [loopback, all], optional: true, description: "Which interface the gateway binds. loopback (default) keeps it off the network" }
405
+ privateOnly: { type: boolean, optional: true, description: "Close public SSH and gateway access; reach the stack over its tailnet. Requires a verified tailnet address (clawops_harden with tailscale), and refuses unless that address answers SSH now" }
397
406
  outPath: { type: string, optional: true, description: "Absolute path to write plan; if omitted, plan returned inline" }
398
407
  output:
399
408
  planPath: { type: string, description: "Absolute path; only set if outPath provided" }
@@ -499,6 +508,55 @@ tools:
499
508
  # COMPOSITE WORKFLOW TOOLS (R2: encode user intent, not API sequences)
500
509
  # ────────────────────────────────────────────────────────────────────────────
501
510
 
511
+ - name: clawops_harden
512
+ toolset: [cli]
513
+ description: |
514
+ Apply security hardening to a deployed stack: SSH, UFW, fail2ban,
515
+ unattended-upgrades, the Docker socket, and per-cloud checks. Optionally join
516
+ the stack to a Tailscale network and reach it there instead of over the public
517
+ internet.
518
+
519
+ Use when: the user asks to harden, secure, or lock down a stack; asks what the
520
+ hardening report says (with dryRun: true, which changes nothing); or asks to put
521
+ a stack on their tailnet.
522
+
523
+ tailscale: true installs Tailscale, joins the tailnet as clawops-<stack>, and
524
+ then moves clawops onto that address — but only after opening an SSH session to
525
+ it, against host keys pinned over the connection already trusted. If that fails,
526
+ nothing is recorded and the public address stays in use. The auth key comes from
527
+ `clawops secret set TAILSCALE_AUTH_KEY` and is never passed through this tool
528
+ (R6).
529
+
530
+ tailscaleRevert: true takes the host back off the tailnet, over its public
531
+ address. On a private-only stack it refuses and names the plan/apply commands
532
+ that reopen SSH first — relay them rather than trying to work around it.
533
+
534
+ To close the public ports afterwards, plan with privateOnly: true and apply that
535
+ plan; this tool does not change firewall rules.
536
+
537
+ Do NOT use when: the user wants to know whether a stack is healthy — that is
538
+ clawops_doctor. Do NOT pass tailscale and tailscaleRevert together.
539
+ annotations:
540
+ title: Harden Stack
541
+ readOnlyHint: false
542
+ destructiveHint: true
543
+ idempotentHint: true
544
+ openWorldHint: true
545
+ input:
546
+ stackName: { type: string, optional: true }
547
+ options: { type: string, optional: true, description: "Comma-separated module IDs; default is every defaultOn module for the provider" }
548
+ dryRun: { type: boolean, optional: true, default: false, description: "Report the current state, change nothing" }
549
+ tailscale: { type: boolean, optional: true, default: false, description: "Join the tailnet, verify this machine reaches the host there, then use that address" }
550
+ tailscaleRevert: { type: boolean, optional: true, default: false, description: "Leave the tailnet and go back to the public address" }
551
+ yes: { type: boolean, optional: true, default: false, description: "Skip elicitation; CI/scripted use only" }
552
+ output:
553
+ modules:
554
+ type: array
555
+ description: "Per module: id, status, whether it changed anything, and detail"
556
+ tailnet:
557
+ type: object
558
+ description: "Set when tailscale or tailscaleRevert ran: the address now in use, or why it was refused"
559
+
502
560
  - name: clawops_workflow_deploy_app
503
561
  toolset: [workflow]
504
562
  description: |