@clawops/cli 1.2.1 → 1.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # clawops
2
2
 
3
- Deploy OpenClaw once. Operate it from your terminal or Claude Code.
3
+ MCP-native infrastructure ops for OpenClaw with read-only mode, destructive-action confirmation, and audit logs built in.
4
4
 
5
5
  **clawops** is a CLI and [MCP](https://modelcontextprotocol.io/) server for deploying and operating
6
6
  self-hosted [OpenClaw](https://github.com/openclaw/openclaw) instances. Provision on AWS, GCP,
@@ -9,6 +9,31 @@ and Cursor drive them through typed MCP tools with explicit safety controls.
9
9
 
10
10
  ---
11
11
 
12
+ ## What's new in v1.4.0
13
+
14
+ **`clawops monitor`** — live dashboard for any deployed stack. Shows gateway health, container
15
+ status, CPU/memory, disk usage, and a rolling log tail. Refreshes on an interval with `[r]`,
16
+ toggles logs with `[l]`, and quits with `[q]`.
17
+
18
+ Run without `--stack` to get an **interactive stack picker** first — probes all registered stacks
19
+ in parallel, shows only running ones by default, and lets you toggle to a full list (`[a]`) where
20
+ not-deployed stacks can be deleted from the registry with `[d]`. Press `[s]` inside the dashboard
21
+ to go back to the picker.
22
+
23
+ ```bash
24
+ clawops monitor # interactive stack selection → dashboard
25
+ clawops monitor --stack prod # jump straight to a named stack
26
+ clawops monitor --stack prod | cat # one-shot snapshot for CI
27
+ ```
28
+
29
+ **`clawops_monitor` MCP tool** — same snapshot as a single structured JSON call, so Claude Code
30
+ and Cursor can check stack health without opening a terminal.
31
+
32
+ **`clawops stacks delete` guard** — now blocks deletion of a still-deployed stack and requires
33
+ `--force` to remove from the registry without tearing down cloud resources first.
34
+
35
+ ---
36
+
12
37
  ## Who this is for
13
38
 
14
39
  - **OpenClaw users** who want the simplest path to self-hosting across cloud or local VMs, with
@@ -393,6 +418,78 @@ Key design decisions:
393
418
 
394
419
  See [`docs/architecture.md`](docs/architecture.md) for a full narrative, and [`docs/decisions/`](docs/decisions/) for ADRs.
395
420
 
421
+ ### Cloud provider stacks
422
+
423
+ 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.
424
+
425
+ #### AWS
426
+
427
+ ```mermaid
428
+ flowchart LR
429
+ subgraph NET["Networking"]
430
+ VPC["VPC (10.0.0.0/16)"]
431
+ IGW[Internet Gateway]
432
+ SUBNET["Subnet (10.0.1.0/24)"]
433
+ RT[Route Table]
434
+ SG["Security Group (ports 22, 18789)"]
435
+ end
436
+ subgraph IAM["IAM"]
437
+ ROLE[IAM Role]
438
+ SSM[SSM Policy Attachment]
439
+ BED["Bedrock Policy Attachment (optional)"]
440
+ IP[Instance Profile]
441
+ end
442
+ subgraph COMPUTE["Compute"]
443
+ KP[EC2 Key Pair]
444
+ EC2["EC2 Instance (Ubuntu 22.04, IMDSv2)"]
445
+ EIP[Elastic IP]
446
+ end
447
+ ```
448
+
449
+ [Detailed diagram →](docs/providers/aws.md#stack-diagram)
450
+
451
+ #### GCP
452
+
453
+ ```mermaid
454
+ flowchart LR
455
+ subgraph NET["Networking"]
456
+ NW[VPC Network]
457
+ SN["Subnetwork (10.0.0.0/24)"]
458
+ FW1["Firewall: SSH port 22 (conditional)"]
459
+ FW2["Firewall: Gateway port 18789 (conditional)"]
460
+ ADDR[Static External IP]
461
+ end
462
+ subgraph COMPUTE["Compute"]
463
+ VM["Compute Instance (Debian 12, 20 GB)"]
464
+ end
465
+ ```
466
+
467
+ [Detailed diagram →](docs/providers/gcp.md#stack-diagram)
468
+
469
+ #### Azure
470
+
471
+ ```mermaid
472
+ flowchart LR
473
+ RG[Resource Group]
474
+ subgraph NET["Networking"]
475
+ VNET["Virtual Network (10.0.0.0/16)"]
476
+ SUBNET["Subnet (10.0.1.0/24)"]
477
+ NSG["Network Security Group (ports 22, 18789)"]
478
+ PIP["Public IP Address (Static)"]
479
+ NIC[Network Interface]
480
+ end
481
+ subgraph COMPUTE["Compute"]
482
+ VM["VM (Ubuntu 22.04, managed identity)"]
483
+ end
484
+ subgraph KV["Key Vault (optional)"]
485
+ VAULT["Key Vault (RBAC, name max 24 chars)"]
486
+ RA["Role Assignment (Secrets User)"]
487
+ SECRET["Secret: gateway-token"]
488
+ end
489
+ ```
490
+
491
+ [Detailed diagram →](docs/providers/azure.md#stack-diagram)
492
+
396
493
  ---
397
494
 
398
495
  ## Development
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  applyPlan
4
- } from "./chunk-LU63NZD3.js";
4
+ } from "./chunk-ZONXY3C6.js";
5
5
  import "./chunk-YTH4L2GN.js";
6
+ import "./chunk-6ZFIFDBJ.js";
6
7
  import "./chunk-BOPSG2LI.js";
7
- import "./chunk-ACYJBSLJ.js";
8
- import "./chunk-BRPU7AQC.js";
9
- import "./chunk-UDNZUSKA.js";
8
+ import "./chunk-T5EX55GP.js";
9
+ import "./chunk-ZFNPM2WG.js";
10
+ import "./chunk-A2I76FTA.js";
10
11
  import "./chunk-CX5SL5HP.js";
11
12
  import "./chunk-KGXPLI7W.js";
12
13
  export {
@@ -142,6 +142,12 @@ var awsProgram = async () => {
142
142
  iamInstanceProfile: instanceProfile.name,
143
143
  keyName: keyPair.keyName,
144
144
  userData: makeStartupScript(openclawVersion, bedrockEnabled),
145
+ // IMDSv2 with hopLimit=2 so Docker containers on this host can reach IMDS
146
+ // and obtain the instance role credentials (required for Bedrock access).
147
+ metadataOptions: {
148
+ httpTokens: "required",
149
+ httpPutResponseHopLimit: 2
150
+ },
145
151
  tags: { Name: "clawops" }
146
152
  });
147
153
  const eip = new aws.ec2.Eip("clawops-eip", {
@@ -164,8 +170,7 @@ var awsProgram = async () => {
164
170
  };
165
171
  };
166
172
  function makeStartupScript(openclawVersion, bedrockEnabled) {
167
- const bedrockEnvSetup = bedrockEnabled ? '\n# Write AWS_PROFILE so the container can use the instance role for Bedrock\necho "AWS_PROFILE=default" > /etc/openclaw.env\n' : "";
168
- const bedrockEnvFlag = bedrockEnabled ? " --env-file /etc/openclaw.env \\\\\n" : "";
173
+ const bedrockEnvFlag = bedrockEnabled ? " -e AWS_DEFAULT_REGION=$(curl -sf http://169.254.169.254/latest/meta-data/placement/region || echo us-east-1) \\\n" : "";
169
174
  return `#!/bin/bash
170
175
  set -euo pipefail
171
176
 
@@ -175,7 +180,7 @@ mkdir -p /home/clawops/.ssh
175
180
  chmod 700 /home/clawops/.ssh
176
181
  chown clawops:clawops /home/clawops/.ssh
177
182
 
178
- # Install Docker if not present (Ubuntu/Debian only; AWS AMI is ubuntu-22.04)
183
+ # Install Docker if not present (AWS AMI is ubuntu-22.04)
179
184
  if ! command -v docker &>/dev/null; then
180
185
  export DEBIAN_FRONTEND=noninteractive
181
186
  apt-get update -q
@@ -193,7 +198,7 @@ if ! command -v docker &>/dev/null; then
193
198
  fi
194
199
 
195
200
  usermod -aG docker clawops
196
- ${bedrockEnvSetup}
201
+
197
202
  # Pull OpenClaw image
198
203
  OPENCLAW_VERSION="${openclawVersion}"
199
204
  docker pull ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION}
@@ -4,12 +4,14 @@
4
4
  import process2 from "process";
5
5
 
6
6
  // src/providers/azure/program.ts
7
+ import { createHash } from "crypto";
7
8
  var GATEWAY_PORT = 18789;
8
9
  var SSH_PORT = 22;
9
10
  var azureProgram = async () => {
10
- const [pulumi, azure, { resolveIngressCidrs, detectEgressIp }] = await Promise.all([
11
+ const [pulumi, azure, random, { resolveIngressCidrs, detectEgressIp }] = await Promise.all([
11
12
  import("@pulumi/pulumi"),
12
13
  import("@pulumi/azure-native"),
14
+ import("@pulumi/random"),
13
15
  import("./firewall-YYDOWDDP.js")
14
16
  ]);
15
17
  const cfg = new pulumi.Config();
@@ -138,8 +140,9 @@ var azureProgram = async () => {
138
140
  }
139
141
  });
140
142
  if (keyVaultEnabled) {
141
- const rawKvName = `clawops-${stackName}-kv`;
142
- const kvName = rawKvName.length > 24 ? rawKvName.slice(0, 24) : rawKvName;
143
+ const safeName = stackName.replace(/[^a-zA-Z0-9-]/g, "-");
144
+ const rawKvName = `clawops-${safeName}-kv`;
145
+ const kvName = rawKvName.length > 24 ? `cl-${createHash("sha256").update(stackName).digest("hex").slice(0, 20)}` : rawKvName;
143
146
  const kv = new azure.keyvault.Vault("clawops-kv", {
144
147
  resourceGroupName: rg.name,
145
148
  location: rg.location,
@@ -157,11 +160,15 @@ var azureProgram = async () => {
157
160
  principalId: pulumi.output(vm.identity).apply((i) => i?.principalId ?? ""),
158
161
  principalType: "ServicePrincipal"
159
162
  });
163
+ const gatewayToken = new random.RandomPassword("clawops-kv-gateway-token", {
164
+ length: 32,
165
+ special: false
166
+ });
160
167
  new azure.keyvault.Secret("clawops-gateway-token", {
161
168
  resourceGroupName: rg.name,
162
169
  vaultName: kv.name,
163
170
  secretName: "gateway-token",
164
- properties: { value: "CHANGEME" }
171
+ properties: { value: gatewayToken.result }
165
172
  });
166
173
  }
167
174
  const resolvedIp = pulumi.output(publicIp.ipAddress).apply((ip) => ip ?? "");
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- writeLocalState
4
- } from "./chunk-BRPU7AQC.js";
5
2
  import {
6
3
  acquireSession
7
4
  } from "./chunk-ZVOEQCNW.js";
8
5
  import "./chunk-4U3LTLWZ.js";
6
+ import {
7
+ writeLocalState
8
+ } from "./chunk-A2I76FTA.js";
9
9
  import "./chunk-CX5SL5HP.js";
10
10
  import {
11
11
  ProviderError
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/plan/overlay-store.ts
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
5
+ import path from "path";
6
+ import os from "os";
7
+ function overlayPath(stackName) {
8
+ return path.join(os.homedir(), ".clawops", "overlays", `${stackName}.json`);
9
+ }
10
+ function saveOverlay(stackName, overlay, secrets) {
11
+ const dir = path.join(os.homedir(), ".clawops", "overlays");
12
+ mkdirSync(dir, { recursive: true });
13
+ const data = { stackName, savedAt: (/* @__PURE__ */ new Date()).toISOString(), overlay, secrets };
14
+ writeFileSync(overlayPath(stackName), JSON.stringify(data, null, 2) + "\n", "utf-8");
15
+ }
16
+ function loadOverlay(stackName) {
17
+ const p = overlayPath(stackName);
18
+ if (!existsSync(p)) return null;
19
+ try {
20
+ return JSON.parse(readFileSync(p, "utf-8"));
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function listOverlays() {
26
+ const dir = path.join(os.homedir(), ".clawops", "overlays");
27
+ if (!existsSync(dir)) return [];
28
+ return readdirSync(dir).filter((f) => f.endsWith(".json")).flatMap((f) => {
29
+ try {
30
+ return [JSON.parse(readFileSync(path.join(dir, f), "utf-8"))];
31
+ } catch {
32
+ return [];
33
+ }
34
+ });
35
+ }
36
+
37
+ export {
38
+ saveOverlay,
39
+ loadOverlay,
40
+ listOverlays
41
+ };
@@ -30,8 +30,20 @@ function writeLocalState(stackName, state) {
30
30
  writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n", "utf-8");
31
31
  renameSync(tmp, dest);
32
32
  }
33
+ function localStateToConnectionInfo(state) {
34
+ return {
35
+ host: state.sshHost,
36
+ port: state.sshPort,
37
+ user: state.sshUser,
38
+ privateKeyPath: state.privateKeyPath,
39
+ knownHostsPath: state.knownHostsPath
40
+ };
41
+ }
33
42
 
34
43
  export {
44
+ stateDir,
45
+ statePath,
35
46
  readLocalState,
36
- writeLocalState
47
+ writeLocalState,
48
+ localStateToConnectionInfo
37
49
  };