@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.
- package/README.md +140 -56
- package/dist/apply-KBF7OPWL.js +14 -0
- package/dist/{automation-JG2YBEL7.js → automation-RULUSJBW.js} +2 -2
- package/dist/{aws-A3323GNM.js → aws-KQBWY43W.js} +12 -10
- package/dist/{azure-LNWBK2ZI.js → azure-4EJFVJZL.js} +4 -3
- package/dist/{bootstrap-NRIDVS5F.js → bootstrap-UYCOEJQX.js} +48 -12
- package/dist/{chunk-LT24GUUO.js → chunk-3QJBNAHW.js} +1 -1
- package/dist/{chunk-5XEZAU7V.js → chunk-4U3LTLWZ.js} +6 -83
- package/dist/{chunk-PERSDQMT.js → chunk-ACYJBSLJ.js} +7 -7
- package/dist/chunk-BOPSG2LI.js +77 -0
- package/dist/{chunk-PRYLTCS4.js → chunk-BRPU7AQC.js} +1 -1
- package/dist/{chunk-ALSUDYA7.js → chunk-CX5SL5HP.js} +1 -1
- package/dist/{chunk-ZSE4QRKE.js → chunk-KGXPLI7W.js} +19 -1
- package/dist/chunk-LU63NZD3.js +116 -0
- package/dist/chunk-QJ6ERXHN.js +266 -0
- package/dist/{chunk-2MJFYG7U.js → chunk-QMXM2SMO.js} +10 -6
- package/dist/chunk-UDNZUSKA.js +97 -0
- package/dist/chunk-ZVOEQCNW.js +92 -0
- package/dist/cli.js +1259 -62
- package/dist/context-VUAYRAJY.js +10 -0
- package/dist/errors-OK47MQFD.js +21 -0
- package/dist/{gcp-OHWCTFLL.js → gcp-SKURG3L6.js} +4 -3
- package/dist/generate-WG5VONSE.js +14 -0
- package/dist/js-yaml-PTEEG4FO.js +2647 -0
- package/dist/{outputs-6DAVEEAZ.js → outputs-DJHBY7EE.js} +2 -2
- package/dist/{package-RCGVJCON.js → package-3JZ767DN.js} +8 -3
- package/dist/{pool-D4JCUA2V.js → pool-FBFHATDG.js} +3 -2
- package/dist/remote-config-JQ77SRC2.js +21 -0
- package/dist/secrets-SVZWNAPK.js +7 -0
- package/dist/{server-YO4JFQND.js → server-QJRWK2RV.js} +64 -150
- package/dist/ssh-IQXNME3D.js +8 -0
- package/dist/{store-ARJ2EO6L.js → store-SDUR52Z5.js} +2 -2
- package/package.json +6 -3
- package/dist/apply-GTVALBAY.js +0 -12
- package/dist/chunk-RZE35FQF.js +0 -56
- package/dist/context-T52JWL3P.js +0 -10
- package/dist/generate-H3D7VQUP.js +0 -14
package/README.md
CHANGED
|
@@ -1,52 +1,85 @@
|
|
|
1
1
|
# clawops
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deploy OpenClaw once. Operate it from your terminal or Claude Code.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
##
|
|
12
|
+
## Who this is for
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quick Start — local VM (fastest path)
|
|
37
49
|
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
91
|
+
# Review the plan, then apply
|
|
59
92
|
clawops apply /tmp/plan.json
|
|
93
|
+
```
|
|
60
94
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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 #
|
|
126
|
+
clawops status # Stack outputs: IP, gateway URL, SSH info
|
|
69
127
|
clawops logs -f # Tail OpenClaw logs over SSH
|
|
70
|
-
clawops ssh #
|
|
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
|
|
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
|
-
| `
|
|
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
|
|
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 —
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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`
|
|
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
|
+
};
|
|
@@ -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") ?? "
|
|
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
|
-
|
|
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
|
|
167
|
-
|
|
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
|
-
${
|
|
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
|
-
{"
|
|
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") ?? "
|
|
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
|
-
{"
|
|
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-
|
|
8
|
-
import
|
|
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-
|
|
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 = `
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
NetworkError
|
|
4
|
-
} from "./chunk-
|
|
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
|
-
|
|
253
|
-
drainPool
|
|
176
|
+
connect
|
|
254
177
|
};
|