@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.
Files changed (38) hide show
  1. package/README.md +6 -7
  2. package/knip.json +0 -1
  3. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/package.json +4 -1
  4. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  5. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  6. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  7. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  8. package/node_modules/@vellumai/local-mode/src/__tests__/guardian-token-refresh.test.ts +2 -8
  9. package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +2 -8
  10. package/node_modules/@vellumai/local-mode/src/__tests__/helpers/child-process-mock.ts +55 -0
  11. package/node_modules/@vellumai/local-mode/src/__tests__/retire.test.ts +2 -8
  12. package/node_modules/@vellumai/local-mode/src/__tests__/upgrade.test.ts +2 -8
  13. package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +2 -8
  14. package/node_modules/@vellumai/local-mode/src/devices.test.ts +5 -8
  15. package/node_modules/@vellumai/local-mode/src/index.ts +1 -0
  16. package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +6 -0
  17. package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +165 -0
  18. package/node_modules/@vellumai/local-mode/src/lockfile.ts +63 -6
  19. package/node_modules/@vellumai/service-contracts/package.json +4 -1
  20. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  23. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  24. package/package.json +1 -1
  25. package/src/__tests__/assistant-api-key-resolution.test.ts +102 -0
  26. package/src/__tests__/constants.test.ts +6 -2
  27. package/src/commands/hatch.ts +1 -367
  28. package/src/commands/login.ts +16 -29
  29. package/src/commands/teleport.ts +14 -26
  30. package/src/lib/assistant-api-key-resolution.ts +89 -0
  31. package/src/lib/aws.ts +0 -593
  32. package/src/lib/constants.ts +0 -2
  33. package/src/lib/gcp.ts +3 -671
  34. package/src/lib/platform-client.ts +58 -5
  35. package/src/__tests__/ssh-user-guard.test.ts +0 -28
  36. package/src/adapters/openclaw-http-server.ts +0 -209
  37. package/src/adapters/openclaw.ts +0 -131
  38. package/src/lib/openclaw-runtime-server.ts +0 -28
package/src/lib/aws.ts CHANGED
@@ -1,597 +1,4 @@
1
- import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
2
- import { homedir, tmpdir, userInfo } from "os";
3
- import { join } from "path";
4
-
5
- import { buildStartupScript, watchHatching } from "../commands/hatch";
6
- import type { PollResult } from "../commands/hatch";
7
- import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
8
- import type { AssistantEntry } from "./assistant-config";
9
- import { GATEWAY_PORT } from "./constants";
10
- import { PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
11
- import type { Species } from "./constants";
12
- import { leaseGuardianToken } from "./guardian-token";
13
- import { generateInstanceName } from "./random-name";
14
1
  import { exec, execOutput } from "./step-runner";
15
- import { emitProgress } from "./desktop-progress.js";
16
-
17
- const KEY_PAIR_NAME = "vellum-assistant";
18
- const DEFAULT_SSH_USER = "admin";
19
- const AWS_INSTANCE_TYPE = "t3.xlarge";
20
- const AWS_DEFAULT_REGION = "us-east-1";
21
-
22
- export async function getActiveRegion(): Promise<string> {
23
- try {
24
- const output = await execOutput("aws", ["configure", "get", "region"]);
25
- const region = output.trim();
26
- if (region) return region;
27
- } catch {}
28
- throw new Error(
29
- "No active AWS region. Set AWS_REGION or run `aws configure set region <region>` first.",
30
- );
31
- }
32
-
33
- export async function getDefaultVpcId(region: string): Promise<string> {
34
- const output = await execOutput("aws", [
35
- "ec2",
36
- "describe-vpcs",
37
- "--filters",
38
- "Name=isDefault,Values=true",
39
- "--query",
40
- "Vpcs[0].VpcId",
41
- "--output",
42
- "text",
43
- "--region",
44
- region,
45
- ]);
46
- const vpcId = output.trim();
47
- if (!vpcId || vpcId === "None") {
48
- throw new Error(
49
- "No default VPC found. Please create a default VPC or specify one.",
50
- );
51
- }
52
- return vpcId;
53
- }
54
-
55
- export async function ensureSecurityGroup(
56
- groupName: string,
57
- vpcId: string,
58
- gatewayPort: number,
59
- region: string,
60
- ): Promise<string> {
61
- try {
62
- const output = await execOutput("aws", [
63
- "ec2",
64
- "describe-security-groups",
65
- "--filters",
66
- `Name=group-name,Values=${groupName}`,
67
- `Name=vpc-id,Values=${vpcId}`,
68
- "--query",
69
- "SecurityGroups[0].GroupId",
70
- "--output",
71
- "text",
72
- "--region",
73
- region,
74
- ]);
75
- const groupId = output.trim();
76
- if (groupId && groupId !== "None") return groupId;
77
- } catch {}
78
-
79
- const createOutput = await execOutput("aws", [
80
- "ec2",
81
- "create-security-group",
82
- "--group-name",
83
- groupName,
84
- "--description",
85
- "Security group for vellum-assistant instances",
86
- "--vpc-id",
87
- vpcId,
88
- "--query",
89
- "GroupId",
90
- "--output",
91
- "text",
92
- "--region",
93
- region,
94
- ]);
95
- const groupId = createOutput.trim();
96
-
97
- await exec("aws", [
98
- "ec2",
99
- "authorize-security-group-ingress",
100
- "--group-id",
101
- groupId,
102
- "--protocol",
103
- "tcp",
104
- "--port",
105
- String(gatewayPort),
106
- "--cidr",
107
- "0.0.0.0/0",
108
- "--region",
109
- region,
110
- ]);
111
-
112
- await exec("aws", [
113
- "ec2",
114
- "authorize-security-group-ingress",
115
- "--group-id",
116
- groupId,
117
- "--protocol",
118
- "tcp",
119
- "--port",
120
- "22",
121
- "--cidr",
122
- "0.0.0.0/0",
123
- "--region",
124
- region,
125
- ]);
126
-
127
- return groupId;
128
- }
129
-
130
- export async function ensureKeyPair(region: string): Promise<string> {
131
- const sshDir = join(homedir(), ".ssh");
132
- const keyPath = join(sshDir, `${KEY_PAIR_NAME}.pem`);
133
-
134
- try {
135
- await execOutput("aws", [
136
- "ec2",
137
- "describe-key-pairs",
138
- "--key-names",
139
- KEY_PAIR_NAME,
140
- "--region",
141
- region,
142
- ]);
143
- if (!existsSync(keyPath)) {
144
- throw new Error(
145
- `Key pair '${KEY_PAIR_NAME}' exists in AWS but private key not found at ${keyPath}. ` +
146
- `Delete it with: aws ec2 delete-key-pair --key-name ${KEY_PAIR_NAME} --region ${region}`,
147
- );
148
- }
149
- return keyPath;
150
- } catch (error) {
151
- if (error instanceof Error && error.message.includes("not found at")) {
152
- throw error;
153
- }
154
- }
155
-
156
- if (!existsSync(sshDir)) {
157
- mkdirSync(sshDir, { recursive: true, mode: 0o700 });
158
- }
159
- const output = await execOutput("aws", [
160
- "ec2",
161
- "create-key-pair",
162
- "--key-name",
163
- KEY_PAIR_NAME,
164
- "--query",
165
- "KeyMaterial",
166
- "--output",
167
- "text",
168
- "--region",
169
- region,
170
- ]);
171
- writeFileSync(keyPath, output.trim() + "\n", { mode: 0o600 });
172
- return keyPath;
173
- }
174
-
175
- export async function getLatestDebianAmi(region: string): Promise<string> {
176
- const output = await execOutput("aws", [
177
- "ec2",
178
- "describe-images",
179
- "--owners",
180
- "136693071363",
181
- "--filters",
182
- "Name=name,Values=debian-11-amd64-*",
183
- "Name=state,Values=available",
184
- "--query",
185
- "sort_by(Images, &CreationDate)[-1].ImageId",
186
- "--output",
187
- "text",
188
- "--region",
189
- region,
190
- ]);
191
- const amiId = output.trim();
192
- if (!amiId || amiId === "None") {
193
- throw new Error("Could not find a Debian 11 AMI in this region.");
194
- }
195
- return amiId;
196
- }
197
-
198
- export async function instanceExistsByName(
199
- name: string,
200
- region: string,
201
- ): Promise<boolean> {
202
- try {
203
- const output = await execOutput("aws", [
204
- "ec2",
205
- "describe-instances",
206
- "--filters",
207
- `Name=tag:Name,Values=${name}`,
208
- "Name=instance-state-name,Values=pending,running,stopping,stopped",
209
- "--query",
210
- "Reservations[0].Instances[0].InstanceId",
211
- "--output",
212
- "text",
213
- "--region",
214
- region,
215
- ]);
216
- return output.trim() !== "" && output.trim() !== "None";
217
- } catch {
218
- return false;
219
- }
220
- }
221
-
222
- export async function launchInstance(
223
- name: string,
224
- amiId: string,
225
- instanceType: string,
226
- securityGroupId: string,
227
- userDataPath: string,
228
- species: string,
229
- region: string,
230
- hatchedBy?: string,
231
- ): Promise<string> {
232
- const blockDeviceMappings = JSON.stringify([
233
- {
234
- DeviceName: "/dev/xvda",
235
- Ebs: { VolumeSize: 50, VolumeType: "gp3" },
236
- },
237
- ]);
238
- const tags = [
239
- { Key: "Name", Value: name },
240
- { Key: "vellum-assistant", Value: "true" },
241
- { Key: "species", Value: species },
242
- ];
243
- if (hatchedBy) {
244
- tags.push({ Key: "hatched-by", Value: hatchedBy });
245
- }
246
- const tagSpecifications = JSON.stringify([
247
- {
248
- ResourceType: "instance",
249
- Tags: tags,
250
- },
251
- ]);
252
-
253
- const output = await execOutput("aws", [
254
- "ec2",
255
- "run-instances",
256
- "--image-id",
257
- amiId,
258
- "--instance-type",
259
- instanceType,
260
- "--key-name",
261
- KEY_PAIR_NAME,
262
- "--security-group-ids",
263
- securityGroupId,
264
- "--user-data",
265
- `file://${userDataPath}`,
266
- "--block-device-mappings",
267
- blockDeviceMappings,
268
- "--tag-specifications",
269
- tagSpecifications,
270
- "--query",
271
- "Instances[0].InstanceId",
272
- "--output",
273
- "text",
274
- "--region",
275
- region,
276
- ]);
277
- return output.trim();
278
- }
279
-
280
- export async function waitForInstanceRunning(
281
- instanceId: string,
282
- region: string,
283
- ): Promise<void> {
284
- await exec("aws", [
285
- "ec2",
286
- "wait",
287
- "instance-running",
288
- "--instance-ids",
289
- instanceId,
290
- "--region",
291
- region,
292
- ]);
293
- }
294
-
295
- export async function getInstancePublicIp(
296
- instanceId: string,
297
- region: string,
298
- ): Promise<string | null> {
299
- const output = await execOutput("aws", [
300
- "ec2",
301
- "describe-instances",
302
- "--instance-ids",
303
- instanceId,
304
- "--query",
305
- "Reservations[0].Instances[0].PublicIpAddress",
306
- "--output",
307
- "text",
308
- "--region",
309
- region,
310
- ]);
311
- const ip = output.trim();
312
- return ip && ip !== "None" ? ip : null;
313
- }
314
-
315
- async function awsSshExec(
316
- ip: string,
317
- keyPath: string,
318
- command: string,
319
- ): Promise<string> {
320
- return execOutput("ssh", [
321
- "-i",
322
- keyPath,
323
- "-o",
324
- "StrictHostKeyChecking=no",
325
- "-o",
326
- "UserKnownHostsFile=/dev/null",
327
- "-o",
328
- "ConnectTimeout=10",
329
- "-o",
330
- "LogLevel=ERROR",
331
- `${DEFAULT_SSH_USER}@${ip}`,
332
- command,
333
- ]);
334
- }
335
-
336
- async function pollAwsInstance(
337
- ip: string,
338
- keyPath: string,
339
- ): Promise<PollResult> {
340
- try {
341
- const remoteCmd =
342
- "L=$(tail -1 /var/log/startup-script.log 2>/dev/null || true); " +
343
- "S=$(cloud-init status 2>/dev/null | awk '/status:/{print $2}' || echo unknown); " +
344
- "E=$(cat /var/log/startup-error 2>/dev/null || true); " +
345
- 'printf "%s\\n===HATCH_SEP===\\n%s\\n===HATCH_ERR===\\n%s" "$L" "$S" "$E"';
346
- const output = await awsSshExec(ip, keyPath, remoteCmd);
347
- const sepIdx = output.indexOf("===HATCH_SEP===");
348
- if (sepIdx === -1) {
349
- return {
350
- lastLine: output.trim() || null,
351
- done: false,
352
- failed: false,
353
- errorContent: "",
354
- };
355
- }
356
- const errIdx = output.indexOf("===HATCH_ERR===");
357
- const lastLine = output.substring(0, sepIdx).trim() || null;
358
- const statusEnd = errIdx === -1 ? undefined : errIdx;
359
- const status = output
360
- .substring(sepIdx + "===HATCH_SEP===".length, statusEnd)
361
- .trim();
362
- const errorContent =
363
- errIdx === -1
364
- ? ""
365
- : output.substring(errIdx + "===HATCH_ERR===".length).trim();
366
- const done =
367
- lastLine !== null && status !== "running" && status !== "pending";
368
- const failed = errorContent.length > 0 || status === "error";
369
- return { lastLine, done, failed, errorContent };
370
- } catch {
371
- return { lastLine: null, done: false, failed: false, errorContent: "" };
372
- }
373
- }
374
-
375
- export async function hatchAws(
376
- species: Species,
377
- detached: boolean,
378
- name: string | null,
379
- configValues: Record<string, string> = {},
380
- ): Promise<void> {
381
- const startTime = Date.now();
382
- try {
383
- const region =
384
- process.env.AWS_REGION ??
385
- process.env.AWS_DEFAULT_REGION ??
386
- (await getActiveRegion().catch(() => AWS_DEFAULT_REGION));
387
- let instanceName: string;
388
-
389
- instanceName = generateInstanceName(species, name);
390
-
391
- console.log(`\u{1F95A} Creating new assistant: ${instanceName}`);
392
- console.log(` Species: ${species}`);
393
- console.log(` Cloud: AWS`);
394
- console.log(` Region: ${region}`);
395
- console.log(` Instance type: ${AWS_INSTANCE_TYPE}`);
396
- console.log("");
397
-
398
- if (name) {
399
- if (await instanceExistsByName(name, region)) {
400
- console.error(
401
- `Error: Instance name '${name}' is already taken. Please choose a different name.`,
402
- );
403
- process.exit(1);
404
- }
405
- } else {
406
- while (await instanceExistsByName(instanceName, region)) {
407
- console.log(
408
- `\u26a0\ufe0f Instance name ${instanceName} already exists, generating a new name...`,
409
- );
410
- instanceName = generateInstanceName(species);
411
- }
412
- }
413
-
414
- let sshUser: string;
415
- try {
416
- sshUser = userInfo().username;
417
- } catch {
418
- sshUser = process.env.USER ?? "";
419
- }
420
- if (!sshUser) {
421
- console.error(
422
- "Error: Could not determine SSH username. Set the USER environment variable and try again.",
423
- );
424
- process.exit(1);
425
- }
426
- const hatchedBy = process.env.VELLUM_HATCHED_BY;
427
- const providerApiKeys: Record<string, string> = {};
428
- for (const [, envVar] of Object.entries(PROVIDER_ENV_VAR_NAMES)) {
429
- const value = process.env[envVar];
430
- if (value) {
431
- providerApiKeys[envVar] = value;
432
- }
433
- }
434
- if (Object.keys(providerApiKeys).length === 0) {
435
- console.error(
436
- "Error: No provider API key environment variable is set. " +
437
- "Set at least one of: " +
438
- Object.values(PROVIDER_ENV_VAR_NAMES).join(", "),
439
- );
440
- process.exit(1);
441
- }
442
-
443
- const vpcId = await getDefaultVpcId(region);
444
-
445
- console.log("\u{1F512} Ensuring security group...");
446
- const securityGroupId = await ensureSecurityGroup(
447
- "vellum-assistant",
448
- vpcId,
449
- GATEWAY_PORT,
450
- region,
451
- );
452
-
453
- console.log("\u{1F511} Ensuring SSH key pair...");
454
- const keyPath = await ensureKeyPair(region);
455
-
456
- console.log("\u{1F50D} Finding latest Debian AMI...");
457
- const amiId = await getLatestDebianAmi(region);
458
-
459
- emitProgress(1, 5, "Preparing startup script...");
460
- const { script: startupScript, laptopBootstrapSecret } =
461
- await buildStartupScript(
462
- species,
463
- sshUser,
464
- providerApiKeys,
465
- instanceName,
466
- "aws",
467
- configValues,
468
- );
469
- const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
470
- writeFileSync(startupScriptPath, startupScript);
471
-
472
- emitProgress(2, 5, "Launching instance...");
473
- console.log("\u{1F528} Launching instance...");
474
- let instanceId: string;
475
- try {
476
- instanceId = await launchInstance(
477
- instanceName,
478
- amiId,
479
- AWS_INSTANCE_TYPE,
480
- securityGroupId,
481
- startupScriptPath,
482
- species,
483
- region,
484
- hatchedBy,
485
- );
486
- } finally {
487
- try {
488
- unlinkSync(startupScriptPath);
489
- } catch {}
490
- }
491
-
492
- console.log(`\u2705 Instance ${instanceName} (${instanceId}) launched\n`);
493
-
494
- console.log("\u23f3 Waiting for instance to be running...");
495
- await waitForInstanceRunning(instanceId, region);
496
-
497
- let externalIp: string | null = null;
498
- try {
499
- externalIp = await getInstancePublicIp(instanceId, region);
500
- } catch {
501
- console.log(
502
- "\u26a0\ufe0f Could not retrieve external IP yet (instance may still be starting)",
503
- );
504
- }
505
-
506
- const runtimeUrl = externalIp
507
- ? `http://${externalIp}:${GATEWAY_PORT}`
508
- : `http://${instanceName}:${GATEWAY_PORT}`;
509
- emitProgress(3, 5, "Saving configuration...");
510
- const awsEntry: AssistantEntry = {
511
- assistantId: instanceName,
512
- runtimeUrl,
513
- cloud: "aws",
514
- instanceId,
515
- region,
516
- species,
517
- sshUser,
518
- hatchedAt: new Date().toISOString(),
519
- };
520
- saveAssistantEntry(awsEntry);
521
- setActiveAssistant(instanceName);
522
-
523
- if (detached) {
524
- console.log("\u{1F680} Startup script is running on the instance...");
525
- console.log("");
526
- console.log("\u2705 Assistant is hatching!\n");
527
- console.log("Instance details:");
528
- console.log(` Name: ${instanceName}`);
529
- console.log(` Instance ID: ${instanceId}`);
530
- console.log(` Region: ${region}`);
531
- if (externalIp) {
532
- console.log(` External IP: ${externalIp}`);
533
- }
534
- console.log("");
535
- } else {
536
- console.log(" Press Ctrl+C to detach (instance will keep running)");
537
- console.log("");
538
-
539
- if (externalIp) {
540
- const ip = externalIp;
541
- emitProgress(4, 5, "Installing software...");
542
- const result = await watchHatching(
543
- () => pollAwsInstance(ip, keyPath),
544
- instanceName,
545
- startTime,
546
- species,
547
- );
548
-
549
- if (!result.success) {
550
- console.log("");
551
- if (result.errorContent) {
552
- console.log("📋 Startup error:");
553
- console.log(` ${result.errorContent}`);
554
- console.log("");
555
- }
556
- process.exit(1);
557
- }
558
-
559
- emitProgress(5, 5, "Finalizing...");
560
- try {
561
- await leaseGuardianToken(
562
- runtimeUrl,
563
- instanceName,
564
- laptopBootstrapSecret,
565
- );
566
- } catch (err) {
567
- console.warn(
568
- `\u26a0\ufe0f Could not lease guardian token: ${err instanceof Error ? err.message : err}`,
569
- );
570
- }
571
- } else {
572
- console.log(
573
- "\u26a0\ufe0f No external IP available for monitoring. Instance is still running.",
574
- );
575
- console.log(` Monitor with: vel logs ${instanceName}`);
576
- console.log("");
577
- }
578
-
579
- console.log("Instance details:");
580
- console.log(` Name: ${instanceName}`);
581
- console.log(` Instance ID: ${instanceId}`);
582
- console.log(` Region: ${region}`);
583
- if (externalIp) {
584
- console.log(` External IP: ${externalIp}`);
585
- }
586
- }
587
- } catch (error) {
588
- console.error(
589
- "\u274c Error:",
590
- error instanceof Error ? error.message : error,
591
- );
592
- process.exit(1);
593
- }
594
- }
595
2
 
596
3
  async function getInstanceIdByName(
597
4
  name: string,
@@ -18,8 +18,6 @@ export const DEFAULT_CES_PORT = 8090;
18
18
 
19
19
  export const VALID_REMOTE_HOSTS = [
20
20
  "local",
21
- "gcp",
22
- "aws",
23
21
  "docker",
24
22
  "custom",
25
23
  "vellum",