@clawops/cli 2.0.0 → 2.0.1

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 (75) hide show
  1. package/README.md +98 -12
  2. package/dist/apply-KYFPWI5U.js +27 -0
  3. package/dist/automation-RMAIBNVC.js +44 -0
  4. package/dist/bedrock-XVASJX2K.js +61 -0
  5. package/dist/{bootstrap-DMRPWJGN.js → bootstrap-HNGKY6WW.js} +11 -8
  6. package/dist/{chunk-N44K3LHE.js → chunk-2I7KCMXZ.js} +6 -4
  7. package/dist/{chunk-3WYA2QDM.js → chunk-2Z7J3ERQ.js} +1 -1
  8. package/dist/chunk-3OLUJPCB.js +36 -0
  9. package/dist/chunk-5XT5IAJK.js +69 -0
  10. package/dist/chunk-AEEMRFZV.js +24 -0
  11. package/dist/{chunk-YR3O2I3S.js → chunk-BSLIYB5Q.js} +6 -1
  12. package/dist/{chunk-AHAAEXWC.js → chunk-E4NJSJ54.js} +16 -93
  13. package/dist/chunk-FEK7VJAY.js +16 -0
  14. package/dist/{chunk-6TIYVHPX.js → chunk-GIEVAGEK.js} +7 -33
  15. package/dist/chunk-HSPHXG6W.js +67 -0
  16. package/dist/{chunk-GYV4KNW7.js → chunk-IVWP4QUJ.js} +133 -40
  17. package/dist/{chunk-AX74VXTV.js → chunk-KC3GC3PE.js} +58 -49
  18. package/dist/{chunk-UFRO4H5W.js → chunk-LBE6COC4.js} +56 -40
  19. package/dist/chunk-LEVIW2PV.js +890 -0
  20. package/dist/chunk-MC5ATCXS.js +108 -0
  21. package/dist/chunk-N4WZLWJ4.js +16 -0
  22. package/dist/{chunk-SBLQH2TJ.js → chunk-NUL5F6YV.js} +1 -1
  23. package/dist/chunk-RHOWYIWT.js +52 -0
  24. package/dist/{chunk-WHJMKDV7.js → chunk-STLRHLLV.js} +65 -12
  25. package/dist/chunk-V25QRZRB.js +45 -0
  26. package/dist/chunk-VOEJYRIK.js +33 -0
  27. package/dist/{providers-2OABPW2E.js → chunk-VUFMZEIM.js} +3 -2
  28. package/dist/chunk-W3OLNNJW.js +51 -0
  29. package/dist/cli-WLD3EBEM.js +11 -0
  30. package/dist/cli-auth-GD5SK7JU.js +13 -0
  31. package/dist/cli.js +518 -206
  32. package/dist/context-7EVC6W45.js +20 -0
  33. package/dist/docker-3PDO4TS7.js +12 -0
  34. package/dist/{firewall-T3NTPZJD.js → firewall-5VKZ4WBH.js} +22 -1
  35. package/dist/generate-I6YQ5V2T.js +29 -0
  36. package/dist/{harden-PCDK3UQ5.js → harden-72JMRT4E.js} +8 -5
  37. package/dist/index.d.ts +56 -0
  38. package/dist/known-hosts-file-I3Q3NBYQ.js +22 -0
  39. package/dist/{logs-P7YCKVF4.js → logs-5A7YYA53.js} +1 -1
  40. package/dist/{mcp-wire-BFEY2Y6H.js → mcp-wire-3SWBM7D6.js} +25 -15
  41. package/dist/models-AGTQWYDL.js +32 -0
  42. package/dist/network-args-PYJRJBYO.js +64 -0
  43. package/dist/{package-3HUW57XH.js → package-5BCMEBTE.js} +10 -3
  44. package/dist/passphrase-MJBE45N7.js +15 -0
  45. package/dist/{pool-EX4ZTA5P.js → pool-PIIKJ4L2.js} +3 -2
  46. package/dist/preflight-EF52S3WW.js +162 -0
  47. package/dist/preflight-MWWM5JT3.js +181 -0
  48. package/dist/preflight-TFD4N5ZU.js +141 -0
  49. package/dist/providers-FW6H6OPF.js +11 -0
  50. package/dist/ready-KDNRBNSU.js +95 -0
  51. package/dist/{remote-config-Z6IIZR3D.js → remote-config-3HZLUWEA.js} +3 -2
  52. package/dist/{runtime-LZ7NA53V.js → runtime-X3KW5TCP.js} +4 -2
  53. package/dist/server-GBA7BJGK.js +46 -0
  54. package/dist/ssh-VAI5DU5M.js +11 -0
  55. package/dist/ssh-key-DUO6OKOI.js +7 -0
  56. package/dist/stack-config-R4SPSUQZ.js +12 -0
  57. package/dist/{validate-BOQRWX4F.js → validate-YVI3BJ55.js} +1 -1
  58. package/dist/wait-WO72TRRQ.js +79 -0
  59. package/package.json +10 -3
  60. package/spec/mcp-tools.yaml +21 -2
  61. package/spec/models.yaml +14 -3
  62. package/spec/providers.schema.json +163 -28
  63. package/dist/apply-AM5HERE7.js +0 -19
  64. package/dist/automation-RULUSJBW.js +0 -30
  65. package/dist/aws-AMON7DA7.js +0 -265
  66. package/dist/azure-T4G6OBJQ.js +0 -273
  67. package/dist/chunk-CKSPROD5.js +0 -129
  68. package/dist/chunk-LU2V3LXA.js +0 -114
  69. package/dist/context-KHKWJJMC.js +0 -10
  70. package/dist/gcp-2JBTD7ZM.js +0 -204
  71. package/dist/generate-2XV2GSP2.js +0 -15
  72. package/dist/local-DXBEVZ5C.js +0 -47
  73. package/dist/server-A5AB67GQ.js +0 -36
  74. package/dist/ssh-B3NZAU4I.js +0 -8
  75. package/dist/{version-guard-GPSTCPT7.js → version-guard-XVYW5WFJ.js} +1 -1
@@ -0,0 +1,890 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CONFIG_FILENAME,
4
+ CONTAINER_UID,
5
+ STATE_DIR_HOST_LINUX,
6
+ gatewayRunCommand
7
+ } from "./chunk-GIEVAGEK.js";
8
+ import {
9
+ GATEWAY_PORT
10
+ } from "./chunk-VOEJYRIK.js";
11
+ import {
12
+ INSTANCE_TYPE_MAP as INSTANCE_TYPE_MAP2
13
+ } from "./chunk-FEK7VJAY.js";
14
+ import {
15
+ azureCliAccount
16
+ } from "./chunk-RHOWYIWT.js";
17
+ import {
18
+ INSTANCE_TYPE_MAP
19
+ } from "./chunk-N4WZLWJ4.js";
20
+ import {
21
+ registerProvider
22
+ } from "./chunk-VUFMZEIM.js";
23
+
24
+ // src/providers/aws/index.ts
25
+ import process2 from "process";
26
+
27
+ // src/providers/startup.ts
28
+ function makeStartupScript(opts) {
29
+ const {
30
+ openclawVersion,
31
+ os,
32
+ bedrockEnabled = false,
33
+ publishGateway = "loopback",
34
+ gatewayPort = GATEWAY_PORT
35
+ } = opts;
36
+ const dockerDistro = os === "ubuntu" ? "ubuntu" : "debian";
37
+ const bedrockEnvBlock = bedrockEnabled ? makeBedrockEnvBlock() : "";
38
+ return `#!/bin/bash
39
+ set -euo pipefail
40
+
41
+ # \u2500\u2500 User setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
42
+ id -u clawops &>/dev/null || useradd -m -s /bin/bash clawops
43
+ mkdir -p /home/clawops/.ssh
44
+ chmod 700 /home/clawops/.ssh
45
+ chown clawops:clawops /home/clawops/.ssh
46
+
47
+ # \u2500\u2500 Docker installation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
48
+ if ! command -v docker &>/dev/null; then
49
+ export DEBIAN_FRONTEND=noninteractive
50
+ apt-get update -q
51
+ apt-get install -y -q ca-certificates curl gnupg lsb-release
52
+ install -m 0755 -d /etc/apt/keyrings
53
+ curl -fsSL https://download.docker.com/linux/${dockerDistro}/gpg \\
54
+ -o /etc/apt/keyrings/docker.asc
55
+ chmod a+r /etc/apt/keyrings/docker.asc
56
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \\
57
+ https://download.docker.com/linux/${dockerDistro} $(lsb_release -cs) stable" \\
58
+ > /etc/apt/sources.list.d/docker.list
59
+ apt-get update -q
60
+ apt-get install -y -q \\
61
+ docker-ce \\
62
+ docker-ce-cli \\
63
+ containerd.io \\
64
+ docker-buildx-plugin \\
65
+ docker-compose-plugin
66
+ systemctl enable --now docker
67
+ fi
68
+
69
+ usermod -aG docker clawops
70
+
71
+ # \u2500\u2500 OpenClaw image \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
72
+ OPENCLAW_VERSION="${openclawVersion}"
73
+ docker pull ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION}
74
+
75
+ # \u2500\u2500 Gateway auth token \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
76
+ # OpenClaw refuses a non-loopback bind without auth, and in a container it always
77
+ # binds 0.0.0.0. Without a token the gateway exits 78 and restart-loops.
78
+ OPENCLAW_ENV_FILE=/home/clawops/openclaw.env
79
+ if [ ! -s "\${OPENCLAW_ENV_FILE}" ]; then
80
+ OPENCLAW_TOKEN=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \\n')
81
+ printf 'OPENCLAW_GATEWAY_TOKEN=%s\\n' "\${OPENCLAW_TOKEN}" > "\${OPENCLAW_ENV_FILE}"
82
+ chmod 600 "\${OPENCLAW_ENV_FILE}"
83
+ chown clawops:clawops "\${OPENCLAW_ENV_FILE}"
84
+ fi
85
+
86
+ # \u2500\u2500 State directory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
87
+ # One directory holds config, the SQLite state and any plugins installed below.
88
+ # Owned NUMERICALLY by the uid the container runs as: useradd clawops gets 1001 on
89
+ # Ubuntu 24.04 (the ubuntu user already holds 1000), and a 1001-owned state dir makes the
90
+ # gateway exit 1 with EACCES on its own SQLite WAL. There is no degraded mode.
91
+ OPENCLAW_STATE_DIR=${STATE_DIR_HOST_LINUX}
92
+ OPENCLAW_CONFIG="\${OPENCLAW_STATE_DIR}/${CONFIG_FILENAME}"
93
+ mkdir -p "\${OPENCLAW_STATE_DIR}"
94
+
95
+ # Migrate a pre-2.0 deployment: the config used to be a bare file in the service user's
96
+ # home. Without this an in-place upgrade comes up with no configuration at all. Guarded on
97
+ # the target being absent, so re-running provisioning never clobbers a later edit.
98
+ OPENCLAW_LEGACY_CONFIG=/home/clawops/openclaw.json
99
+ if [ -f "\${OPENCLAW_LEGACY_CONFIG}" ] && [ ! -f "\${OPENCLAW_CONFIG}" ]; then
100
+ cp -p "\${OPENCLAW_LEGACY_CONFIG}" "\${OPENCLAW_CONFIG}"
101
+ mv "\${OPENCLAW_LEGACY_CONFIG}" "\${OPENCLAW_LEGACY_CONFIG}.migrated"
102
+ echo "clawops: migrated config from \${OPENCLAW_LEGACY_CONFIG}"
103
+ OPENCLAW_MIGRATED=1
104
+ fi
105
+
106
+ # \u2500\u2500 Default config (apply.ts will overwrite with plan overlay) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
107
+ if [ ! -f "\${OPENCLAW_CONFIG}" ]; then
108
+ cat > "\${OPENCLAW_CONFIG}" <<'OPENCLAWJSON'
109
+ {"meta":{"lastTouchedVersion":"2026.9"},"gateway":{"mode":"local","port":${gatewayPort},"auth":{"mode":"token"}},"models":{},"channels":{}}
110
+ OPENCLAWJSON
111
+ fi
112
+
113
+ # Numeric, and last, so it covers the config and anything migrated above.
114
+ chown -R ${CONTAINER_UID}:${CONTAINER_UID} "\${OPENCLAW_STATE_DIR}"
115
+
116
+ # A 1.x config has no gateway.mode, and clawops no longer passes --allow-unconfigured \u2014
117
+ # so a migrated deployment would exit 78 with "existing config is missing gateway.mode".
118
+ # Normalised with OpenClaw's own tooling rather than by editing JSON in shell: it is
119
+ # schema-aware, writes atomically, and applies OpenClaw's internal config migrations at the
120
+ # same time. Runs after the chown so the container can write.
121
+ if [ "\${OPENCLAW_MIGRATED:-0}" = "1" ]; then
122
+ docker run --rm -v "\${OPENCLAW_STATE_DIR}":/home/node/.openclaw \\
123
+ ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION} \\
124
+ openclaw config set gateway.mode local || \\
125
+ echo "clawops: WARNING could not set gateway.mode; gateway may refuse to start" >&2
126
+ fi
127
+
128
+ # \u2500\u2500 Start OpenClaw container \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
129
+ # Built by src/openclaw/runtime.ts, so this cannot drift from the restart paths.
130
+ ${gatewayRunCommand({
131
+ image: "ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}",
132
+ stateDir: '"${OPENCLAW_STATE_DIR}"',
133
+ envFilePath: '"${OPENCLAW_ENV_FILE}"',
134
+ extraArgs: bedrockEnvBlock.trim(),
135
+ publish: publishGateway,
136
+ port: gatewayPort
137
+ })}
138
+ `;
139
+ }
140
+ function makeBedrockEnvBlock() {
141
+ return ' -e AWS_DEFAULT_REGION=$( \\\n IMDS_TOKEN=$(curl -sf -X PUT \\\n "http://169.254.169.254/latest/api/token" \\\n -H "X-aws-ec2-metadata-token-ttl-seconds: 60") && \\\n curl -sf \\\n -H "X-aws-ec2-metadata-token: ${IMDS_TOKEN}" \\\n "http://169.254.169.254/latest/meta-data/placement/region" \\\n || echo us-east-1) \\\n';
142
+ }
143
+
144
+ // src/providers/aws/program.ts
145
+ var SSH_PORT = 22;
146
+ var awsProgram = async () => {
147
+ const [pulumi, aws, { resolveIngressCidrs, resolveGatewayIngressCidrs, resolveGatewayPort, detectEgressIp }] = await Promise.all([
148
+ import("@pulumi/pulumi"),
149
+ import("@pulumi/aws"),
150
+ import("./firewall-5VKZ4WBH.js")
151
+ ]);
152
+ const cfg = new pulumi.Config();
153
+ const instanceType = cfg.get("instanceType") ?? "t3.small";
154
+ const region = cfg.get("region") ?? "us-east-1";
155
+ const openclawVersion = cfg.get("openclawVersion") ?? "latest";
156
+ const publishGateway = cfg.get("publishGateway") === "all" ? "all" : "loopback";
157
+ const gatewayPort = resolveGatewayPort(cfg.get("gatewayPort"), GATEWAY_PORT);
158
+ const accessMode = cfg.get("accessMode") ?? "restricted";
159
+ const allowedCidrs = cfg.get("allowedCidrs") ?? "";
160
+ const sshCidrs = cfg.get("sshCidrs") ?? "";
161
+ const gatewayCidrs = cfg.get("gatewayCidrs") ?? "";
162
+ const bedrockEnabled = cfg.get("bedrockEnabled") === "true";
163
+ const pinnedAmiId = cfg.get("amiId");
164
+ const sshPublicKey = cfg.get("sshPublicKey");
165
+ if (!sshPublicKey) {
166
+ throw new Error(
167
+ 'Stack config "sshPublicKey" is required for the AWS adapter. Set it with: pulumi config set --stack <name> sshPublicKey "ssh-ed25519 ..."'
168
+ );
169
+ }
170
+ const egressResult = accessMode === "auto" ? await detectEgressIp("https://ifconfig.me/ip") : { ok: true, ip: "" };
171
+ if (accessMode === "open") {
172
+ process.stderr.write(
173
+ "[clawops] WARNING: accessMode=open allows 0.0.0.0/0 on SSH and gateway ports. Only use this for development/sandbox stacks.\n"
174
+ );
175
+ }
176
+ const sshIngressCidrs = resolveIngressCidrs(accessMode, allowedCidrs, sshCidrs, egressResult);
177
+ const gatewayIngressCidrs = resolveGatewayIngressCidrs(
178
+ publishGateway,
179
+ accessMode,
180
+ allowedCidrs,
181
+ gatewayCidrs,
182
+ egressResult
183
+ );
184
+ const vpc = new aws.ec2.Vpc("clawops-vpc", {
185
+ cidrBlock: "10.0.0.0/16",
186
+ enableDnsHostnames: true,
187
+ enableDnsSupport: true,
188
+ tags: { Name: "clawops" }
189
+ });
190
+ const igw = new aws.ec2.InternetGateway("clawops-igw", {
191
+ tags: { Name: "clawops" }
192
+ });
193
+ new aws.ec2.InternetGatewayAttachment("clawops-igw-attach", {
194
+ vpcId: vpc.id,
195
+ internetGatewayId: igw.id
196
+ });
197
+ const subnet = new aws.ec2.Subnet("clawops-subnet", {
198
+ vpcId: vpc.id,
199
+ cidrBlock: "10.0.1.0/24",
200
+ mapPublicIpOnLaunch: false,
201
+ availabilityZone: pulumi.interpolate`${region}a`,
202
+ tags: { Name: "clawops" }
203
+ });
204
+ const routeTable = new aws.ec2.RouteTable("clawops-rt", {
205
+ vpcId: vpc.id,
206
+ tags: { Name: "clawops" }
207
+ });
208
+ new aws.ec2.Route("clawops-route", {
209
+ routeTableId: routeTable.id,
210
+ destinationCidrBlock: "0.0.0.0/0",
211
+ gatewayId: igw.id
212
+ });
213
+ new aws.ec2.RouteTableAssociation("clawops-rta", {
214
+ subnetId: subnet.id,
215
+ routeTableId: routeTable.id
216
+ });
217
+ const sg = new aws.ec2.SecurityGroup("clawops-sg", {
218
+ vpcId: vpc.id,
219
+ description: "clawops managed security group",
220
+ tags: { Name: "clawops" }
221
+ });
222
+ sshIngressCidrs.forEach((cidr, i) => {
223
+ new aws.vpc.SecurityGroupIngressRule(`clawops-sg-ssh-${i}`, {
224
+ securityGroupId: sg.id,
225
+ ipProtocol: "tcp",
226
+ fromPort: SSH_PORT,
227
+ toPort: SSH_PORT,
228
+ cidrIpv4: cidr,
229
+ tags: { Name: `clawops-ssh-${i}` }
230
+ });
231
+ });
232
+ gatewayIngressCidrs.forEach((cidr, i) => {
233
+ new aws.vpc.SecurityGroupIngressRule(`clawops-sg-gw-${i}`, {
234
+ securityGroupId: sg.id,
235
+ ipProtocol: "tcp",
236
+ fromPort: gatewayPort,
237
+ toPort: gatewayPort,
238
+ cidrIpv4: cidr,
239
+ tags: { Name: `clawops-gateway-${i}` }
240
+ });
241
+ });
242
+ new aws.vpc.SecurityGroupEgressRule("clawops-sg-egress", {
243
+ securityGroupId: sg.id,
244
+ ipProtocol: "-1",
245
+ cidrIpv4: "0.0.0.0/0",
246
+ tags: { Name: "clawops-egress" }
247
+ });
248
+ const role = new aws.iam.Role("clawops-role", {
249
+ assumeRolePolicy: JSON.stringify({
250
+ Version: "2012-10-17",
251
+ Statement: [{
252
+ Effect: "Allow",
253
+ Principal: { Service: "ec2.amazonaws.com" },
254
+ Action: "sts:AssumeRole"
255
+ }]
256
+ }),
257
+ tags: { Name: "clawops" }
258
+ });
259
+ new aws.iam.RolePolicyAttachment("clawops-ssm", {
260
+ role: role.name,
261
+ policyArn: "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
262
+ });
263
+ if (bedrockEnabled) {
264
+ new aws.iam.RolePolicy("clawops-bedrock-invoke", {
265
+ role: role.name,
266
+ policy: JSON.stringify({
267
+ Version: "2012-10-17",
268
+ Statement: [{
269
+ Effect: "Allow",
270
+ Action: [
271
+ "bedrock:InvokeModel",
272
+ "bedrock:InvokeModelWithResponseStream"
273
+ ],
274
+ Resource: "*"
275
+ }]
276
+ })
277
+ });
278
+ }
279
+ const instanceProfile = new aws.iam.InstanceProfile("clawops-profile", {
280
+ role: role.name
281
+ });
282
+ const keyPair = new aws.ec2.KeyPair("clawops-keypair", {
283
+ publicKey: sshPublicKey,
284
+ tags: { Name: "clawops" }
285
+ });
286
+ const amiId = pinnedAmiId ?? (await aws.ec2.getAmi({
287
+ mostRecent: true,
288
+ owners: ["099720109477"],
289
+ // Canonical
290
+ filters: [
291
+ { name: "name", values: ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] },
292
+ { name: "virtualization-type", values: ["hvm"] }
293
+ ]
294
+ })).id;
295
+ process.stderr.write(`[clawops] Using AMI: ${amiId}
296
+ `);
297
+ const instance = new aws.ec2.Instance("clawops-instance", {
298
+ ami: amiId,
299
+ instanceType,
300
+ subnetId: subnet.id,
301
+ vpcSecurityGroupIds: [sg.id],
302
+ iamInstanceProfile: instanceProfile.name,
303
+ keyName: keyPair.keyName,
304
+ userData: makeStartupScript({ openclawVersion, os: "ubuntu", bedrockEnabled, publishGateway, gatewayPort }),
305
+ // IMDSv2 with hopLimit=2 so Docker containers on this host can reach IMDS
306
+ // and obtain the instance role credentials (required for Bedrock access).
307
+ metadataOptions: {
308
+ httpTokens: "required",
309
+ httpPutResponseHopLimit: 2
310
+ },
311
+ tags: { Name: "clawops" }
312
+ });
313
+ const eip = new aws.ec2.Eip("clawops-eip", {
314
+ domain: "vpc",
315
+ tags: { Name: "clawops" }
316
+ });
317
+ new aws.ec2.EipAssociation("clawops-eip-assoc", {
318
+ instanceId: instance.id,
319
+ allocationId: eip.id
320
+ });
321
+ return {
322
+ instanceId: instance.id,
323
+ publicIp: eip.publicIp,
324
+ gatewayUrl: pulumi.interpolate`https://${eip.publicIp}:${gatewayPort}`,
325
+ sshHost: eip.publicIp,
326
+ sshPort: SSH_PORT,
327
+ sshUser: "ubuntu",
328
+ region,
329
+ provisionedAt: (/* @__PURE__ */ new Date()).toISOString()
330
+ };
331
+ };
332
+
333
+ // src/providers/aws/index.ts
334
+ var awsAdapter = {
335
+ name: "aws",
336
+ get program() {
337
+ return awsProgram;
338
+ },
339
+ getConnectionInfo(outputs) {
340
+ return {
341
+ host: String(outputs["sshHost"] ?? ""),
342
+ port: Number(outputs["sshPort"] ?? 22),
343
+ user: String(outputs["sshUser"] ?? "ubuntu"),
344
+ privateKeyPath: String(outputs["privateKeyPath"] ?? ""),
345
+ knownHostsPath: String(outputs["knownHostsPath"] ?? "")
346
+ };
347
+ },
348
+ normalizeInstanceType(alias) {
349
+ const mapped = INSTANCE_TYPE_MAP[alias];
350
+ if (!mapped) throw new Error(`Unknown instance alias: ${alias}`);
351
+ return mapped;
352
+ },
353
+ defaultRegion() {
354
+ return "us-east-1";
355
+ },
356
+ stateBackendUrl(bucket) {
357
+ return `s3://${bucket}`;
358
+ },
359
+ async preflight(opts) {
360
+ const { awsPreflight } = await import("./preflight-TFD4N5ZU.js");
361
+ return awsPreflight(opts);
362
+ },
363
+ async validateConfig() {
364
+ const errors = [];
365
+ const hasProfile = Boolean(process2.env["AWS_PROFILE"]);
366
+ const hasKeyId = Boolean(process2.env["AWS_ACCESS_KEY_ID"]);
367
+ const hasOidc = Boolean(process2.env["AWS_ROLE_ARN"] && process2.env["AWS_WEB_IDENTITY_TOKEN_FILE"]);
368
+ if (!hasProfile && !hasKeyId && !hasOidc) {
369
+ const onAws = await checkImds();
370
+ if (!onAws) {
371
+ errors.push(
372
+ "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."
373
+ );
374
+ }
375
+ }
376
+ return { ok: errors.length === 0, errors };
377
+ }
378
+ };
379
+ async function checkImds() {
380
+ try {
381
+ const res = await fetch(
382
+ "http://169.254.169.254/latest/meta-data/instance-id",
383
+ { signal: AbortSignal.timeout(1e3) }
384
+ );
385
+ return res.ok;
386
+ } catch {
387
+ return false;
388
+ }
389
+ }
390
+ registerProvider(awsAdapter);
391
+
392
+ // src/providers/gcp/index.ts
393
+ import process3 from "process";
394
+
395
+ // src/providers/gcp/program.ts
396
+ var SSH_PORT2 = 22;
397
+ var gcpProgram = async () => {
398
+ const [pulumi, gcp, { resolveIngressCidrs, resolveGatewayIngressCidrs, resolveGatewayPort, detectEgressIp }] = await Promise.all([
399
+ import("@pulumi/pulumi"),
400
+ import("@pulumi/gcp"),
401
+ import("./firewall-5VKZ4WBH.js")
402
+ ]);
403
+ const cfg = new pulumi.Config();
404
+ const instanceType = cfg.get("instanceType") ?? "e2-standard-2";
405
+ const region = cfg.get("region") ?? "us-central1";
406
+ const openclawVersion = cfg.get("openclawVersion") ?? "latest";
407
+ const publishGateway = cfg.get("publishGateway") === "all" ? "all" : "loopback";
408
+ const gatewayPort = resolveGatewayPort(cfg.get("gatewayPort"), GATEWAY_PORT);
409
+ const zone = cfg.get("zone") ?? `${region}-a`;
410
+ const accessMode = cfg.get("accessMode") ?? "restricted";
411
+ const allowedCidrs = cfg.get("allowedCidrs") ?? "";
412
+ const sshCidrs = cfg.get("sshCidrs") ?? "";
413
+ const gatewayCidrs = cfg.get("gatewayCidrs") ?? "";
414
+ const sshPublicKey = cfg.get("sshPublicKey");
415
+ if (!sshPublicKey) {
416
+ throw new Error(
417
+ 'Stack config "sshPublicKey" is required for the GCP adapter. Set it with: pulumi config set --stack <name> sshPublicKey "ssh-ed25519 ..."'
418
+ );
419
+ }
420
+ const egressResult = accessMode === "auto" ? await detectEgressIp("https://ifconfig.me/ip") : { ok: true, ip: "" };
421
+ if (accessMode === "open") {
422
+ process.stderr.write(
423
+ "[clawops] WARNING: accessMode=open allows 0.0.0.0/0 on SSH and gateway ports. Only use this for development/sandbox stacks.\n"
424
+ );
425
+ }
426
+ const sshIngressCidrs = resolveIngressCidrs(accessMode, allowedCidrs, sshCidrs, egressResult);
427
+ const gatewayIngressCidrs = resolveGatewayIngressCidrs(
428
+ publishGateway,
429
+ accessMode,
430
+ allowedCidrs,
431
+ gatewayCidrs,
432
+ egressResult
433
+ );
434
+ const network = new gcp.compute.Network("clawops-network", {
435
+ autoCreateSubnetworks: false,
436
+ description: "clawops managed network"
437
+ });
438
+ const subnet = new gcp.compute.Subnetwork("clawops-subnet", {
439
+ ipCidrRange: "10.0.0.0/24",
440
+ region,
441
+ network: network.selfLink
442
+ });
443
+ if (sshIngressCidrs.length > 0) {
444
+ new gcp.compute.Firewall("clawops-firewall-ssh", {
445
+ network: network.selfLink,
446
+ allows: [{ protocol: "tcp", ports: [String(SSH_PORT2)] }],
447
+ sourceRanges: sshIngressCidrs,
448
+ targetTags: ["clawops"]
449
+ });
450
+ }
451
+ if (gatewayIngressCidrs.length > 0) {
452
+ new gcp.compute.Firewall("clawops-firewall-gateway", {
453
+ network: network.selfLink,
454
+ allows: [{ protocol: "tcp", ports: [String(gatewayPort)] }],
455
+ sourceRanges: gatewayIngressCidrs,
456
+ targetTags: ["clawops"]
457
+ });
458
+ }
459
+ const address = new gcp.compute.Address("clawops-address", { region });
460
+ const instance = new gcp.compute.Instance("clawops-instance", {
461
+ machineType: instanceType,
462
+ zone,
463
+ tags: ["clawops"],
464
+ bootDisk: {
465
+ initializeParams: {
466
+ image: "debian-cloud/debian-12",
467
+ size: 20
468
+ }
469
+ },
470
+ networkInterfaces: [
471
+ {
472
+ network: network.selfLink,
473
+ subnetwork: subnet.selfLink,
474
+ accessConfigs: [
475
+ {
476
+ natIp: address.address,
477
+ networkTier: "PREMIUM"
478
+ }
479
+ ]
480
+ }
481
+ ],
482
+ metadata: {
483
+ // GCP guest agent reads 'ssh-keys' and populates /home/<user>/.ssh/authorized_keys
484
+ "ssh-keys": `clawops:${sshPublicKey}`,
485
+ "startup-script": makeStartupScript({ openclawVersion, os: "debian", publishGateway, gatewayPort })
486
+ },
487
+ serviceAccount: {
488
+ scopes: ["https://www.googleapis.com/auth/cloud-platform"]
489
+ }
490
+ });
491
+ return {
492
+ instanceId: instance.id,
493
+ publicIp: address.address,
494
+ gatewayUrl: pulumi.interpolate`https://${address.address}:${gatewayPort}`,
495
+ sshHost: address.address,
496
+ sshPort: SSH_PORT2,
497
+ sshUser: "clawops",
498
+ region,
499
+ provisionedAt: (/* @__PURE__ */ new Date()).toISOString()
500
+ };
501
+ };
502
+
503
+ // src/providers/gcp/index.ts
504
+ var INSTANCE_TYPE_MAP3 = {
505
+ micro: "e2-micro",
506
+ small: "e2-standard-2",
507
+ medium: "e2-standard-4",
508
+ large: "e2-standard-8",
509
+ gpu: "n1-standard-4"
510
+ // TODO M3: add accelerator config for GPU instances
511
+ };
512
+ var gcpAdapter = {
513
+ name: "gcp",
514
+ get program() {
515
+ return gcpProgram;
516
+ },
517
+ getConnectionInfo(outputs) {
518
+ return {
519
+ host: String(outputs["sshHost"] ?? ""),
520
+ port: Number(outputs["sshPort"] ?? 22),
521
+ user: String(outputs["sshUser"] ?? "clawops"),
522
+ privateKeyPath: String(outputs["privateKeyPath"] ?? ""),
523
+ knownHostsPath: String(outputs["knownHostsPath"] ?? "")
524
+ };
525
+ },
526
+ normalizeInstanceType(alias) {
527
+ if (alias === "gpu") {
528
+ throw new Error(
529
+ "GPU instances are not yet supported on GCP. Use --provider aws (g4dn.xlarge) or --provider azure (Standard_NC6s_v3) for GPU workloads."
530
+ );
531
+ }
532
+ const mapped = INSTANCE_TYPE_MAP3[alias];
533
+ if (!mapped) throw new Error(`Unknown instance alias: ${alias}`);
534
+ return mapped;
535
+ },
536
+ defaultRegion() {
537
+ return "us-central1";
538
+ },
539
+ stateBackendUrl(bucket) {
540
+ return `gs://${bucket}`;
541
+ },
542
+ async validateConfig() {
543
+ const errors = [];
544
+ const hasKeyFile = Boolean(process3.env["GOOGLE_APPLICATION_CREDENTIALS"]);
545
+ const hasToken = Boolean(process3.env["GOOGLE_OAUTH_ACCESS_TOKEN"]);
546
+ const hasAdcFile = await checkAdcFile();
547
+ if (!hasKeyFile && !hasToken && !hasAdcFile) {
548
+ const onGcp = await checkInstanceMetadata();
549
+ if (!onGcp) {
550
+ errors.push(
551
+ "No GCP credentials found. Set GOOGLE_APPLICATION_CREDENTIALS to a service account key file, or run `gcloud auth application-default login`."
552
+ );
553
+ }
554
+ }
555
+ return { ok: errors.length === 0, errors };
556
+ },
557
+ async preflight(opts) {
558
+ const { gcpPreflight } = await import("./preflight-EF52S3WW.js");
559
+ return gcpPreflight(opts);
560
+ }
561
+ };
562
+ async function checkAdcFile() {
563
+ try {
564
+ const { accessSync: accessSync2 } = await import("fs");
565
+ const { join } = await import("path");
566
+ const home = process3.env["HOME"] ?? process3.env["USERPROFILE"] ?? "";
567
+ accessSync2(join(home, ".config", "gcloud", "application_default_credentials.json"));
568
+ return true;
569
+ } catch {
570
+ return false;
571
+ }
572
+ }
573
+ async function checkInstanceMetadata() {
574
+ try {
575
+ const res = await fetch(
576
+ "http://metadata.google.internal/computeMetadata/v1/instance/id",
577
+ {
578
+ headers: { "Metadata-Flavor": "Google" },
579
+ signal: AbortSignal.timeout(1e3)
580
+ }
581
+ );
582
+ return res.ok;
583
+ } catch {
584
+ return false;
585
+ }
586
+ }
587
+ registerProvider(gcpAdapter);
588
+
589
+ // src/providers/azure/index.ts
590
+ import process4 from "process";
591
+
592
+ // src/providers/azure/program.ts
593
+ import { createHash } from "crypto";
594
+ var SSH_PORT3 = 22;
595
+ var azureProgram = async () => {
596
+ const [pulumi, azure, random, { resolveIngressCidrs, resolveGatewayIngressCidrs, resolveGatewayPort, detectEgressIp }] = await Promise.all([
597
+ import("@pulumi/pulumi"),
598
+ import("@pulumi/azure-native"),
599
+ import("@pulumi/random"),
600
+ import("./firewall-5VKZ4WBH.js")
601
+ ]);
602
+ const cfg = new pulumi.Config();
603
+ const stackName = pulumi.getStack();
604
+ const instanceType = cfg.get("instanceType") ?? "Standard_B2s";
605
+ const location = cfg.get("region") ?? "eastus";
606
+ const openclawVersion = cfg.get("openclawVersion") ?? "latest";
607
+ const publishGateway = cfg.get("publishGateway") === "all" ? "all" : "loopback";
608
+ const gatewayPort = resolveGatewayPort(cfg.get("gatewayPort"), GATEWAY_PORT);
609
+ const accessMode = cfg.get("accessMode") ?? "restricted";
610
+ const allowedCidrs = cfg.get("allowedCidrs") ?? "";
611
+ const sshCidrs = cfg.get("sshCidrs") ?? "";
612
+ const gatewayCidrs = cfg.get("gatewayCidrs") ?? "";
613
+ const keyVaultEnabled = cfg.get("keyVaultEnabled") === "true";
614
+ const resourceGroupName = cfg.get("resourceGroupName") ?? `clawops-${stackName}`;
615
+ const sshPublicKey = cfg.get("sshPublicKey");
616
+ if (!sshPublicKey) {
617
+ throw new Error(
618
+ 'Stack config "sshPublicKey" is required for the Azure adapter. Set it with: pulumi config set --stack <name> sshPublicKey "ssh-ed25519 ..."'
619
+ );
620
+ }
621
+ const clientConfig = await azure.authorization.getClientConfig({});
622
+ const egressResult = accessMode === "auto" ? await detectEgressIp("https://ifconfig.me/ip") : { ok: true, ip: "" };
623
+ if (accessMode === "open") {
624
+ process.stderr.write(
625
+ "[clawops] WARNING: accessMode=open allows 0.0.0.0/0 on SSH and gateway ports. Only use this for development/sandbox stacks.\n"
626
+ );
627
+ }
628
+ const sshIngressCidrs = resolveIngressCidrs(accessMode, allowedCidrs, sshCidrs, egressResult);
629
+ const gatewayIngressCidrs = resolveGatewayIngressCidrs(
630
+ publishGateway,
631
+ accessMode,
632
+ allowedCidrs,
633
+ gatewayCidrs,
634
+ egressResult
635
+ );
636
+ const rg = new azure.resources.ResourceGroup("clawops-rg", {
637
+ resourceGroupName,
638
+ location
639
+ });
640
+ const vnet = new azure.network.VirtualNetwork("clawops-vnet", {
641
+ resourceGroupName: rg.name,
642
+ location: rg.location,
643
+ addressSpace: { addressPrefixes: ["10.0.0.0/16"] }
644
+ });
645
+ const subnet = new azure.network.Subnet("clawops-subnet", {
646
+ resourceGroupName: rg.name,
647
+ virtualNetworkName: vnet.name,
648
+ addressPrefix: "10.0.1.0/24"
649
+ });
650
+ const securityRules = [];
651
+ let priority = 100;
652
+ for (const cidr of sshIngressCidrs) {
653
+ securityRules.push({
654
+ name: `allow-ssh-${priority}`,
655
+ priority: priority++,
656
+ direction: "Inbound",
657
+ access: "Allow",
658
+ protocol: "Tcp",
659
+ sourceAddressPrefix: cidr,
660
+ sourcePortRange: "*",
661
+ destinationAddressPrefix: "*",
662
+ destinationPortRange: String(SSH_PORT3)
663
+ });
664
+ }
665
+ for (const cidr of gatewayIngressCidrs) {
666
+ securityRules.push({
667
+ name: `allow-gateway-${priority}`,
668
+ priority: priority++,
669
+ direction: "Inbound",
670
+ access: "Allow",
671
+ protocol: "Tcp",
672
+ sourceAddressPrefix: cidr,
673
+ sourcePortRange: "*",
674
+ destinationAddressPrefix: "*",
675
+ destinationPortRange: String(gatewayPort)
676
+ });
677
+ }
678
+ const nsg = new azure.network.NetworkSecurityGroup("clawops-nsg", {
679
+ resourceGroupName: rg.name,
680
+ location: rg.location,
681
+ securityRules
682
+ });
683
+ const publicIp = new azure.network.PublicIPAddress("clawops-pip", {
684
+ resourceGroupName: rg.name,
685
+ location: rg.location,
686
+ publicIPAllocationMethod: "Static",
687
+ sku: { name: "Standard" }
688
+ });
689
+ const nic = new azure.network.NetworkInterface("clawops-nic", {
690
+ resourceGroupName: rg.name,
691
+ location: rg.location,
692
+ networkSecurityGroup: { id: nsg.id },
693
+ ipConfigurations: [{
694
+ name: "clawops-ipconfig",
695
+ subnet: { id: subnet.id },
696
+ publicIPAddress: { id: publicIp.id },
697
+ privateIPAllocationMethod: "Dynamic"
698
+ }]
699
+ });
700
+ const vm = new azure.compute.VirtualMachine("clawops-vm", {
701
+ resourceGroupName: rg.name,
702
+ location: rg.location,
703
+ hardwareProfile: { vmSize: instanceType },
704
+ identity: { type: "SystemAssigned" },
705
+ osProfile: {
706
+ adminUsername: "clawops",
707
+ computerName: "clawops",
708
+ customData: Buffer.from(makeStartupScript({ openclawVersion, os: "ubuntu", publishGateway, gatewayPort })).toString("base64"),
709
+ linuxConfiguration: {
710
+ disablePasswordAuthentication: true,
711
+ ssh: {
712
+ publicKeys: [{
713
+ keyData: sshPublicKey,
714
+ path: "/home/clawops/.ssh/authorized_keys"
715
+ }]
716
+ }
717
+ }
718
+ },
719
+ storageProfile: {
720
+ imageReference: {
721
+ publisher: "Canonical",
722
+ // Ubuntu 22.04 LTS — Canonical migrated from the legacy 'UbuntuServer'
723
+ // offer to this naming scheme. 22_04-lts-gen2 is available in all regions.
724
+ offer: "0001-com-ubuntu-server-jammy",
725
+ sku: "22_04-lts-gen2",
726
+ version: "latest"
727
+ },
728
+ osDisk: {
729
+ createOption: "FromImage",
730
+ managedDisk: { storageAccountType: "Premium_LRS" },
731
+ diskSizeGB: 30
732
+ }
733
+ },
734
+ networkProfile: {
735
+ networkInterfaces: [{ id: nic.id, primary: true }]
736
+ }
737
+ });
738
+ if (keyVaultEnabled) {
739
+ const safeName = stackName.replace(/[^a-zA-Z0-9-]/g, "-");
740
+ const rawKvName = `clawops-${safeName}-kv`;
741
+ const kvName = rawKvName.length > 24 ? `cl-${createHash("sha256").update(stackName).digest("hex").slice(0, 20)}` : rawKvName;
742
+ const kv = new azure.keyvault.Vault("clawops-kv", {
743
+ resourceGroupName: rg.name,
744
+ location: rg.location,
745
+ vaultName: kvName,
746
+ properties: {
747
+ sku: { family: "A", name: "standard" },
748
+ tenantId: pulumi.output(vm.identity).apply((i) => i?.tenantId ?? ""),
749
+ enableRbacAuthorization: true
750
+ }
751
+ });
752
+ const kvSecretsUserRoleId = `/subscriptions/${clientConfig.subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/4633458b-17de-408a-b874-0445c86b69e6`;
753
+ new azure.authorization.RoleAssignment("clawops-kv-role", {
754
+ scope: kv.id,
755
+ roleDefinitionId: kvSecretsUserRoleId,
756
+ principalId: pulumi.output(vm.identity).apply((i) => i?.principalId ?? ""),
757
+ principalType: "ServicePrincipal"
758
+ });
759
+ const gatewayToken = new random.RandomPassword("clawops-kv-gateway-token", {
760
+ length: 32,
761
+ special: false
762
+ });
763
+ new azure.keyvault.Secret("clawops-gateway-token", {
764
+ resourceGroupName: rg.name,
765
+ vaultName: kv.name,
766
+ secretName: "gateway-token",
767
+ properties: { value: gatewayToken.result }
768
+ });
769
+ }
770
+ const resolvedIp = pulumi.output(publicIp.ipAddress).apply((ip) => ip ?? "");
771
+ return {
772
+ instanceId: vm.id,
773
+ publicIp: resolvedIp,
774
+ gatewayUrl: pulumi.interpolate`https://${resolvedIp}:${gatewayPort}`,
775
+ sshHost: resolvedIp,
776
+ sshPort: SSH_PORT3,
777
+ sshUser: "clawops",
778
+ region: location,
779
+ provisionedAt: (/* @__PURE__ */ new Date()).toISOString()
780
+ };
781
+ };
782
+
783
+ // src/providers/azure/index.ts
784
+ var azureAdapter = {
785
+ name: "azure",
786
+ get program() {
787
+ return azureProgram;
788
+ },
789
+ getConnectionInfo(outputs) {
790
+ return {
791
+ host: String(outputs["sshHost"] ?? ""),
792
+ port: Number(outputs["sshPort"] ?? 22),
793
+ user: String(outputs["sshUser"] ?? "clawops"),
794
+ privateKeyPath: String(outputs["privateKeyPath"] ?? ""),
795
+ knownHostsPath: String(outputs["knownHostsPath"] ?? "")
796
+ };
797
+ },
798
+ normalizeInstanceType(alias) {
799
+ const mapped = INSTANCE_TYPE_MAP2[alias];
800
+ if (!mapped) throw new Error(`Unknown instance alias: ${alias}`);
801
+ return mapped;
802
+ },
803
+ defaultRegion() {
804
+ return "eastus";
805
+ },
806
+ stateBackendUrl(bucket) {
807
+ return `azblob://${bucket}`;
808
+ },
809
+ async preflight(opts) {
810
+ const { azurePreflight } = await import("./preflight-MWWM5JT3.js");
811
+ return azurePreflight(opts);
812
+ },
813
+ async validateConfig() {
814
+ const errors = [];
815
+ const clientId = process4.env["AZURE_CLIENT_ID"];
816
+ const tenantId = process4.env["AZURE_TENANT_ID"];
817
+ const clientSecret = process4.env["AZURE_CLIENT_SECRET"];
818
+ const federatedToken = process4.env["AZURE_FEDERATED_TOKEN_FILE"];
819
+ const hasServicePrincipal = Boolean(clientId && tenantId && clientSecret);
820
+ const hasOidc = Boolean(clientId && tenantId && federatedToken);
821
+ const hasCliLogin = Boolean(azureCliAccount());
822
+ if (!hasServicePrincipal && !hasOidc && !hasCliLogin) {
823
+ const onAzure = await checkImds2();
824
+ if (!onAzure) {
825
+ errors.push(
826
+ "No Azure credentials found. Run `az login`, or set AZURE_CLIENT_ID + AZURE_TENANT_ID + AZURE_CLIENT_SECRET (service principal), or AZURE_CLIENT_ID + AZURE_TENANT_ID + AZURE_FEDERATED_TOKEN_FILE (OIDC), or run on an Azure VM with a managed identity."
827
+ );
828
+ }
829
+ }
830
+ return { ok: errors.length === 0, errors };
831
+ }
832
+ };
833
+ async function checkImds2() {
834
+ try {
835
+ const res = await fetch(
836
+ "http://169.254.169.254/metadata/instance?api-version=2021-02-01",
837
+ {
838
+ headers: { Metadata: "true" },
839
+ signal: AbortSignal.timeout(1e3)
840
+ }
841
+ );
842
+ return res.ok;
843
+ } catch {
844
+ return false;
845
+ }
846
+ }
847
+ registerProvider(azureAdapter);
848
+
849
+ // src/providers/local/index.ts
850
+ import { accessSync } from "fs";
851
+ var localAdapter = {
852
+ name: "local",
853
+ /** Never called — local stacks bypass Pulumi entirely. */
854
+ get program() {
855
+ return async () => ({});
856
+ },
857
+ getConnectionInfo(outputs) {
858
+ return {
859
+ host: String(outputs["sshHost"] ?? ""),
860
+ port: Number(outputs["sshPort"] ?? 22),
861
+ user: String(outputs["sshUser"] ?? "root"),
862
+ privateKeyPath: String(outputs["privateKeyPath"] ?? ""),
863
+ knownHostsPath: String(outputs["knownHostsPath"] ?? "")
864
+ };
865
+ },
866
+ normalizeInstanceType(_alias) {
867
+ return "local";
868
+ },
869
+ defaultRegion() {
870
+ return "local";
871
+ },
872
+ stateBackendUrl(_bucket) {
873
+ return "file://~/.clawops/state";
874
+ },
875
+ async validateConfig() {
876
+ const keyPath = process.env["CLAWOPS_SSH_KEY_PATH"];
877
+ if (keyPath) {
878
+ try {
879
+ accessSync(keyPath);
880
+ } catch {
881
+ return {
882
+ ok: false,
883
+ errors: [`SSH key not readable: ${keyPath}`]
884
+ };
885
+ }
886
+ }
887
+ return { ok: true, errors: [] };
888
+ }
889
+ };
890
+ registerProvider(localAdapter);