@clawops/cli 0.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/LICENSE +373 -0
- package/README.md +335 -0
- package/dist/apply-GTVALBAY.js +12 -0
- package/dist/automation-JG2YBEL7.js +30 -0
- package/dist/aws-A3323GNM.js +285 -0
- package/dist/azure-LNWBK2ZI.js +295 -0
- package/dist/bootstrap-NRIDVS5F.js +91 -0
- package/dist/chunk-5XEZAU7V.js +254 -0
- package/dist/chunk-ALSUDYA7.js +89 -0
- package/dist/chunk-ENQY5OW2.js +113 -0
- package/dist/chunk-LT24GUUO.js +39 -0
- package/dist/chunk-PERSDQMT.js +114 -0
- package/dist/chunk-PRYLTCS4.js +37 -0
- package/dist/chunk-RZE35FQF.js +56 -0
- package/dist/chunk-YTH4L2GN.js +41 -0
- package/dist/chunk-ZSE4QRKE.js +44 -0
- package/dist/cli.js +1752 -0
- package/dist/context-T52JWL3P.js +10 -0
- package/dist/firewall-YYDOWDDP.js +36 -0
- package/dist/gcp-OHWCTFLL.js +198 -0
- package/dist/generate-Y6O465VB.js +14 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +0 -0
- package/dist/local-DXBEVZ5C.js +47 -0
- package/dist/outputs-6DAVEEAZ.js +8 -0
- package/dist/package-YZFYSSQM.js +109 -0
- package/dist/pool-D4JCUA2V.js +10 -0
- package/dist/providers-2OABPW2E.js +24 -0
- package/dist/server-TKDURITQ.js +1349 -0
- package/dist/store-ARJ2EO6L.js +16 -0
- package/dist/validate-T5M5EHSJ.js +9 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# clawops
|
|
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.
|
|
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
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why clawops?
|
|
16
|
+
|
|
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 | ✓ |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### Prerequisites
|
|
34
|
+
|
|
35
|
+
- **Node.js ≥ 22** (LTS)
|
|
36
|
+
- Cloud credentials available in the environment (see [Configuration](#configuration))
|
|
37
|
+
|
|
38
|
+
### Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g @clawops/cli
|
|
42
|
+
# or without a global install:
|
|
43
|
+
npx @clawops/cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Provision on AWS
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Write ~/.clawops/config.json and generate an SSH key pair
|
|
50
|
+
clawops init --provider aws
|
|
51
|
+
|
|
52
|
+
# Edit ~/.clawops/config.json — set stateUrl to your S3 bucket:
|
|
53
|
+
# "stateUrl": "s3://my-clawops-state"
|
|
54
|
+
|
|
55
|
+
# Generate a deploy plan (runs pulumi preview internally)
|
|
56
|
+
clawops plan --provider aws --stack default --out /tmp/plan.json
|
|
57
|
+
|
|
58
|
+
# Review the plan JSON, then apply
|
|
59
|
+
clawops apply /tmp/plan.json
|
|
60
|
+
|
|
61
|
+
# Or preview + apply in one step (no plan file needed)
|
|
62
|
+
clawops up
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Day-to-day operations
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
clawops status # Show stack outputs: IP, gateway URL, SSH info
|
|
69
|
+
clawops logs -f # Tail OpenClaw logs over SSH
|
|
70
|
+
clawops ssh # Open an interactive SSH session
|
|
71
|
+
clawops ssh --command "docker ps"
|
|
72
|
+
|
|
73
|
+
clawops config get maxAgents
|
|
74
|
+
clawops config set maxAgents 8
|
|
75
|
+
|
|
76
|
+
clawops tunnel # Port-forward gateway UI to localhost
|
|
77
|
+
|
|
78
|
+
clawops destroy --yes # Destroy cloud-provider stack (AWS/GCP/Azure)
|
|
79
|
+
clawops down --yes # Destroy local-provider stack
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Commands
|
|
85
|
+
|
|
86
|
+
| Command | Description |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `init` | Interactive setup wizard — writes config, generates SSH key pair |
|
|
89
|
+
| `up` | Provision or update stack (`--dry-run` for preview) |
|
|
90
|
+
| `down` | Destroy local-provider stack (requires `--yes`; `--dry-run` shows current outputs) |
|
|
91
|
+
| `destroy` | Destroy cloud-provider stack with confirmation prompt (`--dry-run` shows current outputs) |
|
|
92
|
+
| `status` | Show stack outputs: IP, gateway URL, region, provisioned time |
|
|
93
|
+
| `plan` | Generate a Maker deploy-plan JSON artifact (dry-run safe) |
|
|
94
|
+
| `apply` | Apply a previously reviewed plan file (`--dry-run` validates and shows diff without applying) |
|
|
95
|
+
| `ssh` | Interactive SSH session or run a remote command |
|
|
96
|
+
| `logs` | Stream OpenClaw logs (`-f`, `--tail N`, `--since 5m`) |
|
|
97
|
+
| `tunnel` | Local port-forward to gateway UI over SSH |
|
|
98
|
+
| `config` | Get/set remote OpenClaw config values (`--dry-run` shows would-write JSON) |
|
|
99
|
+
| `agents` | List or restart OpenClaw agents |
|
|
100
|
+
| `gateway` | Restart the OpenClaw gateway service |
|
|
101
|
+
| `backup` | Create or restore an OpenClaw state backup |
|
|
102
|
+
| `stacks` | List named stacks and their state |
|
|
103
|
+
| `doctor` | Check Node version, config, SSH key, provider credentials, and Pulumi home |
|
|
104
|
+
| `mcp` | Start the embedded MCP server (`mcp serve`) |
|
|
105
|
+
|
|
106
|
+
Full flag reference: `clawops <command> --help`
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Plan → Apply workflow
|
|
111
|
+
|
|
112
|
+
For non-local providers, clawops enforces a review-before-apply discipline:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# 1. Generate a plan — runs `pulumi preview` internally, produces JSON
|
|
116
|
+
clawops plan --provider aws --region us-east-1 --out /tmp/plan.json
|
|
117
|
+
|
|
118
|
+
# 2. Review plan.json — it shows exactly which resources will change
|
|
119
|
+
cat /tmp/plan.json | jq .diff
|
|
120
|
+
|
|
121
|
+
# 3. Apply — reads and validates the plan file, then runs `pulumi up`
|
|
122
|
+
clawops apply /tmp/plan.json
|
|
123
|
+
|
|
124
|
+
# Without --yes, apply prompts: "Continue? (y/N)"
|
|
125
|
+
clawops apply /tmp/plan.json --yes # skip prompt in automation
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The plan JSON conforms to `spec/deploy-plan.schema.json` (AJV-validated). Plans are portable — generated on one machine, applied on another.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## MCP server
|
|
133
|
+
|
|
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.
|
|
135
|
+
|
|
136
|
+
### Stdio mode (Claude Code / VS Code)
|
|
137
|
+
|
|
138
|
+
Add to your Claude Code MCP config (`~/.claude.json` or project `.mcp.json`):
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"mcpServers": {
|
|
143
|
+
"clawops": {
|
|
144
|
+
"command": "clawops",
|
|
145
|
+
"args": ["mcp", "serve"]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### HTTP mode (remote / multi-client)
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
clawops mcp serve --http --port 3333 --bind 127.0.0.1
|
|
155
|
+
# MCP HTTP server listening on 127.0.0.1:3333
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Point your MCP client at `http://127.0.0.1:3333`.
|
|
159
|
+
|
|
160
|
+
### Available tools
|
|
161
|
+
|
|
162
|
+
| Tool | Toolset | Description |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `clawops_up` | cli | Provision or update a stack |
|
|
165
|
+
| `clawops_status` | read | Show stack outputs |
|
|
166
|
+
| `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
|
+
| `clawops_config_get` | read | Read a remote config value |
|
|
172
|
+
| `clawops_config_set` | cli | Write a remote config value |
|
|
173
|
+
| `clawops_agents_list` | read | List running agents |
|
|
174
|
+
| `clawops_stacks_list` | admin | List all stacks and their state |
|
|
175
|
+
| `clawops_task_status` | read | Poll a long-running task |
|
|
176
|
+
| `clawops_workflow_deploy_app` | workflow | End-to-end deploy: plan → confirm → apply → status |
|
|
177
|
+
|
|
178
|
+
Destructive tools require explicit confirmation (R19 elicitation) unless `yes: true` is passed.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Configuration
|
|
183
|
+
|
|
184
|
+
Config lives at `~/.clawops/config.json` (override with `$CLAWOPS_HOME`).
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"version": 1,
|
|
189
|
+
"defaults": {
|
|
190
|
+
"provider": "aws",
|
|
191
|
+
"stack": "default"
|
|
192
|
+
},
|
|
193
|
+
"stacks": {
|
|
194
|
+
"default": {
|
|
195
|
+
"provider": "aws",
|
|
196
|
+
"region": "us-east-1",
|
|
197
|
+
"stateUrl": "s3://my-clawops-state"
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"ssh": {
|
|
201
|
+
"keyPath": "~/.clawops/id_ed25519",
|
|
202
|
+
"knownHostsPath": "~/.clawops/known_hosts"
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Cloud credentials are never stored in config** — clawops reads them from the environment:
|
|
208
|
+
|
|
209
|
+
| Provider | Credential source |
|
|
210
|
+
|---|---|
|
|
211
|
+
| AWS | `AWS_PROFILE` or standard AWS credential chain (`~/.aws/credentials`) |
|
|
212
|
+
| GCP | `GOOGLE_APPLICATION_CREDENTIALS` or `gcloud auth application-default login` |
|
|
213
|
+
| Azure | `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` or `az login` |
|
|
214
|
+
| Local | SSH host + key configured in `stacks[name].localOpts` |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Architecture
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
clawops
|
|
222
|
+
├── src/cli/ citty-based commands (one file per verb)
|
|
223
|
+
├── src/config/ ~/.clawops/config.json management
|
|
224
|
+
├── src/providers/ Cloud adapters (AWS, GCP, Azure, local)
|
|
225
|
+
│ ├── aws/ Pulumi inline program + ProviderAdapter
|
|
226
|
+
│ ├── gcp/
|
|
227
|
+
│ ├── azure/
|
|
228
|
+
│ └── local/ SSH bootstrap (no Pulumi)
|
|
229
|
+
├── src/pulumi/ Pulumi Automation API wrapper + output helpers
|
|
230
|
+
├── src/transport/ SSH client (ssh2) + connection pool + tunnels
|
|
231
|
+
├── src/mcp/ MCP server, tool handlers, progress tracking
|
|
232
|
+
├── src/plan/ Maker plan generation, AJV validation, apply
|
|
233
|
+
├── src/output/ ASCII table, spinner, JSON, human-readable output
|
|
234
|
+
├── src/errors/ Typed error hierarchy with exit codes
|
|
235
|
+
└── spec/ Machine-readable ground truth (JSON Schema, YAML)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Key design decisions:
|
|
239
|
+
|
|
240
|
+
- **Pulumi Automation API (embedded):** no `pulumi` binary required; Pulumi home is sandboxed to `~/.clawops/.pulumi`; stack programs are inline TypeScript closures
|
|
241
|
+
- **State in cloud blob storage:** GCS (`gs://`), S3 (`s3://`), Azure Blob — no local state files, no `pulumi.yaml`
|
|
242
|
+
- **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
|
+
- **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
|
|
245
|
+
|
|
246
|
+
See [`docs/architecture.md`](docs/architecture.md) for a full narrative, and [`docs/decisions/`](docs/decisions/) for ADRs.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Development
|
|
251
|
+
|
|
252
|
+
### Setup
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
git clone https://github.com/dfridkin/clawops.git
|
|
256
|
+
cd clawops
|
|
257
|
+
# Node 22+ required; use nvm: nvm use
|
|
258
|
+
pnpm install
|
|
259
|
+
pnpm dev doctor # verify toolchain
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Scripts
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
pnpm dev # run CLI from src/ via tsx
|
|
266
|
+
pnpm build # tsup → dist/
|
|
267
|
+
pnpm test # vitest (356 tests, ~2s)
|
|
268
|
+
pnpm test:changed # vitest --changed (fast edit loop)
|
|
269
|
+
pnpm typecheck # tsc --noEmit
|
|
270
|
+
pnpm lint # eslint src/ tests/ scripts/ (--max-warnings=0)
|
|
271
|
+
pnpm gen:schemas # regenerate src/providers/types.ts + src/mcp/tools/_generated.ts
|
|
272
|
+
pnpm gen:schemas --check # CI guard: committed generated files match spec
|
|
273
|
+
pnpm changeset # record a release note before merging
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Project layout
|
|
277
|
+
|
|
278
|
+
| Path | Purpose |
|
|
279
|
+
|---|---|
|
|
280
|
+
| `spec/` | Machine-readable ground truth: JSON Schema, YAML. **Treat as source of truth.** |
|
|
281
|
+
| `SPEC.md` | Full technical specification (milestones, rules, schemas) |
|
|
282
|
+
| `DESIGN_RULES.md` | 25 normative rules (R1–R25) referenced throughout the codebase |
|
|
283
|
+
| `docs/architecture.md` | Narrative system overview |
|
|
284
|
+
| `docs/ci.md` | CI integration guide: OIDC, env vars, plan → apply in CI |
|
|
285
|
+
| `docs/decisions/` | Architecture Decision Records |
|
|
286
|
+
| `.claude/skills/` | Invokable procedures: `/add-provider`, `/release`, `/tdd`, `/mcp-tool` |
|
|
287
|
+
| `.claude/rules/` | Path-scoped lint rules loaded by Claude Code |
|
|
288
|
+
|
|
289
|
+
### Code generation
|
|
290
|
+
|
|
291
|
+
Two files are generated from `spec/` and must not be hand-edited:
|
|
292
|
+
|
|
293
|
+
- `src/providers/types.ts` — `ProviderAdapter` interface from `spec/providers.schema.json`
|
|
294
|
+
- `src/mcp/tools/_generated.ts` — Zod schemas and type exports from `spec/mcp-tools.yaml`
|
|
295
|
+
|
|
296
|
+
Run `pnpm gen:schemas` after modifying either spec file. CI enforces this with `--check`.
|
|
297
|
+
|
|
298
|
+
### Adding a provider
|
|
299
|
+
|
|
300
|
+
Use the `/add-provider` skill in Claude Code, or follow [`src/providers/CLAUDE.md`](src/providers/CLAUDE.md). Every adapter must satisfy `ProviderAdapter` in `src/providers/types.ts` — do not relax the schema to fit the adapter.
|
|
301
|
+
|
|
302
|
+
### Adding an MCP tool
|
|
303
|
+
|
|
304
|
+
Use the `/mcp-tool` skill. The skill adds the tool to `spec/mcp-tools.yaml`, runs `pnpm gen:schemas`, creates the handler in `src/mcp/tools/<toolset>/<name>.ts`, and wires it into the registry. All four annotation hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) are required on every tool.
|
|
305
|
+
|
|
306
|
+
### Conventional commits
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
feat(scope): description
|
|
310
|
+
fix(scope): description
|
|
311
|
+
docs / refactor / chore / test / perf / ci
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Use `pnpm changeset` to record a release note before merging a `feat` or `fix`.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Milestones
|
|
319
|
+
|
|
320
|
+
| Milestone | Status | What ships |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| M0 — Scaffold | ✅ | Tooling, CI, stubs, generated types |
|
|
323
|
+
| M1 — GCP MVP | ✅ | `init` / `up` / `down` / `status` / `ssh` / `logs` on GCP |
|
|
324
|
+
| M2 — Remote Mgmt | ✅ | `tunnel`, `config`, `agents`, `gateway`; SSH connection pool |
|
|
325
|
+
| M3 — AWS + Azure | ✅ | AWS EC2 + Azure VM adapters; `stacks list` |
|
|
326
|
+
| M4 — Local VM | ✅ | Local adapter (SSH bootstrap, no Pulumi); `doctor` |
|
|
327
|
+
| M5 — MCP Layer | ✅ | `mcp serve` (stdio), all CLI ops as MCP tools, progress tracking |
|
|
328
|
+
| 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 |
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## License
|
|
334
|
+
|
|
335
|
+
MPL-2.0 — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
applyPlan
|
|
4
|
+
} from "./chunk-RZE35FQF.js";
|
|
5
|
+
import "./chunk-PERSDQMT.js";
|
|
6
|
+
import "./chunk-PRYLTCS4.js";
|
|
7
|
+
import "./chunk-YTH4L2GN.js";
|
|
8
|
+
import "./chunk-ALSUDYA7.js";
|
|
9
|
+
import "./chunk-ZSE4QRKE.js";
|
|
10
|
+
export {
|
|
11
|
+
applyPlan
|
|
12
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getConfigDir
|
|
4
|
+
} from "./chunk-ALSUDYA7.js";
|
|
5
|
+
import "./chunk-ZSE4QRKE.js";
|
|
6
|
+
|
|
7
|
+
// src/pulumi/automation.ts
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { LocalWorkspace } from "@pulumi/pulumi/automation";
|
|
10
|
+
async function getOrCreateStack(opts) {
|
|
11
|
+
const configDir = opts.configDir ?? getConfigDir();
|
|
12
|
+
return await LocalWorkspace.createOrSelectStack(
|
|
13
|
+
{
|
|
14
|
+
stackName: opts.stack,
|
|
15
|
+
projectName: "clawops",
|
|
16
|
+
program: opts.program
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
pulumiHome: path.join(configDir, ".pulumi"),
|
|
20
|
+
envVars: {
|
|
21
|
+
PULUMI_BACKEND_URL: opts.stateUrl,
|
|
22
|
+
PULUMI_SKIP_UPDATE_CHECK: "1"
|
|
23
|
+
// R6: cloud credentials inherited from process.env, never set here
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
getOrCreateStack
|
|
30
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/providers/aws/index.ts
|
|
4
|
+
import process2 from "process";
|
|
5
|
+
|
|
6
|
+
// src/providers/aws/program.ts
|
|
7
|
+
var GATEWAY_PORT = 18789;
|
|
8
|
+
var SSH_PORT = 22;
|
|
9
|
+
var awsProgram = async () => {
|
|
10
|
+
const [pulumi, aws, { resolveIngressCidrs, detectEgressIp }] = await Promise.all([
|
|
11
|
+
import("@pulumi/pulumi"),
|
|
12
|
+
import("@pulumi/aws"),
|
|
13
|
+
import("./firewall-YYDOWDDP.js")
|
|
14
|
+
]);
|
|
15
|
+
const cfg = new pulumi.Config();
|
|
16
|
+
const instanceType = cfg.get("instanceType") ?? "t3.small";
|
|
17
|
+
const region = cfg.get("region") ?? "us-east-1";
|
|
18
|
+
const openclawVersion = cfg.get("openclawVersion") ?? "stable";
|
|
19
|
+
const accessMode = cfg.get("accessMode") ?? "restricted";
|
|
20
|
+
const allowedCidrs = cfg.get("allowedCidrs") ?? "";
|
|
21
|
+
const sshCidrs = cfg.get("sshCidrs") ?? "";
|
|
22
|
+
const gatewayCidrs = cfg.get("gatewayCidrs") ?? "";
|
|
23
|
+
const bedrockEnabled = cfg.get("bedrockEnabled") === "true";
|
|
24
|
+
const sshPublicKey = cfg.get("sshPublicKey");
|
|
25
|
+
if (!sshPublicKey) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Stack config "sshPublicKey" is required for the AWS adapter. Set it with: pulumi config set --stack <name> sshPublicKey "ssh-ed25519 ..."'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const detectedIp = accessMode === "auto" ? await detectEgressIp("https://checkip.amazonaws.com") : "";
|
|
31
|
+
if (accessMode === "open") {
|
|
32
|
+
process.stderr.write(
|
|
33
|
+
"[clawops] WARNING: accessMode=open allows 0.0.0.0/0 on SSH and gateway ports. Only use this for development/sandbox stacks.\n"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const sshIngressCidrs = resolveIngressCidrs(accessMode, allowedCidrs, sshCidrs, detectedIp);
|
|
37
|
+
const gatewayIngressCidrs = resolveIngressCidrs(accessMode, allowedCidrs, gatewayCidrs, detectedIp);
|
|
38
|
+
const vpc = new aws.ec2.Vpc("clawops-vpc", {
|
|
39
|
+
cidrBlock: "10.0.0.0/16",
|
|
40
|
+
enableDnsHostnames: true,
|
|
41
|
+
enableDnsSupport: true,
|
|
42
|
+
tags: { Name: "clawops" }
|
|
43
|
+
});
|
|
44
|
+
const igw = new aws.ec2.InternetGateway("clawops-igw", {
|
|
45
|
+
tags: { Name: "clawops" }
|
|
46
|
+
});
|
|
47
|
+
new aws.ec2.InternetGatewayAttachment("clawops-igw-attach", {
|
|
48
|
+
vpcId: vpc.id,
|
|
49
|
+
internetGatewayId: igw.id
|
|
50
|
+
});
|
|
51
|
+
const subnet = new aws.ec2.Subnet("clawops-subnet", {
|
|
52
|
+
vpcId: vpc.id,
|
|
53
|
+
cidrBlock: "10.0.1.0/24",
|
|
54
|
+
mapPublicIpOnLaunch: false,
|
|
55
|
+
availabilityZone: pulumi.interpolate`${region}a`,
|
|
56
|
+
tags: { Name: "clawops" }
|
|
57
|
+
});
|
|
58
|
+
const routeTable = new aws.ec2.RouteTable("clawops-rt", {
|
|
59
|
+
vpcId: vpc.id,
|
|
60
|
+
tags: { Name: "clawops" }
|
|
61
|
+
});
|
|
62
|
+
new aws.ec2.Route("clawops-route", {
|
|
63
|
+
routeTableId: routeTable.id,
|
|
64
|
+
destinationCidrBlock: "0.0.0.0/0",
|
|
65
|
+
gatewayId: igw.id
|
|
66
|
+
});
|
|
67
|
+
new aws.ec2.RouteTableAssociation("clawops-rta", {
|
|
68
|
+
subnetId: subnet.id,
|
|
69
|
+
routeTableId: routeTable.id
|
|
70
|
+
});
|
|
71
|
+
const ingressRules = [
|
|
72
|
+
...sshIngressCidrs.map((cidr) => ({
|
|
73
|
+
protocol: "tcp",
|
|
74
|
+
fromPort: SSH_PORT,
|
|
75
|
+
toPort: SSH_PORT,
|
|
76
|
+
cidrBlocks: [cidr],
|
|
77
|
+
description: "SSH"
|
|
78
|
+
})),
|
|
79
|
+
...gatewayIngressCidrs.map((cidr) => ({
|
|
80
|
+
protocol: "tcp",
|
|
81
|
+
fromPort: GATEWAY_PORT,
|
|
82
|
+
toPort: GATEWAY_PORT,
|
|
83
|
+
cidrBlocks: [cidr],
|
|
84
|
+
description: "OpenClaw gateway"
|
|
85
|
+
}))
|
|
86
|
+
];
|
|
87
|
+
const sg = new aws.ec2.SecurityGroup("clawops-sg", {
|
|
88
|
+
vpcId: vpc.id,
|
|
89
|
+
ingress: ingressRules,
|
|
90
|
+
egress: [{
|
|
91
|
+
protocol: "-1",
|
|
92
|
+
fromPort: 0,
|
|
93
|
+
toPort: 0,
|
|
94
|
+
cidrBlocks: ["0.0.0.0/0"],
|
|
95
|
+
description: "Allow all egress"
|
|
96
|
+
}],
|
|
97
|
+
tags: { Name: "clawops" }
|
|
98
|
+
});
|
|
99
|
+
const role = new aws.iam.Role("clawops-role", {
|
|
100
|
+
assumeRolePolicy: JSON.stringify({
|
|
101
|
+
Version: "2012-10-17",
|
|
102
|
+
Statement: [{
|
|
103
|
+
Effect: "Allow",
|
|
104
|
+
Principal: { Service: "ec2.amazonaws.com" },
|
|
105
|
+
Action: "sts:AssumeRole"
|
|
106
|
+
}]
|
|
107
|
+
}),
|
|
108
|
+
tags: { Name: "clawops" }
|
|
109
|
+
});
|
|
110
|
+
new aws.iam.RolePolicyAttachment("clawops-ssm", {
|
|
111
|
+
role: role.name,
|
|
112
|
+
policyArn: "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
|
|
113
|
+
});
|
|
114
|
+
if (bedrockEnabled) {
|
|
115
|
+
new aws.iam.RolePolicyAttachment("clawops-bedrock", {
|
|
116
|
+
role: role.name,
|
|
117
|
+
policyArn: "arn:aws:iam::aws:policy/AmazonBedrockReadOnly"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const instanceProfile = new aws.iam.InstanceProfile("clawops-profile", {
|
|
121
|
+
role: role.name
|
|
122
|
+
});
|
|
123
|
+
const keyPair = new aws.ec2.KeyPair("clawops-keypair", {
|
|
124
|
+
publicKey: sshPublicKey,
|
|
125
|
+
tags: { Name: "clawops" }
|
|
126
|
+
});
|
|
127
|
+
const ami = await aws.ec2.getAmi({
|
|
128
|
+
mostRecent: true,
|
|
129
|
+
owners: ["099720109477"],
|
|
130
|
+
// Canonical
|
|
131
|
+
filters: [
|
|
132
|
+
{ name: "name", values: ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] },
|
|
133
|
+
{ name: "virtualization-type", values: ["hvm"] }
|
|
134
|
+
]
|
|
135
|
+
});
|
|
136
|
+
const instance = new aws.ec2.Instance("clawops-instance", {
|
|
137
|
+
ami: ami.id,
|
|
138
|
+
instanceType,
|
|
139
|
+
subnetId: subnet.id,
|
|
140
|
+
vpcSecurityGroupIds: [sg.id],
|
|
141
|
+
iamInstanceProfile: instanceProfile.name,
|
|
142
|
+
keyName: keyPair.keyName,
|
|
143
|
+
userData: makeStartupScript(openclawVersion, bedrockEnabled),
|
|
144
|
+
tags: { Name: "clawops" }
|
|
145
|
+
});
|
|
146
|
+
const eip = new aws.ec2.Eip("clawops-eip", {
|
|
147
|
+
domain: "vpc",
|
|
148
|
+
tags: { Name: "clawops" }
|
|
149
|
+
});
|
|
150
|
+
new aws.ec2.EipAssociation("clawops-eip-assoc", {
|
|
151
|
+
instanceId: instance.id,
|
|
152
|
+
allocationId: eip.id
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
instanceId: instance.id,
|
|
156
|
+
publicIp: eip.publicIp,
|
|
157
|
+
gatewayUrl: pulumi.interpolate`https://${eip.publicIp}:${GATEWAY_PORT}`,
|
|
158
|
+
sshHost: eip.publicIp,
|
|
159
|
+
sshPort: SSH_PORT,
|
|
160
|
+
sshUser: "ubuntu",
|
|
161
|
+
region,
|
|
162
|
+
provisionedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
function makeStartupScript(openclawVersion, bedrockEnabled) {
|
|
166
|
+
const bedrockEnvFile = bedrockEnabled ? `
|
|
167
|
+
# Write AWS_PROFILE for Bedrock
|
|
168
|
+
echo "AWS_PROFILE=default" > /etc/openclaw.env
|
|
169
|
+
` : "";
|
|
170
|
+
return `#!/bin/bash
|
|
171
|
+
set -euo pipefail
|
|
172
|
+
|
|
173
|
+
# Create clawops user with SSH access
|
|
174
|
+
id -u clawops &>/dev/null || useradd -m -s /bin/bash clawops
|
|
175
|
+
mkdir -p /home/clawops/.ssh
|
|
176
|
+
chmod 700 /home/clawops/.ssh
|
|
177
|
+
chown clawops:clawops /home/clawops/.ssh
|
|
178
|
+
|
|
179
|
+
# Install Docker if not present
|
|
180
|
+
if ! command -v docker &>/dev/null; then
|
|
181
|
+
apt-get update -q
|
|
182
|
+
apt-get install -y -q ca-certificates curl gnupg lsb-release
|
|
183
|
+
install -m 0755 -d /etc/apt/keyrings
|
|
184
|
+
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \\
|
|
185
|
+
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
|
186
|
+
chmod a+r /etc/apt/keyrings/docker.gpg
|
|
187
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \\
|
|
188
|
+
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \\
|
|
189
|
+
> /etc/apt/sources.list.d/docker.list
|
|
190
|
+
apt-get update -q
|
|
191
|
+
apt-get install -y -q docker-ce docker-ce-cli containerd.io
|
|
192
|
+
systemctl enable --now docker
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
usermod -aG docker clawops
|
|
196
|
+
${bedrockEnvFile}
|
|
197
|
+
# Pull OpenClaw image
|
|
198
|
+
OPENCLAW_VERSION="${openclawVersion}"
|
|
199
|
+
docker pull ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION}
|
|
200
|
+
|
|
201
|
+
# Create default openclaw.json if not present
|
|
202
|
+
OPENCLAW_CONFIG=/home/clawops/openclaw.json
|
|
203
|
+
if [ ! -f "\${OPENCLAW_CONFIG}" ]; then
|
|
204
|
+
cat > "\${OPENCLAW_CONFIG}" <<'OPENCLAWJSON'
|
|
205
|
+
{"version":"2026.4","gateway":{"port":18789,"auth":{"mode":"token"}},"models":{},"channels":[]}
|
|
206
|
+
OPENCLAWJSON
|
|
207
|
+
chown clawops:clawops "\${OPENCLAW_CONFIG}"
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
# Start OpenClaw container
|
|
211
|
+
docker stop openclaw 2>/dev/null || true
|
|
212
|
+
docker rm openclaw 2>/dev/null || true
|
|
213
|
+
docker run -d \\
|
|
214
|
+
--name openclaw \\
|
|
215
|
+
--restart unless-stopped \\
|
|
216
|
+
-p ${GATEWAY_PORT}:${GATEWAY_PORT} \\
|
|
217
|
+
-v "\${OPENCLAW_CONFIG}":/app/config.json:ro \\
|
|
218
|
+
ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION}
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/providers/aws/index.ts
|
|
223
|
+
var INSTANCE_TYPE_MAP = {
|
|
224
|
+
micro: "t3.micro",
|
|
225
|
+
small: "t3.small",
|
|
226
|
+
medium: "t3.medium",
|
|
227
|
+
large: "t3.large",
|
|
228
|
+
gpu: "g4dn.xlarge"
|
|
229
|
+
};
|
|
230
|
+
var awsAdapter = {
|
|
231
|
+
name: "aws",
|
|
232
|
+
get program() {
|
|
233
|
+
return awsProgram;
|
|
234
|
+
},
|
|
235
|
+
getConnectionInfo(outputs) {
|
|
236
|
+
return {
|
|
237
|
+
host: String(outputs["sshHost"] ?? ""),
|
|
238
|
+
port: Number(outputs["sshPort"] ?? 22),
|
|
239
|
+
user: String(outputs["sshUser"] ?? "ubuntu"),
|
|
240
|
+
privateKeyPath: String(outputs["privateKeyPath"] ?? ""),
|
|
241
|
+
knownHostsPath: String(outputs["knownHostsPath"] ?? "")
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
normalizeInstanceType(alias) {
|
|
245
|
+
const mapped = INSTANCE_TYPE_MAP[alias];
|
|
246
|
+
if (!mapped) throw new Error(`Unknown instance alias: ${alias}`);
|
|
247
|
+
return mapped;
|
|
248
|
+
},
|
|
249
|
+
defaultRegion() {
|
|
250
|
+
return "us-east-1";
|
|
251
|
+
},
|
|
252
|
+
stateBackendUrl(bucket) {
|
|
253
|
+
return `s3://${bucket}`;
|
|
254
|
+
},
|
|
255
|
+
async validateConfig() {
|
|
256
|
+
const errors = [];
|
|
257
|
+
const hasProfile = Boolean(process2.env["AWS_PROFILE"]);
|
|
258
|
+
const hasKeyId = Boolean(process2.env["AWS_ACCESS_KEY_ID"]);
|
|
259
|
+
const hasOidc = Boolean(process2.env["AWS_ROLE_ARN"] && process2.env["AWS_WEB_IDENTITY_TOKEN_FILE"]);
|
|
260
|
+
if (!hasProfile && !hasKeyId && !hasOidc) {
|
|
261
|
+
const onAws = await checkImds();
|
|
262
|
+
if (!onAws) {
|
|
263
|
+
errors.push(
|
|
264
|
+
"No AWS credentials found. Set AWS_PROFILE, AWS_ACCESS_KEY_ID, or AWS_ROLE_ARN + AWS_WEB_IDENTITY_TOKEN_FILE, or run on an EC2 instance with an IAM instance role."
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { ok: errors.length === 0, errors };
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
async function checkImds() {
|
|
272
|
+
try {
|
|
273
|
+
const res = await fetch(
|
|
274
|
+
"http://169.254.169.254/latest/meta-data/instance-id",
|
|
275
|
+
{ signal: AbortSignal.timeout(1e3) }
|
|
276
|
+
);
|
|
277
|
+
return res.ok;
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
var aws_default = awsAdapter;
|
|
283
|
+
export {
|
|
284
|
+
aws_default as default
|
|
285
|
+
};
|