@clawops/cli 1.0.0 → 1.2.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 (37) hide show
  1. package/README.md +140 -56
  2. package/dist/apply-KBF7OPWL.js +14 -0
  3. package/dist/{automation-JG2YBEL7.js → automation-RULUSJBW.js} +2 -2
  4. package/dist/{aws-A3323GNM.js → aws-KQBWY43W.js} +12 -10
  5. package/dist/{azure-LNWBK2ZI.js → azure-4EJFVJZL.js} +4 -3
  6. package/dist/{bootstrap-NRIDVS5F.js → bootstrap-UYCOEJQX.js} +48 -12
  7. package/dist/{chunk-LT24GUUO.js → chunk-3QJBNAHW.js} +1 -1
  8. package/dist/{chunk-5XEZAU7V.js → chunk-4U3LTLWZ.js} +6 -83
  9. package/dist/{chunk-PERSDQMT.js → chunk-ACYJBSLJ.js} +7 -7
  10. package/dist/chunk-BOPSG2LI.js +77 -0
  11. package/dist/{chunk-PRYLTCS4.js → chunk-BRPU7AQC.js} +1 -1
  12. package/dist/{chunk-ALSUDYA7.js → chunk-CX5SL5HP.js} +1 -1
  13. package/dist/{chunk-ZSE4QRKE.js → chunk-KGXPLI7W.js} +19 -1
  14. package/dist/chunk-LU63NZD3.js +116 -0
  15. package/dist/chunk-QJ6ERXHN.js +266 -0
  16. package/dist/{chunk-2MJFYG7U.js → chunk-QMXM2SMO.js} +10 -6
  17. package/dist/chunk-UDNZUSKA.js +97 -0
  18. package/dist/chunk-ZVOEQCNW.js +92 -0
  19. package/dist/cli.js +1259 -62
  20. package/dist/context-VUAYRAJY.js +10 -0
  21. package/dist/errors-OK47MQFD.js +21 -0
  22. package/dist/{gcp-OHWCTFLL.js → gcp-SKURG3L6.js} +4 -3
  23. package/dist/generate-WG5VONSE.js +14 -0
  24. package/dist/js-yaml-PTEEG4FO.js +2647 -0
  25. package/dist/{outputs-6DAVEEAZ.js → outputs-DJHBY7EE.js} +2 -2
  26. package/dist/{package-RCGVJCON.js → package-3JZ767DN.js} +8 -3
  27. package/dist/{pool-D4JCUA2V.js → pool-FBFHATDG.js} +3 -2
  28. package/dist/remote-config-JQ77SRC2.js +21 -0
  29. package/dist/secrets-SVZWNAPK.js +7 -0
  30. package/dist/{server-YO4JFQND.js → server-QJRWK2RV.js} +64 -150
  31. package/dist/ssh-IQXNME3D.js +8 -0
  32. package/dist/{store-ARJ2EO6L.js → store-SDUR52Z5.js} +2 -2
  33. package/package.json +6 -3
  34. package/dist/apply-GTVALBAY.js +0 -12
  35. package/dist/chunk-RZE35FQF.js +0 -56
  36. package/dist/context-T52JWL3P.js +0 -10
  37. package/dist/generate-H3D7VQUP.js +0 -14
package/README.md CHANGED
@@ -1,52 +1,85 @@
1
1
  # clawops
2
2
 
3
- **clawops** is a provider-agnostic CLI for deploying and operating self-hosted [OpenClaw](https://github.com/openclaw/openclaw) instances across AWS, GCP, Azure, and local VMs. It uses the [Pulumi Automation API](https://www.pulumi.com/docs/using-pulumi/automation-api/) (embedded — no `pulumi` binary required) for idempotent infrastructure management and exposes every operation as both a CLI command and an [MCP](https://modelcontextprotocol.io/) tool, so Claude Code, Cursor, and other AI agents can drive deployments deterministically.
3
+ Deploy OpenClaw once. Operate it from your terminal or Claude Code.
4
4
 
5
- ```bash
6
- npm install -g @clawops/cli
7
- clawops init --provider aws
8
- clawops plan --out /tmp/my-plan.json # generate + review
9
- clawops apply /tmp/my-plan.json # apply after review
10
- clawops logs -f
11
- ```
5
+ **clawops** is a CLI and [MCP](https://modelcontextprotocol.io/) server for deploying and operating
6
+ self-hosted [OpenClaw](https://github.com/openclaw/openclaw) instances. Provision on AWS, GCP,
7
+ Azure, or any Linux VM — then manage day-to-day operations from the terminal, or let Claude Code
8
+ and Cursor drive them through typed MCP tools with explicit safety controls.
12
9
 
13
10
  ---
14
11
 
15
- ## Why clawops?
12
+ ## Who this is for
16
13
 
17
- Every existing OpenClaw deployment path is cloud-specific, Kubernetes-bound, or fully managed SaaS. No open-source tool unifies provisioning + lifecycle management + remote agent interaction across providers under a single CLI with first-class AI agent integration.
18
-
19
- | Capability | clawops |
20
- |---|---|
21
- | AWS, GCP, Azure, local VM | ✓ |
22
- | Idempotent infra via Pulumi (embedded) | |
23
- | SSH transport (pure Node no system `ssh`) | |
24
- | MCP server (stdio + HTTP) | ✓ |
25
- | Plan → review → apply discipline | ✓ |
26
- | JSON output everywhere (`--json`) | ✓ |
27
- | No credentials in config | ✓ |
14
+ - **OpenClaw users** who want the simplest path to self-hosting across cloud or local VMs, with
15
+ reliable deploy, status checks, logs, backups, and upgrades in a single CLI.
16
+ - **Claude Code / Cursor / MCP users** looking for a real-world reference implementation of safe
17
+ infrastructure operations through MCP — typed tool schemas, read-only mode, destructive-action
18
+ confirmation, and audit logs.
19
+ - **Self-hosted AI and local-first developers** who want to run their own AI assistant without
20
+ committing to Kubernetes, a managed SaaS platform, or a single cloud provider.
28
21
 
29
22
  ---
30
23
 
31
- ## Quick Start
24
+ ## What clawops does
25
+
26
+ - Provisions and tears down OpenClaw infrastructure on **AWS, GCP, Azure, and local VMs** using
27
+ the Pulumi Automation API (embedded — no `pulumi` binary required).
28
+ - Manages day-to-day operations: status, logs, SSH, tunnels, config, agents, gateway, backups.
29
+ - Exposes every operation as a **typed MCP tool** so AI agents can drive ops safely.
30
+ - Enforces a **plan → review → apply** discipline for cloud deployments.
31
+ - Emits **JSON output everywhere** (`--json`) for scripting and automation.
32
+ - Never stores cloud credentials — reads them from your environment's existing CLI profiles.
33
+
34
+ ## What clawops does not do
32
35
 
33
- ### Prerequisites
36
+ - **No high availability or clustering.** Optimized for single-node deployments.
37
+ - **No Kubernetes.** It deploys to VMs, not container orchestration platforms.
38
+ - **No OpenClaw skill/agent authoring.** clawops manages infrastructure; what runs on it is up to
39
+ you and OpenClaw.
40
+ - **No TLS or domain automation** (yet). Bring your own reverse proxy or see
41
+ [`docs/limitations.md`](docs/limitations.md) for the manual path.
42
+ - **No credential storage.** Cloud credentials must be configured in your environment before using
43
+ clawops. They are never written to `~/.clawops/config.json`.
44
+ - **No native Windows.** WSL2 is fully supported; see [`docs/support-matrix.md`](docs/support-matrix.md).
34
45
 
35
- - **Node.js ≥ 22** (LTS)
36
- - Cloud credentials available in the environment (see [Configuration](#configuration))
46
+ ---
47
+
48
+ ## Quick Start — local VM (fastest path)
37
49
 
38
- ### Install
50
+ The local provider needs only a Linux host reachable over SSH — no cloud account required.
51
+
52
+ **Prerequisites:** Node.js ≥ 22, an SSH key, a Linux host (Ubuntu/Debian/RHEL) you can reach.
39
53
 
40
54
  ```bash
41
55
  npm install -g @clawops/cli
42
- # or without a global install:
43
- npx @clawops/cli
56
+
57
+ # Check your environment
58
+ clawops doctor
59
+
60
+ # Configure clawops for a local host
61
+ clawops init --provider local --host 192.168.1.50 --user ubuntu --key-path ~/.ssh/id_ed25519
62
+
63
+ # Bootstrap OpenClaw on the host (installs Docker + OpenClaw over SSH)
64
+ clawops up
65
+
66
+ # Verify it's running
67
+ clawops status
68
+
69
+ # Start the MCP server for Claude Code
70
+ clawops mcp serve
44
71
  ```
45
72
 
46
- ### Provision on AWS
73
+ See [`docs/examples/local-vm.md`](docs/examples/local-vm.md) for the full walkthrough including
74
+ SSH prerequisites, firewall setup, and troubleshooting.
75
+
76
+ ## Quick Start — cloud (AWS)
47
77
 
48
78
  ```bash
49
- # Write ~/.clawops/config.json and generate an SSH key pair
79
+ npm install -g @clawops/cli
80
+ # or: npx @clawops/cli
81
+
82
+ # Requires AWS credentials in your environment (AWS_PROFILE or ~/.aws/credentials)
50
83
  clawops init --provider aws
51
84
 
52
85
  # Edit ~/.clawops/config.json — set stateUrl to your S3 bucket:
@@ -55,19 +88,44 @@ clawops init --provider aws
55
88
  # Generate a deploy plan (runs pulumi preview internally)
56
89
  clawops plan --provider aws --stack default --out /tmp/plan.json
57
90
 
58
- # Review the plan JSON, then apply
91
+ # Review the plan, then apply
59
92
  clawops apply /tmp/plan.json
93
+ ```
60
94
 
61
- # Or preview + apply in one step (no plan file needed)
62
- clawops up
95
+ ---
96
+
97
+ ## Connect Claude Code
98
+
99
+ Add to your Claude Code MCP config (`~/.claude.json` or project `.mcp.json`):
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "clawops": {
105
+ "command": "clawops",
106
+ "args": ["mcp", "serve", "--read-only"]
107
+ }
108
+ }
109
+ }
63
110
  ```
64
111
 
65
- ### Day-to-day operations
112
+ **Start with `--read-only`** — it enables status, logs, config reads, and diagnostics while
113
+ blocking any operation that mutates infrastructure. Remove `--read-only` only once you understand
114
+ which tools are destructive and have reviewed [`docs/security/mcp-safety.md`](docs/security/mcp-safety.md).
115
+
116
+ Destructive tools (`clawops_destroy`, `clawops_up`, `clawops_config_set`, etc.) require explicit
117
+ confirmation from the agent before executing — they will never run silently.
118
+
119
+ For Cursor, VS Code, or HTTP mode setup see [`docs/mcp/`](docs/mcp/).
120
+
121
+ ---
122
+
123
+ ## Day-to-day operations
66
124
 
67
125
  ```bash
68
- clawops status # Show stack outputs: IP, gateway URL, SSH info
126
+ clawops status # Stack outputs: IP, gateway URL, SSH info
69
127
  clawops logs -f # Tail OpenClaw logs over SSH
70
- clawops ssh # Open an interactive SSH session
128
+ clawops ssh # Interactive SSH session
71
129
  clawops ssh --command "docker ps"
72
130
 
73
131
  clawops config get maxAgents
@@ -75,7 +133,7 @@ clawops config set maxAgents 8
75
133
 
76
134
  clawops tunnel # Port-forward gateway UI to localhost
77
135
 
78
- clawops destroy --yes # Destroy cloud-provider stack (AWS/GCP/Azure)
136
+ clawops destroy --yes # Destroy cloud-provider stack
79
137
  clawops down --yes # Destroy local-provider stack
80
138
  ```
81
139
 
@@ -85,12 +143,13 @@ clawops down --yes # Destroy local-provider stack
85
143
 
86
144
  | Command | Description |
87
145
  |---|---|
88
- | `init` | Interactive setup wizard — writes config, generates SSH key pair |
146
+ | `setup` | First-run wizard — guided LLM, integrations, and deploy-plan generation |
147
+ | `init` | Register a stack in `~/.clawops/config.json` without provisioning |
89
148
  | `up` | Provision or update stack (`--dry-run` for preview) |
90
149
  | `down` | Destroy local-provider stack (requires `--yes`; `--dry-run` shows current outputs) |
91
150
  | `destroy` | Destroy cloud-provider stack with confirmation prompt (`--dry-run` shows current outputs) |
92
151
  | `status` | Show stack outputs: IP, gateway URL, region, provisioned time |
93
- | `plan` | Generate a Maker deploy-plan JSON artifact (dry-run safe) |
152
+ | `plan` | Generate a deploy-plan JSON artifact (dry-run safe) |
94
153
  | `apply` | Apply a previously reviewed plan file (`--dry-run` validates and shows diff without applying) |
95
154
  | `ssh` | Interactive SSH session or run a remote command |
96
155
  | `logs` | Stream OpenClaw logs (`-f`, `--tail N`, `--since 5m`) |
@@ -115,7 +174,7 @@ For non-local providers, clawops enforces a review-before-apply discipline:
115
174
  # 1. Generate a plan — runs `pulumi preview` internally, produces JSON
116
175
  clawops plan --provider aws --region us-east-1 --out /tmp/plan.json
117
176
 
118
- # 2. Review plan.json — it shows exactly which resources will change
177
+ # 2. Review plan.json — the `diff` field shows projected changes at plan-generation time
119
178
  cat /tmp/plan.json | jq .diff
120
179
 
121
180
  # 3. Apply — reads and validates the plan file, then runs `pulumi up`
@@ -125,24 +184,28 @@ clawops apply /tmp/plan.json
125
184
  clawops apply /tmp/plan.json --yes # skip prompt in automation
126
185
  ```
127
186
 
128
- The plan JSON conforms to `spec/deploy-plan.schema.json` (AJV-validated). Plans are portable — generated on one machine, applied on another.
187
+ The plan JSON conforms to `spec/deploy-plan.schema.json` (AJV-validated) and captures reviewed
188
+ intent: provider, region, instance type, CIDR ranges, and OpenClaw version. `apply` re-runs
189
+ `pulumi up` using those parameters against the current live state — it does not replay a locked
190
+ execution artifact. Review and apply in the same session to minimize drift risk.
191
+
192
+ See [`docs/plan-apply.md`](docs/plan-apply.md) for full semantics, drift guidance, and the safe CI pattern.
129
193
 
130
194
  ---
131
195
 
132
196
  ## MCP server
133
197
 
134
- clawops ships an embedded [MCP](https://modelcontextprotocol.io/) server. Claude Code, Cursor, and any MCP-compatible agent can drive deployments without leaving the chat interface.
198
+ clawops ships an embedded [MCP](https://modelcontextprotocol.io/) server. Claude Code, Cursor, and
199
+ any MCP-compatible agent can drive deployments without leaving the chat interface.
135
200
 
136
201
  ### Stdio mode (Claude Code / VS Code)
137
202
 
138
- Add to your Claude Code MCP config (`~/.claude.json` or project `.mcp.json`):
139
-
140
203
  ```json
141
204
  {
142
205
  "mcpServers": {
143
206
  "clawops": {
144
207
  "command": "clawops",
145
- "args": ["mcp", "serve"]
208
+ "args": ["mcp", "serve", "--read-only"]
146
209
  }
147
210
  }
148
211
  }
@@ -155,27 +218,31 @@ clawops mcp serve --http --port 3333 --bind 127.0.0.1
155
218
  # MCP HTTP server listening on 127.0.0.1:3333
156
219
  ```
157
220
 
158
- Point your MCP client at `http://127.0.0.1:3333`.
221
+ Do not bind to a non-loopback address without additional authentication controls in front of it.
159
222
 
160
223
  ### Available tools
161
224
 
162
225
  | Tool | Toolset | Description |
163
226
  |---|---|---|
164
- | `clawops_up` | cli | Provision or update a stack |
165
227
  | `clawops_status` | read | Show stack outputs |
166
228
  | `clawops_logs_tail` | read | Tail OpenClaw logs |
167
- | `clawops_ssh_exec` | cli | Run a command over SSH |
168
- | `clawops_plan` | cli | Generate a deploy plan |
169
- | `clawops_apply` | cli | Apply a plan file |
170
- | `clawops_destroy` | cli | Destroy a stack (elicits confirmation) |
171
229
  | `clawops_config_get` | read | Read a remote config value |
172
- | `clawops_config_set` | cli | Write a remote config value |
173
230
  | `clawops_agents_list` | read | List running agents |
174
- | `clawops_stacks_list` | admin | List all stacks and their state |
175
231
  | `clawops_task_status` | read | Poll a long-running task |
232
+ | `clawops_stacks_list` | admin | List all stacks and their state |
233
+ | `clawops_up` | cli | Provision or update a stack |
234
+ | `clawops_plan` | cli | Generate a deploy plan |
235
+ | `clawops_apply` | cli | Apply a plan file |
236
+ | `clawops_ssh_exec` | cli | Run a command over SSH |
237
+ | `clawops_config_set` | cli | Write a remote config value |
238
+ | `clawops_destroy` | cli | Destroy a stack (elicits confirmation) |
176
239
  | `clawops_workflow_deploy_app` | workflow | End-to-end deploy: plan → confirm → apply → status |
177
240
 
178
- Destructive tools require explicit confirmation (R19 elicitation) unless `yes: true` is passed.
241
+ `read` toolset tools are available in `--read-only` mode. All other toolsets require full mode.
242
+ Destructive tools require explicit confirmation (elicitation) unless `yes: true` is passed.
243
+
244
+ See [`docs/security/tool-risk-matrix.md`](docs/security/tool-risk-matrix.md) for the full risk
245
+ classification of every tool.
179
246
 
180
247
  ---
181
248
 
@@ -215,6 +282,17 @@ Config lives at `~/.clawops/config.json` (override with `$CLAWOPS_HOME`).
215
282
 
216
283
  ---
217
284
 
285
+ ## Known limitations
286
+
287
+ See [`docs/limitations.md`](docs/limitations.md) for the full list. Key points:
288
+
289
+ - **Single-node deployments only** — not a high-availability or clustering platform.
290
+ - **`clawops apply` is not an immutable plan execution** — see [`docs/plan-apply.md`](docs/plan-apply.md).
291
+ - **No TLS/domain automation** in the current release.
292
+ - **MCP tools execute privileged operations** — use `--read-only` for first evaluation.
293
+
294
+ ---
295
+
218
296
  ## Architecture
219
297
 
220
298
  ```
@@ -241,7 +319,7 @@ Key design decisions:
241
319
  - **State in cloud blob storage:** GCS (`gs://`), S3 (`s3://`), Azure Blob — no local state files, no `pulumi.yaml`
242
320
  - **SSH via `ssh2`:** never shells out to `/usr/bin/ssh`; TOFU host verification against `~/.clawops/known_hosts`; connection pool with 5-min idle TTL
243
321
  - **Plan → apply discipline:** every non-local deployment goes through `generatePlan()` → review → `applyPlan()`; destructive changes always require human review of the plan JSON
244
- - **MCP-first:** every CLI operation has a typed MCP tool; schemas generated from `spec/mcp-tools.yaml`; all destructive tools use R19 elicitation
322
+ - **MCP-first:** every CLI operation has a typed MCP tool; schemas generated from `spec/mcp-tools.yaml`; all destructive tools use elicitation
245
323
 
246
324
  See [`docs/architecture.md`](docs/architecture.md) for a full narrative, and [`docs/decisions/`](docs/decisions/) for ADRs.
247
325
 
@@ -264,8 +342,9 @@ pnpm dev doctor # verify toolchain
264
342
  ```bash
265
343
  pnpm dev # run CLI from src/ via tsx
266
344
  pnpm build # tsup → dist/
267
- pnpm test # vitest (356 tests, ~2s)
345
+ pnpm test # vitest (493 tests, ~3s)
268
346
  pnpm test:changed # vitest --changed (fast edit loop)
347
+ pnpm test:integration # Docker-based SSH integration tests
269
348
  pnpm typecheck # tsc --noEmit
270
349
  pnpm lint # eslint src/ tests/ scripts/ (--max-warnings=0)
271
350
  pnpm gen:schemas # regenerate src/providers/types.ts + src/mcp/tools/_generated.ts
@@ -281,7 +360,10 @@ pnpm changeset # record a release note before merging
281
360
  | `SPEC.md` | Full technical specification (milestones, rules, schemas) |
282
361
  | `DESIGN_RULES.md` | 25 normative rules (R1–R25) referenced throughout the codebase |
283
362
  | `docs/architecture.md` | Narrative system overview |
363
+ | `docs/plan-apply.md` | Plan/apply semantics, drift guidance, CI pattern |
284
364
  | `docs/ci.md` | CI integration guide: OIDC, env vars, plan → apply in CI |
365
+ | `docs/security/` | MCP safety model, tool risk matrix, redaction, audit logs |
366
+ | `docs/providers/matrix.md` | Per-provider capability matrix |
285
367
  | `docs/decisions/` | Architecture Decision Records |
286
368
  | `.claude/skills/` | Invokable procedures: `/add-provider`, `/release`, `/tdd`, `/mcp-tool` |
287
369
  | `.claude/rules/` | Path-scoped lint rules loaded by Claude Code |
@@ -326,7 +408,9 @@ Use `pnpm changeset` to record a release note before merging a `feat` or `fix`.
326
408
  | M4 — Local VM | ✅ | Local adapter (SSH bootstrap, no Pulumi); `doctor` |
327
409
  | M5 — MCP Layer | ✅ | `mcp serve` (stdio), all CLI ops as MCP tools, progress tracking |
328
410
  | M6 — Plan/Apply | ✅ | `plan` + `apply`; deploy-plan schema; MCP HTTP transport; `workflow_deploy_app` |
329
- | M7 — v1.0 Polish | ✅ | Full `doctor` surface; `destroy` command; `--dry-run` on `up`/`down`/`destroy`/`apply`/`config`; `release.yml`; CI integration guide |
411
+ | M7 — v1.0 Polish | ✅ | Full `doctor` surface; `destroy` command; `--dry-run` across commands; CI guide |
412
+
413
+ See [`docs/roadmap.md`](docs/roadmap.md) for the public roadmap and upcoming work.
330
414
 
331
415
  ---
332
416
 
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ applyPlan
4
+ } from "./chunk-LU63NZD3.js";
5
+ import "./chunk-YTH4L2GN.js";
6
+ import "./chunk-BOPSG2LI.js";
7
+ import "./chunk-ACYJBSLJ.js";
8
+ import "./chunk-BRPU7AQC.js";
9
+ import "./chunk-UDNZUSKA.js";
10
+ import "./chunk-CX5SL5HP.js";
11
+ import "./chunk-KGXPLI7W.js";
12
+ export {
13
+ applyPlan
14
+ };
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getConfigDir
4
- } from "./chunk-ALSUDYA7.js";
5
- import "./chunk-ZSE4QRKE.js";
4
+ } from "./chunk-CX5SL5HP.js";
5
+ import "./chunk-KGXPLI7W.js";
6
6
 
7
7
  // src/pulumi/automation.ts
8
8
  import path from "path";
@@ -15,7 +15,7 @@ var awsProgram = async () => {
15
15
  const cfg = new pulumi.Config();
16
16
  const instanceType = cfg.get("instanceType") ?? "t3.small";
17
17
  const region = cfg.get("region") ?? "us-east-1";
18
- const openclawVersion = cfg.get("openclawVersion") ?? "stable";
18
+ const openclawVersion = cfg.get("openclawVersion") ?? "latest";
19
19
  const accessMode = cfg.get("accessMode") ?? "restricted";
20
20
  const allowedCidrs = cfg.get("allowedCidrs") ?? "";
21
21
  const sshCidrs = cfg.get("sshCidrs") ?? "";
@@ -114,7 +114,8 @@ var awsProgram = async () => {
114
114
  if (bedrockEnabled) {
115
115
  new aws.iam.RolePolicyAttachment("clawops-bedrock", {
116
116
  role: role.name,
117
- policyArn: "arn:aws:iam::aws:policy/AmazonBedrockReadOnly"
117
+ // FullAccess required for bedrock:InvokeModel — ReadOnly only covers describe/list.
118
+ policyArn: "arn:aws:iam::aws:policy/AmazonBedrockFullAccess"
118
119
  });
119
120
  }
120
121
  const instanceProfile = new aws.iam.InstanceProfile("clawops-profile", {
@@ -163,10 +164,8 @@ var awsProgram = async () => {
163
164
  };
164
165
  };
165
166
  function makeStartupScript(openclawVersion, bedrockEnabled) {
166
- const bedrockEnvFile = bedrockEnabled ? `
167
- # Write AWS_PROFILE for Bedrock
168
- echo "AWS_PROFILE=default" > /etc/openclaw.env
169
- ` : "";
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" : "";
170
169
  return `#!/bin/bash
171
170
  set -euo pipefail
172
171
 
@@ -176,8 +175,9 @@ mkdir -p /home/clawops/.ssh
176
175
  chmod 700 /home/clawops/.ssh
177
176
  chown clawops:clawops /home/clawops/.ssh
178
177
 
179
- # Install Docker if not present
178
+ # Install Docker if not present (Ubuntu/Debian only; AWS AMI is ubuntu-22.04)
180
179
  if ! command -v docker &>/dev/null; then
180
+ export DEBIAN_FRONTEND=noninteractive
181
181
  apt-get update -q
182
182
  apt-get install -y -q ca-certificates curl gnupg lsb-release
183
183
  install -m 0755 -d /etc/apt/keyrings
@@ -193,16 +193,17 @@ if ! command -v docker &>/dev/null; then
193
193
  fi
194
194
 
195
195
  usermod -aG docker clawops
196
- ${bedrockEnvFile}
196
+ ${bedrockEnvSetup}
197
197
  # Pull OpenClaw image
198
198
  OPENCLAW_VERSION="${openclawVersion}"
199
199
  docker pull ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION}
200
200
 
201
201
  # Create default openclaw.json if not present
202
+ # apply.ts will overwrite this with the plan's config overlay post-provisioning.
202
203
  OPENCLAW_CONFIG=/home/clawops/openclaw.json
203
204
  if [ ! -f "\${OPENCLAW_CONFIG}" ]; then
204
205
  cat > "\${OPENCLAW_CONFIG}" <<'OPENCLAWJSON'
205
- {"version":"2026.4","gateway":{"port":18789,"auth":{"mode":"token"}},"models":{},"channels":[]}
206
+ {"meta":{"lastTouchedVersion":"2026.4"},"gateway":{"port":18789,"auth":{"mode":"token"}},"models":{},"channels":{}}
206
207
  OPENCLAWJSON
207
208
  chown clawops:clawops "\${OPENCLAW_CONFIG}"
208
209
  fi
@@ -215,7 +216,8 @@ docker run -d \\
215
216
  --restart unless-stopped \\
216
217
  -p ${GATEWAY_PORT}:${GATEWAY_PORT} \\
217
218
  -v "\${OPENCLAW_CONFIG}":/app/config.json:ro \\
218
- ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION}
219
+ ${bedrockEnvFlag} ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION} \\
220
+ node openclaw.mjs gateway run --allow-unconfigured
219
221
  `;
220
222
  }
221
223
 
@@ -16,7 +16,7 @@ var azureProgram = async () => {
16
16
  const stackName = pulumi.getStack();
17
17
  const instanceType = cfg.get("instanceType") ?? "Standard_B2s";
18
18
  const location = cfg.get("region") ?? "eastus";
19
- const openclawVersion = cfg.get("openclawVersion") ?? "stable";
19
+ const openclawVersion = cfg.get("openclawVersion") ?? "latest";
20
20
  const accessMode = cfg.get("accessMode") ?? "restricted";
21
21
  const allowedCidrs = cfg.get("allowedCidrs") ?? "";
22
22
  const sshCidrs = cfg.get("sshCidrs") ?? "";
@@ -206,7 +206,7 @@ docker pull ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION}
206
206
  OPENCLAW_CONFIG=/home/clawops/openclaw.json
207
207
  if [ ! -f "\${OPENCLAW_CONFIG}" ]; then
208
208
  cat > "\${OPENCLAW_CONFIG}" <<'OPENCLAWJSON'
209
- {"version":"2026.4","gateway":{"port":18789,"auth":{"mode":"token"}},"models":{},"channels":[]}
209
+ {"meta":{"lastTouchedVersion":"2026.4"},"gateway":{"port":18789,"auth":{"mode":"token"}},"models":{},"channels":{}}
210
210
  OPENCLAWJSON
211
211
  chown clawops:clawops "\${OPENCLAW_CONFIG}"
212
212
  fi
@@ -219,7 +219,8 @@ docker run -d \\
219
219
  --restart unless-stopped \\
220
220
  -p 18789:18789 \\
221
221
  -v "\${OPENCLAW_CONFIG}":/app/config.json:ro \\
222
- ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION}
222
+ ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION} \\
223
+ node openclaw.mjs gateway run --allow-unconfigured
223
224
  `;
224
225
  }
225
226
 
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- acquireSession
4
- } from "./chunk-5XEZAU7V.js";
5
2
  import {
6
3
  writeLocalState
7
- } from "./chunk-PRYLTCS4.js";
8
- import "./chunk-ALSUDYA7.js";
4
+ } from "./chunk-BRPU7AQC.js";
5
+ import {
6
+ acquireSession
7
+ } from "./chunk-ZVOEQCNW.js";
8
+ import "./chunk-4U3LTLWZ.js";
9
+ import "./chunk-CX5SL5HP.js";
9
10
  import {
10
11
  ProviderError
11
- } from "./chunk-ZSE4QRKE.js";
12
+ } from "./chunk-KGXPLI7W.js";
12
13
 
13
14
  // src/providers/local/bootstrap.ts
14
15
  import { readFileSync } from "fs";
@@ -27,7 +28,8 @@ function renderScript(openclawVersion) {
27
28
  async function localBootstrap(opts) {
28
29
  const script = renderScript(opts.openclawVersion);
29
30
  const b64 = Buffer.from(script, "utf-8").toString("base64");
30
- const command = `echo '${b64}' | base64 -d | sudo bash`;
31
+ const command = opts.sudoPassword ? `printf '%s
32
+ ' ${shellQuote(opts.sudoPassword)} | sudo -S -p '' bash -c "echo '${b64}' | base64 -d | bash"` : `echo '${b64}' | base64 -d | sudo bash`;
31
33
  const { session, release } = await acquireSession({
32
34
  host: opts.host,
33
35
  port: opts.port,
@@ -37,12 +39,16 @@ async function localBootstrap(opts) {
37
39
  signal: opts.signal
38
40
  });
39
41
  try {
40
- const result = await session.exec(command, opts.signal);
41
- if (result.code !== 0) {
42
- throw new ProviderError(
43
- `Bootstrap script failed (exit ${result.code}):
42
+ if (opts.onOutput) {
43
+ await execStreaming(session, command, opts.onOutput, opts.signal);
44
+ } else {
45
+ const result = await session.exec(command, opts.signal);
46
+ if (result.code !== 0) {
47
+ throw new ProviderError(
48
+ `Bootstrap script failed (exit ${result.code}):
44
49
  ${result.stderr || result.stdout}`
45
- );
50
+ );
51
+ }
46
52
  }
47
53
  } finally {
48
54
  release();
@@ -86,6 +92,36 @@ async function waitForGateway(host, port, signal) {
86
92
  function sleep(ms) {
87
93
  return new Promise((resolve) => setTimeout(resolve, ms));
88
94
  }
95
+ function shellQuote(s) {
96
+ return `'${s.replace(/'/g, "'\\''")}'`;
97
+ }
98
+ async function execStreaming(session, command, onOutput, signal) {
99
+ const ch = await session.stream(command, signal);
100
+ let leftover = "";
101
+ let stderr = "";
102
+ ch.on("data", (chunk) => {
103
+ const text = leftover + chunk.toString("utf-8");
104
+ const lines = text.split("\n");
105
+ leftover = lines.pop() ?? "";
106
+ for (const line of lines) {
107
+ if (line.trim()) onOutput(line);
108
+ }
109
+ });
110
+ ch.stderr.on("data", (chunk) => {
111
+ stderr += chunk.toString("utf-8");
112
+ });
113
+ const exitCode = await new Promise((resolve, reject) => {
114
+ ch.on("close", (code) => {
115
+ if (leftover.trim()) onOutput(leftover);
116
+ resolve(code ?? 0);
117
+ });
118
+ ch.on("error", reject);
119
+ });
120
+ if (exitCode !== 0) {
121
+ throw new ProviderError(`Bootstrap script failed (exit ${exitCode}):
122
+ ${stderr}`);
123
+ }
124
+ }
89
125
  export {
90
126
  localBootstrap
91
127
  };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  StateError
4
- } from "./chunk-ZSE4QRKE.js";
4
+ } from "./chunk-KGXPLI7W.js";
5
5
 
6
6
  // src/pulumi/outputs.ts
7
7
  function extractBaseOutputs(raw) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  NetworkError
4
- } from "./chunk-ZSE4QRKE.js";
4
+ } from "./chunk-KGXPLI7W.js";
5
5
 
6
6
  // src/transport/ssh.ts
7
7
  import { readFileSync, appendFileSync, mkdirSync } from "fs";
@@ -116,6 +116,10 @@ async function connect(opts) {
116
116
  );
117
117
  }
118
118
  return new Promise((resolve, reject) => {
119
+ if (opts.signal?.aborted) {
120
+ reject(new NetworkError("Connection aborted"));
121
+ return;
122
+ }
119
123
  const client = new Client();
120
124
  const onAbort = () => {
121
125
  client.destroy();
@@ -168,87 +172,6 @@ function verifyHostKey(host, port, keyHash, knownHostsPath) {
168
172
  return true;
169
173
  }
170
174
 
171
- // src/transport/pool.ts
172
- var IDLE_TTL_MS = 5 * 60 * 1e3;
173
- var MAX_PER_HOST = 4;
174
- var CLEANUP_INTERVAL_MS = 3e4;
175
- var pool = /* @__PURE__ */ new Map();
176
- var cleanupTimer = null;
177
- function poolKey(opts) {
178
- return `${opts.user}@${opts.host}:${opts.port}`;
179
- }
180
- function startCleanupTimer() {
181
- if (cleanupTimer) return;
182
- cleanupTimer = setInterval(() => {
183
- const now = Date.now();
184
- for (const [key, entries] of pool) {
185
- const active = entries.filter((e) => {
186
- if (!e.inUse && now - e.lastUsed > IDLE_TTL_MS) {
187
- e.session.close();
188
- return false;
189
- }
190
- return true;
191
- });
192
- if (active.length === 0) {
193
- pool.delete(key);
194
- } else {
195
- pool.set(key, active);
196
- }
197
- }
198
- if (pool.size === 0 && cleanupTimer) {
199
- clearInterval(cleanupTimer);
200
- cleanupTimer = null;
201
- }
202
- }, CLEANUP_INTERVAL_MS);
203
- cleanupTimer.unref();
204
- }
205
- async function acquireSession(opts) {
206
- const key = poolKey(opts);
207
- const entries = pool.get(key) ?? [];
208
- const idle = entries.find((e) => !e.inUse);
209
- if (idle) {
210
- idle.inUse = true;
211
- idle.lastUsed = Date.now();
212
- return {
213
- session: idle.session,
214
- release() {
215
- idle.inUse = false;
216
- idle.lastUsed = Date.now();
217
- }
218
- };
219
- }
220
- if (entries.length >= MAX_PER_HOST) {
221
- throw new NetworkError(
222
- `SSH connection pool exhausted for ${key} (max ${MAX_PER_HOST} concurrent connections).`
223
- );
224
- }
225
- const session = await connect(opts);
226
- const entry = { session, hostKey: key, lastUsed: Date.now(), inUse: true };
227
- entries.push(entry);
228
- pool.set(key, entries);
229
- startCleanupTimer();
230
- return {
231
- session,
232
- release() {
233
- entry.inUse = false;
234
- entry.lastUsed = Date.now();
235
- }
236
- };
237
- }
238
- function drainPool() {
239
- for (const entries of pool.values()) {
240
- for (const e of entries) {
241
- e.session.close();
242
- }
243
- }
244
- pool.clear();
245
- if (cleanupTimer) {
246
- clearInterval(cleanupTimer);
247
- cleanupTimer = null;
248
- }
249
- }
250
-
251
175
  export {
252
- acquireSession,
253
- drainPool
176
+ connect
254
177
  };