@vellumai/cli 0.11.8 → 0.11.9-staging.2
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 +6 -7
- package/knip.json +0 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/guardian-token-refresh.test.ts +2 -8
- package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +2 -8
- package/node_modules/@vellumai/local-mode/src/__tests__/helpers/child-process-mock.ts +55 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/retire.test.ts +2 -8
- package/node_modules/@vellumai/local-mode/src/__tests__/upgrade.test.ts +2 -8
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +2 -8
- package/node_modules/@vellumai/local-mode/src/devices.test.ts +5 -8
- package/node_modules/@vellumai/local-mode/src/index.ts +1 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +6 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +165 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +63 -6
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-api-key-resolution.test.ts +102 -0
- package/src/__tests__/constants.test.ts +6 -2
- package/src/commands/hatch.ts +1 -367
- package/src/commands/login.ts +16 -29
- package/src/commands/teleport.ts +14 -26
- package/src/lib/assistant-api-key-resolution.ts +89 -0
- package/src/lib/aws.ts +0 -593
- package/src/lib/constants.ts +0 -2
- package/src/lib/gcp.ts +3 -671
- package/src/lib/platform-client.ts +58 -5
- package/src/__tests__/ssh-user-guard.test.ts +0 -28
- package/src/adapters/openclaw-http-server.ts +0 -209
- package/src/adapters/openclaw.ts +0 -131
- package/src/lib/openclaw-runtime-server.ts +0 -28
package/src/lib/gcp.ts
CHANGED
|
@@ -1,214 +1,12 @@
|
|
|
1
|
-
import { unlinkSync, writeFileSync } from "fs";
|
|
2
|
-
import { tmpdir, userInfo } from "os";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
|
|
5
|
-
import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
|
|
6
|
-
import type { AssistantEntry } from "./assistant-config";
|
|
7
|
-
import { FIREWALL_TAG, GATEWAY_PORT } from "./constants";
|
|
8
|
-
import { PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
|
|
9
|
-
import type { Species } from "./constants";
|
|
10
|
-
import { leaseGuardianToken } from "./guardian-token";
|
|
11
|
-
import { getPlatformUrl } from "./platform-client";
|
|
12
|
-
import { generateInstanceName } from "./random-name";
|
|
13
1
|
import { exec, execOutput } from "./step-runner";
|
|
14
|
-
import { emitProgress } from "./desktop-progress.js";
|
|
15
|
-
|
|
16
|
-
export async function getActiveProject(): Promise<string> {
|
|
17
|
-
const output = await execOutput("gcloud", ["config", "get-value", "project"]);
|
|
18
|
-
const project = output.trim();
|
|
19
|
-
if (!project || project === "(unset)") {
|
|
20
|
-
throw new Error(
|
|
21
|
-
"No active GCP project. Run `gcloud config set project <project>` first.",
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
return project;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface FirewallRuleSpec {
|
|
28
|
-
name: string;
|
|
29
|
-
direction: "INGRESS" | "EGRESS";
|
|
30
|
-
action: "ALLOW" | "DENY";
|
|
31
|
-
rules: string;
|
|
32
|
-
sourceRanges?: string;
|
|
33
|
-
destinationRanges?: string;
|
|
34
|
-
targetTags: string;
|
|
35
|
-
description: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface FirewallRuleState {
|
|
39
|
-
name: string;
|
|
40
|
-
direction: string;
|
|
41
|
-
allowed: string;
|
|
42
|
-
sourceRanges: string;
|
|
43
|
-
destinationRanges: string;
|
|
44
|
-
targetTags: string;
|
|
45
|
-
description: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function describeFirewallRule(
|
|
49
|
-
ruleName: string,
|
|
50
|
-
project: string,
|
|
51
|
-
account?: string,
|
|
52
|
-
): Promise<FirewallRuleState | null> {
|
|
53
|
-
try {
|
|
54
|
-
const args = [
|
|
55
|
-
"compute",
|
|
56
|
-
"firewall-rules",
|
|
57
|
-
"describe",
|
|
58
|
-
ruleName,
|
|
59
|
-
`--project=${project}`,
|
|
60
|
-
"--format=json(name,direction,allowed,sourceRanges,destinationRanges,targetTags,description)",
|
|
61
|
-
];
|
|
62
|
-
if (account) args.push(`--account=${account}`);
|
|
63
|
-
const output = await execOutput("gcloud", args);
|
|
64
|
-
const parsed = JSON.parse(output);
|
|
65
|
-
const allowed = (parsed.allowed ?? [])
|
|
66
|
-
.map((a: { IPProtocol: string; ports?: string[] }) => {
|
|
67
|
-
const ports = a.ports ?? [];
|
|
68
|
-
if (ports.length === 0) {
|
|
69
|
-
return a.IPProtocol;
|
|
70
|
-
}
|
|
71
|
-
return ports.map((p: string) => `${a.IPProtocol}:${p}`).join(",");
|
|
72
|
-
})
|
|
73
|
-
.filter(Boolean)
|
|
74
|
-
.join(",");
|
|
75
|
-
return {
|
|
76
|
-
name: parsed.name ?? "",
|
|
77
|
-
direction: parsed.direction ?? "",
|
|
78
|
-
allowed,
|
|
79
|
-
sourceRanges: (parsed.sourceRanges ?? []).join(","),
|
|
80
|
-
destinationRanges: (parsed.destinationRanges ?? []).join(","),
|
|
81
|
-
targetTags: (parsed.targetTags ?? []).join(","),
|
|
82
|
-
description: parsed.description ?? "",
|
|
83
|
-
};
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
2
|
|
|
89
|
-
function
|
|
90
|
-
spec: FirewallRuleSpec,
|
|
91
|
-
state: FirewallRuleState,
|
|
92
|
-
): boolean {
|
|
93
|
-
return (
|
|
94
|
-
spec.direction !== state.direction ||
|
|
95
|
-
spec.rules !== state.allowed ||
|
|
96
|
-
(spec.sourceRanges ?? "") !== state.sourceRanges ||
|
|
97
|
-
(spec.destinationRanges ?? "") !== state.destinationRanges ||
|
|
98
|
-
spec.targetTags !== state.targetTags ||
|
|
99
|
-
spec.description !== state.description
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function createFirewallRule(
|
|
104
|
-
spec: FirewallRuleSpec,
|
|
105
|
-
project: string,
|
|
106
|
-
account?: string,
|
|
107
|
-
): Promise<void> {
|
|
108
|
-
const args = [
|
|
109
|
-
"compute",
|
|
110
|
-
"firewall-rules",
|
|
111
|
-
"create",
|
|
112
|
-
spec.name,
|
|
113
|
-
`--project=${project}`,
|
|
114
|
-
`--direction=${spec.direction}`,
|
|
115
|
-
`--action=${spec.action}`,
|
|
116
|
-
`--rules=${spec.rules}`,
|
|
117
|
-
`--target-tags=${spec.targetTags}`,
|
|
118
|
-
`--description=${spec.description}`,
|
|
119
|
-
];
|
|
120
|
-
if (spec.sourceRanges) {
|
|
121
|
-
args.push(`--source-ranges=${spec.sourceRanges}`);
|
|
122
|
-
}
|
|
123
|
-
if (spec.destinationRanges) {
|
|
124
|
-
args.push(`--destination-ranges=${spec.destinationRanges}`);
|
|
125
|
-
}
|
|
126
|
-
if (account) args.push(`--account=${account}`);
|
|
127
|
-
await exec("gcloud", args);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function deleteFirewallRule(
|
|
131
|
-
ruleName: string,
|
|
132
|
-
project: string,
|
|
133
|
-
account?: string,
|
|
134
|
-
): Promise<void> {
|
|
135
|
-
const args = [
|
|
136
|
-
"compute",
|
|
137
|
-
"firewall-rules",
|
|
138
|
-
"delete",
|
|
139
|
-
ruleName,
|
|
140
|
-
`--project=${project}`,
|
|
141
|
-
"--quiet",
|
|
142
|
-
];
|
|
143
|
-
if (account) args.push(`--account=${account}`);
|
|
144
|
-
await exec("gcloud", args);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export async function syncFirewallRules(
|
|
148
|
-
desiredRules: FirewallRuleSpec[],
|
|
149
|
-
project: string,
|
|
150
|
-
tag: string,
|
|
151
|
-
account?: string,
|
|
152
|
-
): Promise<void> {
|
|
153
|
-
let existingNames: string[];
|
|
154
|
-
try {
|
|
155
|
-
const listArgs = [
|
|
156
|
-
"compute",
|
|
157
|
-
"firewall-rules",
|
|
158
|
-
"list",
|
|
159
|
-
`--project=${project}`,
|
|
160
|
-
"--format=json(name,targetTags)",
|
|
161
|
-
];
|
|
162
|
-
if (account) listArgs.push(`--account=${account}`);
|
|
163
|
-
const output = await execOutput("gcloud", listArgs);
|
|
164
|
-
const allRules = JSON.parse(output) as Array<{
|
|
165
|
-
name: string;
|
|
166
|
-
targetTags?: string[];
|
|
167
|
-
}>;
|
|
168
|
-
existingNames = allRules
|
|
169
|
-
.filter((r) => r.targetTags?.includes(tag))
|
|
170
|
-
.map((r) => r.name);
|
|
171
|
-
} catch {
|
|
172
|
-
existingNames = [];
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const desiredNames = new Set(desiredRules.map((r) => r.name));
|
|
176
|
-
|
|
177
|
-
for (const existingName of existingNames) {
|
|
178
|
-
if (!desiredNames.has(existingName)) {
|
|
179
|
-
console.log(` 🗑️ Deleting stale firewall rule: ${existingName}`);
|
|
180
|
-
await deleteFirewallRule(existingName, project, account);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
for (const spec of desiredRules) {
|
|
185
|
-
const state = await describeFirewallRule(spec.name, project, account);
|
|
186
|
-
|
|
187
|
-
if (!state) {
|
|
188
|
-
console.log(` ➕ Creating firewall rule: ${spec.name}`);
|
|
189
|
-
await createFirewallRule(spec, project, account);
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (ruleNeedsUpdate(spec, state)) {
|
|
194
|
-
console.log(` 🔄 Updating firewall rule: ${spec.name}`);
|
|
195
|
-
await deleteFirewallRule(spec.name, project, account);
|
|
196
|
-
await createFirewallRule(spec, project, account);
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
console.log(` ✅ Firewall rule up to date: ${spec.name}`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export async function instanceExists(
|
|
3
|
+
async function instanceExists(
|
|
205
4
|
instanceName: string,
|
|
206
5
|
project: string,
|
|
207
6
|
zone: string,
|
|
208
|
-
account?: string,
|
|
209
7
|
): Promise<boolean> {
|
|
210
8
|
try {
|
|
211
|
-
|
|
9
|
+
await execOutput("gcloud", [
|
|
212
10
|
"compute",
|
|
213
11
|
"instances",
|
|
214
12
|
"describe",
|
|
@@ -216,9 +14,7 @@ export async function instanceExists(
|
|
|
216
14
|
`--project=${project}`,
|
|
217
15
|
`--zone=${zone}`,
|
|
218
16
|
"--format=get(name)",
|
|
219
|
-
];
|
|
220
|
-
if (account) args.push(`--account=${account}`);
|
|
221
|
-
await execOutput("gcloud", args);
|
|
17
|
+
]);
|
|
222
18
|
return true;
|
|
223
19
|
} catch (error) {
|
|
224
20
|
const msg = error instanceof Error ? error.message.toLowerCase() : "";
|
|
@@ -232,45 +28,6 @@ export async function instanceExists(
|
|
|
232
28
|
}
|
|
233
29
|
}
|
|
234
30
|
|
|
235
|
-
export async function fetchAndDisplayStartupLogs(
|
|
236
|
-
instanceName: string,
|
|
237
|
-
project: string,
|
|
238
|
-
zone: string,
|
|
239
|
-
account?: string,
|
|
240
|
-
): Promise<void> {
|
|
241
|
-
try {
|
|
242
|
-
const remoteCmd =
|
|
243
|
-
'echo "=== Last 50 lines of /var/log/startup-script.log ==="; ' +
|
|
244
|
-
"tail -50 /var/log/startup-script.log 2>/dev/null || echo '(no startup log found)'; " +
|
|
245
|
-
'echo ""; ' +
|
|
246
|
-
'echo "=== /var/log/startup-error ==="; ' +
|
|
247
|
-
"cat /var/log/startup-error 2>/dev/null || echo '(no error file found)'";
|
|
248
|
-
const args = [
|
|
249
|
-
"compute",
|
|
250
|
-
"ssh",
|
|
251
|
-
instanceName,
|
|
252
|
-
`--project=${project}`,
|
|
253
|
-
`--zone=${zone}`,
|
|
254
|
-
"--quiet",
|
|
255
|
-
"--ssh-flag=-o StrictHostKeyChecking=no",
|
|
256
|
-
"--ssh-flag=-o UserKnownHostsFile=/dev/null",
|
|
257
|
-
"--ssh-flag=-o ConnectTimeout=10",
|
|
258
|
-
"--ssh-flag=-o LogLevel=ERROR",
|
|
259
|
-
`--command=${remoteCmd}`,
|
|
260
|
-
];
|
|
261
|
-
if (account) args.push(`--account=${account}`);
|
|
262
|
-
const output = await execOutput("gcloud", args);
|
|
263
|
-
console.log("📋 Startup logs from instance:");
|
|
264
|
-
for (const line of output.split("\n")) {
|
|
265
|
-
console.log(` ${line}`);
|
|
266
|
-
}
|
|
267
|
-
console.log("");
|
|
268
|
-
} catch {
|
|
269
|
-
console.log("⚠️ Could not retrieve startup logs from instance");
|
|
270
|
-
console.log("");
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
31
|
async function checkGcloudAvailable(): Promise<boolean> {
|
|
275
32
|
try {
|
|
276
33
|
await execOutput("gcloud", ["--version"]);
|
|
@@ -280,431 +37,6 @@ async function checkGcloudAvailable(): Promise<boolean> {
|
|
|
280
37
|
}
|
|
281
38
|
}
|
|
282
39
|
|
|
283
|
-
export interface PollResult {
|
|
284
|
-
lastLine: string | null;
|
|
285
|
-
done: boolean;
|
|
286
|
-
failed: boolean;
|
|
287
|
-
errorContent: string;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export interface WatchHatchingResult {
|
|
291
|
-
success: boolean;
|
|
292
|
-
errorContent: string;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const INSTALL_SCRIPT_REMOTE_PATH = "/tmp/vellum-install.sh";
|
|
296
|
-
const MACHINE_TYPE = "e2-standard-4"; // 4 vCPUs, 16 GB memory
|
|
297
|
-
|
|
298
|
-
const DESIRED_FIREWALL_RULES: FirewallRuleSpec[] = [
|
|
299
|
-
{
|
|
300
|
-
name: "allow-vellum-assistant-gateway",
|
|
301
|
-
direction: "INGRESS",
|
|
302
|
-
action: "ALLOW",
|
|
303
|
-
rules: `tcp:${GATEWAY_PORT}`,
|
|
304
|
-
sourceRanges: "0.0.0.0/0",
|
|
305
|
-
targetTags: FIREWALL_TAG,
|
|
306
|
-
description: `Allow gateway ingress on port ${GATEWAY_PORT} for vellum-assistant instances`,
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
name: "allow-vellum-assistant-egress",
|
|
310
|
-
direction: "EGRESS",
|
|
311
|
-
action: "ALLOW",
|
|
312
|
-
rules: "all",
|
|
313
|
-
destinationRanges: "0.0.0.0/0",
|
|
314
|
-
targetTags: FIREWALL_TAG,
|
|
315
|
-
description: "Allow all egress traffic for vellum-assistant instances",
|
|
316
|
-
},
|
|
317
|
-
];
|
|
318
|
-
|
|
319
|
-
import INSTALL_SCRIPT_CONTENT from "../adapters/install.sh" with { type: "text" };
|
|
320
|
-
|
|
321
|
-
function resolveInstallScriptPath(): string {
|
|
322
|
-
const tmpPath = join(tmpdir(), `vellum-install-${process.pid}.sh`);
|
|
323
|
-
writeFileSync(tmpPath, INSTALL_SCRIPT_CONTENT, { mode: 0o755 });
|
|
324
|
-
return tmpPath;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function pollInstance(
|
|
328
|
-
instanceName: string,
|
|
329
|
-
project: string,
|
|
330
|
-
zone: string,
|
|
331
|
-
account?: string,
|
|
332
|
-
): Promise<PollResult> {
|
|
333
|
-
try {
|
|
334
|
-
const remoteCmd =
|
|
335
|
-
"L=$(tail -1 /var/log/startup-script.log 2>/dev/null || true); " +
|
|
336
|
-
"S=$(systemctl is-active google-startup-scripts.service 2>/dev/null || true); " +
|
|
337
|
-
"E=$(cat /var/log/startup-error 2>/dev/null || true); " +
|
|
338
|
-
'printf "%s\\n===HATCH_SEP===\\n%s\\n===HATCH_ERR===\\n%s" "$L" "$S" "$E"';
|
|
339
|
-
const args = [
|
|
340
|
-
"compute",
|
|
341
|
-
"ssh",
|
|
342
|
-
instanceName,
|
|
343
|
-
`--project=${project}`,
|
|
344
|
-
`--zone=${zone}`,
|
|
345
|
-
"--quiet",
|
|
346
|
-
"--ssh-flag=-o StrictHostKeyChecking=no",
|
|
347
|
-
"--ssh-flag=-o UserKnownHostsFile=/dev/null",
|
|
348
|
-
"--ssh-flag=-o ConnectTimeout=10",
|
|
349
|
-
"--ssh-flag=-o LogLevel=ERROR",
|
|
350
|
-
`--command=${remoteCmd}`,
|
|
351
|
-
];
|
|
352
|
-
if (account) args.push(`--account=${account}`);
|
|
353
|
-
const output = await execOutput("gcloud", args);
|
|
354
|
-
const sepIdx = output.indexOf("===HATCH_SEP===");
|
|
355
|
-
if (sepIdx === -1) {
|
|
356
|
-
return {
|
|
357
|
-
lastLine: output.trim() || null,
|
|
358
|
-
done: false,
|
|
359
|
-
failed: false,
|
|
360
|
-
errorContent: "",
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
const errIdx = output.indexOf("===HATCH_ERR===");
|
|
364
|
-
const lastLine = output.substring(0, sepIdx).trim() || null;
|
|
365
|
-
const statusEnd = errIdx === -1 ? undefined : errIdx;
|
|
366
|
-
const status = output
|
|
367
|
-
.substring(sepIdx + "===HATCH_SEP===".length, statusEnd)
|
|
368
|
-
.trim();
|
|
369
|
-
const errorContent =
|
|
370
|
-
errIdx === -1
|
|
371
|
-
? ""
|
|
372
|
-
: output.substring(errIdx + "===HATCH_ERR===".length).trim();
|
|
373
|
-
const done =
|
|
374
|
-
lastLine !== null && status !== "active" && status !== "activating";
|
|
375
|
-
const failed = errorContent.length > 0 || status === "failed";
|
|
376
|
-
return { lastLine, done, failed, errorContent };
|
|
377
|
-
} catch {
|
|
378
|
-
return { lastLine: null, done: false, failed: false, errorContent: "" };
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async function checkCurlFailure(
|
|
383
|
-
instanceName: string,
|
|
384
|
-
project: string,
|
|
385
|
-
zone: string,
|
|
386
|
-
account?: string,
|
|
387
|
-
): Promise<boolean> {
|
|
388
|
-
try {
|
|
389
|
-
const args = [
|
|
390
|
-
"compute",
|
|
391
|
-
"ssh",
|
|
392
|
-
instanceName,
|
|
393
|
-
`--project=${project}`,
|
|
394
|
-
`--zone=${zone}`,
|
|
395
|
-
"--quiet",
|
|
396
|
-
"--ssh-flag=-o StrictHostKeyChecking=no",
|
|
397
|
-
"--ssh-flag=-o UserKnownHostsFile=/dev/null",
|
|
398
|
-
"--ssh-flag=-o ConnectTimeout=10",
|
|
399
|
-
"--ssh-flag=-o LogLevel=ERROR",
|
|
400
|
-
`--command=test -s ${INSTALL_SCRIPT_REMOTE_PATH} && echo EXISTS || echo MISSING`,
|
|
401
|
-
];
|
|
402
|
-
if (account) args.push(`--account=${account}`);
|
|
403
|
-
const output = await execOutput("gcloud", args);
|
|
404
|
-
return output.trim() === "MISSING";
|
|
405
|
-
} catch {
|
|
406
|
-
return false;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
async function recoverFromCurlFailure(
|
|
411
|
-
instanceName: string,
|
|
412
|
-
project: string,
|
|
413
|
-
zone: string,
|
|
414
|
-
sshUser: string,
|
|
415
|
-
account?: string,
|
|
416
|
-
): Promise<void> {
|
|
417
|
-
const installScriptPath = resolveInstallScriptPath();
|
|
418
|
-
|
|
419
|
-
const scpArgs = [
|
|
420
|
-
"compute",
|
|
421
|
-
"scp",
|
|
422
|
-
installScriptPath,
|
|
423
|
-
`${instanceName}:${INSTALL_SCRIPT_REMOTE_PATH}`,
|
|
424
|
-
`--zone=${zone}`,
|
|
425
|
-
`--project=${project}`,
|
|
426
|
-
];
|
|
427
|
-
if (account) scpArgs.push(`--account=${account}`);
|
|
428
|
-
console.log("\ud83d\udccb Uploading install script to instance...");
|
|
429
|
-
await exec("gcloud", scpArgs);
|
|
430
|
-
|
|
431
|
-
const sshArgs = [
|
|
432
|
-
"compute",
|
|
433
|
-
"ssh",
|
|
434
|
-
`${sshUser}@${instanceName}`,
|
|
435
|
-
`--zone=${zone}`,
|
|
436
|
-
`--project=${project}`,
|
|
437
|
-
`--command=source ${INSTALL_SCRIPT_REMOTE_PATH}`,
|
|
438
|
-
];
|
|
439
|
-
if (account) sshArgs.push(`--account=${account}`);
|
|
440
|
-
console.log("\ud83d\udd27 Running install script on instance...");
|
|
441
|
-
await exec("gcloud", sshArgs);
|
|
442
|
-
try {
|
|
443
|
-
unlinkSync(installScriptPath);
|
|
444
|
-
} catch {}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
export async function hatchGcp(
|
|
448
|
-
species: Species,
|
|
449
|
-
detached: boolean,
|
|
450
|
-
name: string | null,
|
|
451
|
-
buildStartupScript: (
|
|
452
|
-
species: Species,
|
|
453
|
-
sshUser: string,
|
|
454
|
-
providerApiKeys: Record<string, string>,
|
|
455
|
-
instanceName: string,
|
|
456
|
-
cloud: "gcp",
|
|
457
|
-
configValues?: Record<string, string>,
|
|
458
|
-
) => Promise<{ script: string; laptopBootstrapSecret: string }>,
|
|
459
|
-
watchHatching: (
|
|
460
|
-
pollFn: () => Promise<PollResult>,
|
|
461
|
-
instanceName: string,
|
|
462
|
-
startTime: number,
|
|
463
|
-
species: Species,
|
|
464
|
-
) => Promise<WatchHatchingResult>,
|
|
465
|
-
configValues: Record<string, string> = {},
|
|
466
|
-
): Promise<void> {
|
|
467
|
-
const startTime = Date.now();
|
|
468
|
-
const account = process.env.GCP_ACCOUNT_EMAIL;
|
|
469
|
-
|
|
470
|
-
try {
|
|
471
|
-
const project = process.env.GCP_PROJECT ?? (await getActiveProject());
|
|
472
|
-
let instanceName: string;
|
|
473
|
-
|
|
474
|
-
instanceName = generateInstanceName(species, name);
|
|
475
|
-
|
|
476
|
-
console.log(`\ud83e\udd5a Creating new assistant: ${instanceName}`);
|
|
477
|
-
console.log(` Species: ${species}`);
|
|
478
|
-
console.log(` Cloud: GCP`);
|
|
479
|
-
console.log(` Project: ${project}`);
|
|
480
|
-
const zone = process.env.GCP_DEFAULT_ZONE;
|
|
481
|
-
if (!zone) {
|
|
482
|
-
console.error("Error: GCP_DEFAULT_ZONE environment variable is not set.");
|
|
483
|
-
process.exit(1);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
console.log(` Zone: ${zone}`);
|
|
487
|
-
console.log(` Machine type: ${MACHINE_TYPE}`);
|
|
488
|
-
console.log("");
|
|
489
|
-
|
|
490
|
-
if (name) {
|
|
491
|
-
if (await instanceExists(name, project, zone, account)) {
|
|
492
|
-
console.error(
|
|
493
|
-
`Error: Instance name '${name}' is already taken. Please choose a different name.`,
|
|
494
|
-
);
|
|
495
|
-
process.exit(1);
|
|
496
|
-
}
|
|
497
|
-
} else {
|
|
498
|
-
while (await instanceExists(instanceName, project, zone, account)) {
|
|
499
|
-
console.log(
|
|
500
|
-
`\u26a0\ufe0f Instance name ${instanceName} already exists, generating a new name...`,
|
|
501
|
-
);
|
|
502
|
-
instanceName = generateInstanceName(species);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
let sshUser: string;
|
|
507
|
-
try {
|
|
508
|
-
sshUser = userInfo().username;
|
|
509
|
-
} catch {
|
|
510
|
-
sshUser = process.env.USER ?? "";
|
|
511
|
-
}
|
|
512
|
-
if (!sshUser) {
|
|
513
|
-
console.error(
|
|
514
|
-
"Error: Could not determine SSH username. Set the USER environment variable and try again.",
|
|
515
|
-
);
|
|
516
|
-
process.exit(1);
|
|
517
|
-
}
|
|
518
|
-
const hatchedBy = process.env.VELLUM_HATCHED_BY;
|
|
519
|
-
const providerApiKeys: Record<string, string> = {};
|
|
520
|
-
for (const [, envVar] of Object.entries(PROVIDER_ENV_VAR_NAMES)) {
|
|
521
|
-
const value = process.env[envVar];
|
|
522
|
-
if (value) {
|
|
523
|
-
providerApiKeys[envVar] = value;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if (Object.keys(providerApiKeys).length === 0) {
|
|
527
|
-
console.error(
|
|
528
|
-
"Error: No provider API key environment variable is set. " +
|
|
529
|
-
"Set at least one of: " +
|
|
530
|
-
Object.values(PROVIDER_ENV_VAR_NAMES).join(", "),
|
|
531
|
-
);
|
|
532
|
-
process.exit(1);
|
|
533
|
-
}
|
|
534
|
-
emitProgress(1, 5, "Preparing startup script...");
|
|
535
|
-
const { script: startupScript, laptopBootstrapSecret } =
|
|
536
|
-
await buildStartupScript(
|
|
537
|
-
species,
|
|
538
|
-
sshUser,
|
|
539
|
-
providerApiKeys,
|
|
540
|
-
instanceName,
|
|
541
|
-
"gcp",
|
|
542
|
-
configValues,
|
|
543
|
-
);
|
|
544
|
-
const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
|
|
545
|
-
writeFileSync(startupScriptPath, startupScript);
|
|
546
|
-
|
|
547
|
-
emitProgress(2, 5, "Launching instance...");
|
|
548
|
-
console.log("\ud83d\udd28 Creating instance with startup script...");
|
|
549
|
-
try {
|
|
550
|
-
const createArgs = [
|
|
551
|
-
"compute",
|
|
552
|
-
"instances",
|
|
553
|
-
"create",
|
|
554
|
-
instanceName,
|
|
555
|
-
`--project=${project}`,
|
|
556
|
-
`--zone=${zone}`,
|
|
557
|
-
`--machine-type=${MACHINE_TYPE}`,
|
|
558
|
-
"--image-family=debian-11",
|
|
559
|
-
"--image-project=debian-cloud",
|
|
560
|
-
"--boot-disk-size=50GB",
|
|
561
|
-
"--boot-disk-type=pd-standard",
|
|
562
|
-
`--metadata-from-file=startup-script=${startupScriptPath}`,
|
|
563
|
-
`--labels=species=${species},vellum-assistant=true${hatchedBy ? `,hatched-by=${hatchedBy.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}` : ""}`,
|
|
564
|
-
"--tags=vellum-assistant",
|
|
565
|
-
"--no-service-account",
|
|
566
|
-
"--no-scopes",
|
|
567
|
-
];
|
|
568
|
-
if (account) createArgs.push(`--account=${account}`);
|
|
569
|
-
await exec("gcloud", createArgs);
|
|
570
|
-
} finally {
|
|
571
|
-
try {
|
|
572
|
-
unlinkSync(startupScriptPath);
|
|
573
|
-
} catch {}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
console.log("\ud83d\udd12 Syncing firewall rules...");
|
|
577
|
-
await syncFirewallRules(
|
|
578
|
-
DESIRED_FIREWALL_RULES,
|
|
579
|
-
project,
|
|
580
|
-
FIREWALL_TAG,
|
|
581
|
-
account,
|
|
582
|
-
);
|
|
583
|
-
|
|
584
|
-
console.log(`\u2705 Instance ${instanceName} created successfully\n`);
|
|
585
|
-
|
|
586
|
-
let externalIp: string | null = null;
|
|
587
|
-
try {
|
|
588
|
-
const describeArgs = [
|
|
589
|
-
"compute",
|
|
590
|
-
"instances",
|
|
591
|
-
"describe",
|
|
592
|
-
instanceName,
|
|
593
|
-
`--project=${project}`,
|
|
594
|
-
`--zone=${zone}`,
|
|
595
|
-
"--format=get(networkInterfaces[0].accessConfigs[0].natIP)",
|
|
596
|
-
];
|
|
597
|
-
if (account) describeArgs.push(`--account=${account}`);
|
|
598
|
-
const ipOutput = await execOutput("gcloud", describeArgs);
|
|
599
|
-
externalIp = ipOutput.trim() || null;
|
|
600
|
-
} catch {
|
|
601
|
-
console.log(
|
|
602
|
-
"\u26a0\ufe0f Could not retrieve external IP yet (instance may still be starting)",
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const runtimeUrl = externalIp
|
|
607
|
-
? `http://${externalIp}:${GATEWAY_PORT}`
|
|
608
|
-
: `http://${instanceName}:${GATEWAY_PORT}`;
|
|
609
|
-
emitProgress(3, 5, "Saving configuration...");
|
|
610
|
-
const gcpEntry: AssistantEntry = {
|
|
611
|
-
assistantId: instanceName,
|
|
612
|
-
runtimeUrl,
|
|
613
|
-
cloud: "gcp",
|
|
614
|
-
project,
|
|
615
|
-
zone,
|
|
616
|
-
species,
|
|
617
|
-
sshUser,
|
|
618
|
-
hatchedAt: new Date().toISOString(),
|
|
619
|
-
};
|
|
620
|
-
saveAssistantEntry(gcpEntry);
|
|
621
|
-
setActiveAssistant(instanceName);
|
|
622
|
-
|
|
623
|
-
if (detached) {
|
|
624
|
-
console.log("\ud83d\ude80 Startup script is running on the instance...");
|
|
625
|
-
console.log("");
|
|
626
|
-
console.log("\u2705 Assistant is hatching!\n");
|
|
627
|
-
console.log("Instance details:");
|
|
628
|
-
console.log(` Name: ${instanceName}`);
|
|
629
|
-
console.log(` Project: ${project}`);
|
|
630
|
-
console.log(` Zone: ${zone}`);
|
|
631
|
-
if (externalIp) {
|
|
632
|
-
console.log(` External IP: ${externalIp}`);
|
|
633
|
-
}
|
|
634
|
-
console.log("");
|
|
635
|
-
} else {
|
|
636
|
-
console.log(" Press Ctrl+C to detach (instance will keep running)");
|
|
637
|
-
console.log("");
|
|
638
|
-
|
|
639
|
-
emitProgress(4, 5, "Installing software...");
|
|
640
|
-
const result = await watchHatching(
|
|
641
|
-
() => pollInstance(instanceName, project, zone, account),
|
|
642
|
-
instanceName,
|
|
643
|
-
startTime,
|
|
644
|
-
species,
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
if (!result.success) {
|
|
648
|
-
console.log("");
|
|
649
|
-
if (result.errorContent) {
|
|
650
|
-
console.log("\ud83d\udccb Startup error:");
|
|
651
|
-
console.log(` ${result.errorContent}`);
|
|
652
|
-
console.log("");
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
await fetchAndDisplayStartupLogs(instanceName, project, zone, account);
|
|
656
|
-
|
|
657
|
-
if (
|
|
658
|
-
species === "vellum" &&
|
|
659
|
-
(await checkCurlFailure(instanceName, project, zone, account))
|
|
660
|
-
) {
|
|
661
|
-
const installScriptUrl = `${getPlatformUrl()}/install.sh`;
|
|
662
|
-
console.log(
|
|
663
|
-
`\ud83d\udd04 Detected install script curl failure for ${installScriptUrl}, attempting recovery...`,
|
|
664
|
-
);
|
|
665
|
-
await recoverFromCurlFailure(
|
|
666
|
-
instanceName,
|
|
667
|
-
project,
|
|
668
|
-
zone,
|
|
669
|
-
sshUser,
|
|
670
|
-
account,
|
|
671
|
-
);
|
|
672
|
-
console.log("\u2705 Recovery successful!");
|
|
673
|
-
} else {
|
|
674
|
-
process.exit(1);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
emitProgress(5, 5, "Finalizing...");
|
|
679
|
-
try {
|
|
680
|
-
await leaseGuardianToken(
|
|
681
|
-
runtimeUrl,
|
|
682
|
-
instanceName,
|
|
683
|
-
laptopBootstrapSecret,
|
|
684
|
-
);
|
|
685
|
-
} catch (err) {
|
|
686
|
-
console.warn(
|
|
687
|
-
`\u26a0\ufe0f Could not lease guardian token: ${err instanceof Error ? err.message : err}`,
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
console.log("Instance details:");
|
|
692
|
-
console.log(` Name: ${instanceName}`);
|
|
693
|
-
console.log(` Project: ${project}`);
|
|
694
|
-
console.log(` Zone: ${zone}`);
|
|
695
|
-
if (externalIp) {
|
|
696
|
-
console.log(` External IP: ${externalIp}`);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
} catch (error) {
|
|
700
|
-
console.error(
|
|
701
|
-
"\u274c Error:",
|
|
702
|
-
error instanceof Error ? error.message : error,
|
|
703
|
-
);
|
|
704
|
-
process.exit(1);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
40
|
export async function retireInstance(
|
|
709
41
|
name: string,
|
|
710
42
|
project: string,
|