@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.
- package/README.md +98 -12
- package/dist/apply-KYFPWI5U.js +27 -0
- package/dist/automation-RMAIBNVC.js +44 -0
- package/dist/bedrock-XVASJX2K.js +61 -0
- package/dist/{bootstrap-DMRPWJGN.js → bootstrap-HNGKY6WW.js} +11 -8
- package/dist/{chunk-N44K3LHE.js → chunk-2I7KCMXZ.js} +6 -4
- package/dist/{chunk-3WYA2QDM.js → chunk-2Z7J3ERQ.js} +1 -1
- package/dist/chunk-3OLUJPCB.js +36 -0
- package/dist/chunk-5XT5IAJK.js +69 -0
- package/dist/chunk-AEEMRFZV.js +24 -0
- package/dist/{chunk-YR3O2I3S.js → chunk-BSLIYB5Q.js} +6 -1
- package/dist/{chunk-AHAAEXWC.js → chunk-E4NJSJ54.js} +16 -93
- package/dist/chunk-FEK7VJAY.js +16 -0
- package/dist/{chunk-6TIYVHPX.js → chunk-GIEVAGEK.js} +7 -33
- package/dist/chunk-HSPHXG6W.js +67 -0
- package/dist/{chunk-GYV4KNW7.js → chunk-IVWP4QUJ.js} +133 -40
- package/dist/{chunk-AX74VXTV.js → chunk-KC3GC3PE.js} +58 -49
- package/dist/{chunk-UFRO4H5W.js → chunk-LBE6COC4.js} +56 -40
- package/dist/chunk-LEVIW2PV.js +890 -0
- package/dist/chunk-MC5ATCXS.js +108 -0
- package/dist/chunk-N4WZLWJ4.js +16 -0
- package/dist/{chunk-SBLQH2TJ.js → chunk-NUL5F6YV.js} +1 -1
- package/dist/chunk-RHOWYIWT.js +52 -0
- package/dist/{chunk-WHJMKDV7.js → chunk-STLRHLLV.js} +65 -12
- package/dist/chunk-V25QRZRB.js +45 -0
- package/dist/chunk-VOEJYRIK.js +33 -0
- package/dist/{providers-2OABPW2E.js → chunk-VUFMZEIM.js} +3 -2
- package/dist/chunk-W3OLNNJW.js +51 -0
- package/dist/cli-WLD3EBEM.js +11 -0
- package/dist/cli-auth-GD5SK7JU.js +13 -0
- package/dist/cli.js +518 -206
- package/dist/context-7EVC6W45.js +20 -0
- package/dist/docker-3PDO4TS7.js +12 -0
- package/dist/{firewall-T3NTPZJD.js → firewall-5VKZ4WBH.js} +22 -1
- package/dist/generate-I6YQ5V2T.js +29 -0
- package/dist/{harden-PCDK3UQ5.js → harden-72JMRT4E.js} +8 -5
- package/dist/index.d.ts +56 -0
- package/dist/known-hosts-file-I3Q3NBYQ.js +22 -0
- package/dist/{logs-P7YCKVF4.js → logs-5A7YYA53.js} +1 -1
- package/dist/{mcp-wire-BFEY2Y6H.js → mcp-wire-3SWBM7D6.js} +25 -15
- package/dist/models-AGTQWYDL.js +32 -0
- package/dist/network-args-PYJRJBYO.js +64 -0
- package/dist/{package-3HUW57XH.js → package-5BCMEBTE.js} +10 -3
- package/dist/passphrase-MJBE45N7.js +15 -0
- package/dist/{pool-EX4ZTA5P.js → pool-PIIKJ4L2.js} +3 -2
- package/dist/preflight-EF52S3WW.js +162 -0
- package/dist/preflight-MWWM5JT3.js +181 -0
- package/dist/preflight-TFD4N5ZU.js +141 -0
- package/dist/providers-FW6H6OPF.js +11 -0
- package/dist/ready-KDNRBNSU.js +95 -0
- package/dist/{remote-config-Z6IIZR3D.js → remote-config-3HZLUWEA.js} +3 -2
- package/dist/{runtime-LZ7NA53V.js → runtime-X3KW5TCP.js} +4 -2
- package/dist/server-GBA7BJGK.js +46 -0
- package/dist/ssh-VAI5DU5M.js +11 -0
- package/dist/ssh-key-DUO6OKOI.js +7 -0
- package/dist/stack-config-R4SPSUQZ.js +12 -0
- package/dist/{validate-BOQRWX4F.js → validate-YVI3BJ55.js} +1 -1
- package/dist/wait-WO72TRRQ.js +79 -0
- package/package.json +10 -3
- package/spec/mcp-tools.yaml +21 -2
- package/spec/models.yaml +14 -3
- package/spec/providers.schema.json +163 -28
- package/dist/apply-AM5HERE7.js +0 -19
- package/dist/automation-RULUSJBW.js +0 -30
- package/dist/aws-AMON7DA7.js +0 -265
- package/dist/azure-T4G6OBJQ.js +0 -273
- package/dist/chunk-CKSPROD5.js +0 -129
- package/dist/chunk-LU2V3LXA.js +0 -114
- package/dist/context-KHKWJJMC.js +0 -10
- package/dist/gcp-2JBTD7ZM.js +0 -204
- package/dist/generate-2XV2GSP2.js +0 -15
- package/dist/local-DXBEVZ5C.js +0 -47
- package/dist/server-A5AB67GQ.js +0 -36
- package/dist/ssh-B3NZAU4I.js +0 -8
- 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);
|