@vellumai/cli 0.5.16 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +46 -52
- package/package.json +1 -1
- package/src/__tests__/teleport.test.ts +444 -1
- package/src/commands/hatch.ts +14 -1
- package/src/commands/rollback.ts +6 -0
- package/src/commands/teleport.ts +168 -17
- package/src/commands/upgrade.ts +7 -0
- package/src/lib/aws.ts +2 -1
- package/src/lib/constants.ts +0 -11
- package/src/lib/docker.ts +27 -13
- package/src/lib/gcp.ts +2 -5
- package/src/lib/platform-client.ts +142 -8
- package/src/lib/upgrade-lifecycle.ts +8 -0
- package/src/shared/provider-env-vars.ts +19 -0
package/src/commands/teleport.ts
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
platformDownloadExport,
|
|
21
21
|
platformImportPreflight,
|
|
22
22
|
platformImportBundle,
|
|
23
|
+
platformRequestUploadUrl,
|
|
24
|
+
platformUploadToSignedUrl,
|
|
25
|
+
platformImportPreflightFromGcs,
|
|
26
|
+
platformImportBundleFromGcs,
|
|
23
27
|
} from "../lib/platform-client.js";
|
|
24
28
|
import {
|
|
25
29
|
hatchDocker,
|
|
@@ -628,6 +632,7 @@ async function importToAssistant(
|
|
|
628
632
|
cloud: string,
|
|
629
633
|
bundleData: Uint8Array<ArrayBuffer>,
|
|
630
634
|
dryRun: boolean,
|
|
635
|
+
preUploadedBundleKey?: string | null,
|
|
631
636
|
): Promise<void> {
|
|
632
637
|
if (cloud === "vellum") {
|
|
633
638
|
// Platform target
|
|
@@ -649,6 +654,32 @@ async function importToAssistant(
|
|
|
649
654
|
throw err;
|
|
650
655
|
}
|
|
651
656
|
|
|
657
|
+
// Use pre-uploaded bundle key if provided (string), skip upload if null
|
|
658
|
+
// (signals signed URLs were already tried and unavailable), or try
|
|
659
|
+
// signed-URL upload if undefined (never attempted).
|
|
660
|
+
let bundleKey: string | undefined =
|
|
661
|
+
preUploadedBundleKey === null ? undefined : preUploadedBundleKey;
|
|
662
|
+
if (preUploadedBundleKey === undefined) {
|
|
663
|
+
try {
|
|
664
|
+
const { uploadUrl, bundleKey: key } = await platformRequestUploadUrl(
|
|
665
|
+
token,
|
|
666
|
+
orgId,
|
|
667
|
+
entry.runtimeUrl,
|
|
668
|
+
);
|
|
669
|
+
bundleKey = key;
|
|
670
|
+
console.log("Uploading bundle...");
|
|
671
|
+
await platformUploadToSignedUrl(uploadUrl, bundleData);
|
|
672
|
+
} catch (err) {
|
|
673
|
+
// If signed uploads unavailable (503), fall back to inline upload
|
|
674
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
675
|
+
if (msg.includes("not available")) {
|
|
676
|
+
bundleKey = undefined;
|
|
677
|
+
} else {
|
|
678
|
+
throw err;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
652
683
|
if (dryRun) {
|
|
653
684
|
console.log("Running preflight analysis...\n");
|
|
654
685
|
|
|
@@ -657,12 +688,19 @@ async function importToAssistant(
|
|
|
657
688
|
body: Record<string, unknown>;
|
|
658
689
|
};
|
|
659
690
|
try {
|
|
660
|
-
preflightResult =
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
691
|
+
preflightResult = bundleKey
|
|
692
|
+
? await platformImportPreflightFromGcs(
|
|
693
|
+
bundleKey,
|
|
694
|
+
token,
|
|
695
|
+
orgId,
|
|
696
|
+
entry.runtimeUrl,
|
|
697
|
+
)
|
|
698
|
+
: await platformImportPreflight(
|
|
699
|
+
bundleData,
|
|
700
|
+
token,
|
|
701
|
+
orgId,
|
|
702
|
+
entry.runtimeUrl,
|
|
703
|
+
);
|
|
666
704
|
} catch (err) {
|
|
667
705
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
668
706
|
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
@@ -712,12 +750,19 @@ async function importToAssistant(
|
|
|
712
750
|
|
|
713
751
|
let importResult: { statusCode: number; body: Record<string, unknown> };
|
|
714
752
|
try {
|
|
715
|
-
importResult =
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
753
|
+
importResult = bundleKey
|
|
754
|
+
? await platformImportBundleFromGcs(
|
|
755
|
+
bundleKey,
|
|
756
|
+
token,
|
|
757
|
+
orgId,
|
|
758
|
+
entry.runtimeUrl,
|
|
759
|
+
)
|
|
760
|
+
: await platformImportBundle(
|
|
761
|
+
bundleData,
|
|
762
|
+
token,
|
|
763
|
+
orgId,
|
|
764
|
+
entry.runtimeUrl,
|
|
765
|
+
);
|
|
721
766
|
} catch (err) {
|
|
722
767
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
723
768
|
console.error("Error: Import request timed out after 2 minutes.");
|
|
@@ -751,6 +796,7 @@ async function importToAssistant(
|
|
|
751
796
|
export async function resolveOrHatchTarget(
|
|
752
797
|
targetEnv: "local" | "docker" | "platform",
|
|
753
798
|
targetName?: string,
|
|
799
|
+
orgId?: string,
|
|
754
800
|
): Promise<AssistantEntry> {
|
|
755
801
|
// If a name is provided, try to find an existing assistant
|
|
756
802
|
if (targetName) {
|
|
@@ -830,7 +876,25 @@ export async function resolveOrHatchTarget(
|
|
|
830
876
|
process.exit(1);
|
|
831
877
|
}
|
|
832
878
|
|
|
833
|
-
|
|
879
|
+
let resolvedOrgId: string;
|
|
880
|
+
if (orgId) {
|
|
881
|
+
resolvedOrgId = orgId;
|
|
882
|
+
} else {
|
|
883
|
+
try {
|
|
884
|
+
resolvedOrgId = await fetchOrganizationId(token);
|
|
885
|
+
} catch (err) {
|
|
886
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
887
|
+
if (msg.includes("401") || msg.includes("403")) {
|
|
888
|
+
console.error(
|
|
889
|
+
"Authentication failed. Run 'vellum login' to refresh.",
|
|
890
|
+
);
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
throw err;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const result = await hatchAssistant(token, resolvedOrgId);
|
|
834
898
|
const entry: AssistantEntry = {
|
|
835
899
|
assistantId: result.id,
|
|
836
900
|
runtimeUrl: getPlatformUrl(),
|
|
@@ -1080,10 +1144,19 @@ export async function teleport(): Promise<void> {
|
|
|
1080
1144
|
// No existing target — just describe what would happen
|
|
1081
1145
|
console.log("Dry run summary:");
|
|
1082
1146
|
console.log(` Would export data from: ${from} (${fromCloud})`);
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1147
|
+
if (targetEnv === "platform") {
|
|
1148
|
+
// For platform targets, reflect the reordered flow
|
|
1149
|
+
console.log(` Would upload bundle via signed URL (if available)`);
|
|
1150
|
+
console.log(
|
|
1151
|
+
` Would hatch a new ${targetEnv} assistant${targetName ? ` named '${targetName}'` : ""}`,
|
|
1152
|
+
);
|
|
1153
|
+
console.log(` Would import data into the new assistant`);
|
|
1154
|
+
} else {
|
|
1155
|
+
console.log(
|
|
1156
|
+
` Would hatch a new ${targetEnv} assistant${targetName ? ` named '${targetName}'` : ""}`,
|
|
1157
|
+
);
|
|
1158
|
+
console.log(` Would import data into the new assistant`);
|
|
1159
|
+
}
|
|
1087
1160
|
}
|
|
1088
1161
|
|
|
1089
1162
|
console.log(`Dry run complete — no changes were made.`);
|
|
@@ -1094,6 +1167,84 @@ export async function teleport(): Promise<void> {
|
|
|
1094
1167
|
console.log(`Exporting from ${from} (${fromCloud})...`);
|
|
1095
1168
|
const bundleData = await exportFromAssistant(fromEntry, fromCloud);
|
|
1096
1169
|
|
|
1170
|
+
// Platform target: reordered flow — upload to GCS before hatching so that
|
|
1171
|
+
// if upload fails, no empty assistant is left dangling on the platform.
|
|
1172
|
+
if (targetEnv === "platform") {
|
|
1173
|
+
// Step B — Auth + Org ID
|
|
1174
|
+
const token = readPlatformToken();
|
|
1175
|
+
if (!token) {
|
|
1176
|
+
console.error("Not logged in. Run 'vellum login' first.");
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// If targeting an existing assistant, validate cloud match early — before
|
|
1181
|
+
// uploading — so we don't waste a GCS upload on an invalid command.
|
|
1182
|
+
const existingTarget = targetName ? findAssistantByName(targetName) : null;
|
|
1183
|
+
if (existingTarget) {
|
|
1184
|
+
const existingCloud = resolveCloud(existingTarget);
|
|
1185
|
+
if (existingCloud !== "vellum") {
|
|
1186
|
+
console.error(
|
|
1187
|
+
`Error: Assistant '${targetName}' is a ${existingCloud} assistant, not platform. ` +
|
|
1188
|
+
`Use --${existingCloud} to target it.`,
|
|
1189
|
+
);
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Use the existing target's runtimeUrl for all platform calls so upload,
|
|
1195
|
+
// org ID fetch, and import hit the same instance.
|
|
1196
|
+
const targetPlatformUrl = existingTarget?.runtimeUrl;
|
|
1197
|
+
|
|
1198
|
+
let orgId: string;
|
|
1199
|
+
try {
|
|
1200
|
+
orgId = await fetchOrganizationId(token, targetPlatformUrl);
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1203
|
+
if (msg.includes("401") || msg.includes("403")) {
|
|
1204
|
+
console.error("Authentication failed. Run 'vellum login' to refresh.");
|
|
1205
|
+
process.exit(1);
|
|
1206
|
+
}
|
|
1207
|
+
throw err;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Step C — Upload to GCS
|
|
1211
|
+
// bundleKey: string = uploaded successfully, null = tried but unavailable,
|
|
1212
|
+
// undefined would mean "never tried" (not used here).
|
|
1213
|
+
let bundleKey: string | null = null;
|
|
1214
|
+
try {
|
|
1215
|
+
const { uploadUrl, bundleKey: key } = await platformRequestUploadUrl(
|
|
1216
|
+
token,
|
|
1217
|
+
orgId,
|
|
1218
|
+
targetPlatformUrl,
|
|
1219
|
+
);
|
|
1220
|
+
bundleKey = key;
|
|
1221
|
+
console.log("Uploading bundle to GCS...");
|
|
1222
|
+
await platformUploadToSignedUrl(uploadUrl, bundleData);
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
// If signed uploads unavailable (503), fall back to inline upload later
|
|
1225
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1226
|
+
if (msg.includes("not available")) {
|
|
1227
|
+
bundleKey = null;
|
|
1228
|
+
} else {
|
|
1229
|
+
throw err;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Step D — Hatch (upload succeeded or fallback to inline — safe to hatch)
|
|
1234
|
+
const toEntry = await resolveOrHatchTarget(targetEnv, targetName, orgId);
|
|
1235
|
+
const toCloud = resolveCloud(toEntry);
|
|
1236
|
+
|
|
1237
|
+
// Step E — Import from GCS (or inline fallback)
|
|
1238
|
+
// Pass bundleKey (string) or null to signal "already tried, use inline".
|
|
1239
|
+
console.log(`Importing to ${toEntry.assistantId} (${toCloud})...`);
|
|
1240
|
+
await importToAssistant(toEntry, toCloud, bundleData, false, bundleKey);
|
|
1241
|
+
|
|
1242
|
+
// Success summary
|
|
1243
|
+
console.log(`Teleport complete: ${from} → ${toEntry.assistantId}`);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Non-platform targets (local/docker): existing flow unchanged
|
|
1097
1248
|
// For local<->docker transfers, stop (sleep) the source to free up ports
|
|
1098
1249
|
// before hatching the target. We do NOT retire yet — if hatch or import
|
|
1099
1250
|
// fails, the user can recover by running `vellum wake <source>`.
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -291,6 +291,11 @@ async function upgradeDocker(
|
|
|
291
291
|
` Captured ${Object.keys(capturedEnv).length} env var(s) from ${res.assistantContainer}\n`,
|
|
292
292
|
);
|
|
293
293
|
|
|
294
|
+
// Capture GUARDIAN_BOOTSTRAP_SECRET from the gateway container (it is only
|
|
295
|
+
// set on gateway, not assistant) so it persists across container restarts.
|
|
296
|
+
const gatewayEnv = await captureContainerEnv(res.gatewayContainer);
|
|
297
|
+
const bootstrapSecret = gatewayEnv["GUARDIAN_BOOTSTRAP_SECRET"];
|
|
298
|
+
|
|
294
299
|
// Notify connected clients that an upgrade is about to begin.
|
|
295
300
|
// This must fire BEFORE any progress broadcasts so the UI sets
|
|
296
301
|
// isUpdateInProgress = true and starts displaying status messages.
|
|
@@ -419,6 +424,7 @@ async function upgradeDocker(
|
|
|
419
424
|
await startContainers(
|
|
420
425
|
{
|
|
421
426
|
signingKey,
|
|
427
|
+
bootstrapSecret,
|
|
422
428
|
cesServiceToken,
|
|
423
429
|
extraAssistantEnv,
|
|
424
430
|
gatewayPort,
|
|
@@ -517,6 +523,7 @@ async function upgradeDocker(
|
|
|
517
523
|
await startContainers(
|
|
518
524
|
{
|
|
519
525
|
signingKey,
|
|
526
|
+
bootstrapSecret,
|
|
520
527
|
cesServiceToken,
|
|
521
528
|
extraAssistantEnv,
|
|
522
529
|
gatewayPort,
|
package/src/lib/aws.ts
CHANGED
|
@@ -6,7 +6,8 @@ import { buildStartupScript, watchHatching } from "../commands/hatch";
|
|
|
6
6
|
import type { PollResult } from "../commands/hatch";
|
|
7
7
|
import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
|
|
8
8
|
import type { AssistantEntry } from "./assistant-config";
|
|
9
|
-
import { GATEWAY_PORT
|
|
9
|
+
import { GATEWAY_PORT } from "./constants";
|
|
10
|
+
import { PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
|
|
10
11
|
import type { Species } from "./constants";
|
|
11
12
|
import { leaseGuardianToken } from "./guardian-token";
|
|
12
13
|
import { generateInstanceName } from "./random-name";
|
package/src/lib/constants.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import providerEnvVarsRegistry from "../../../meta/provider-env-vars.json";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Canonical internal assistant ID used as the default/fallback across the CLI
|
|
5
3
|
* and daemon. Mirrors `DAEMON_INTERNAL_ASSISTANT_ID` from
|
|
@@ -28,15 +26,6 @@ export const LOCKFILE_NAMES = [
|
|
|
28
26
|
".vellum.lockfile.json",
|
|
29
27
|
] as const;
|
|
30
28
|
|
|
31
|
-
/**
|
|
32
|
-
* Environment variable names for provider API keys, keyed by provider ID.
|
|
33
|
-
* Loaded from the shared registry at `meta/provider-env-vars.json` — the
|
|
34
|
-
* single source of truth also consumed by the assistant runtime and the
|
|
35
|
-
* macOS client.
|
|
36
|
-
*/
|
|
37
|
-
export const PROVIDER_ENV_VAR_NAMES: Record<string, string> =
|
|
38
|
-
providerEnvVarsRegistry.providers;
|
|
39
|
-
|
|
40
29
|
export const VALID_REMOTE_HOSTS = [
|
|
41
30
|
"local",
|
|
42
31
|
"gcp",
|
package/src/lib/docker.ts
CHANGED
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
} from "./assistant-config";
|
|
14
14
|
import type { AssistantEntry } from "./assistant-config";
|
|
15
15
|
import { writeInitialConfig } from "./config-utils";
|
|
16
|
-
import { DEFAULT_GATEWAY_PORT
|
|
16
|
+
import { DEFAULT_GATEWAY_PORT } from "./constants";
|
|
17
|
+
import { PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
|
|
17
18
|
import type { Species } from "./constants";
|
|
18
19
|
import { leaseGuardianToken } from "./guardian-token";
|
|
19
20
|
import { isVellumProcess, stopProcess } from "./process";
|
|
@@ -479,8 +480,9 @@ async function buildAllImages(
|
|
|
479
480
|
|
|
480
481
|
/**
|
|
481
482
|
* Returns a function that builds the `docker run` arguments for a given
|
|
482
|
-
* service.
|
|
483
|
-
*
|
|
483
|
+
* service. All three containers share a network namespace via
|
|
484
|
+
* `--network=container:` so inter-service traffic is over localhost,
|
|
485
|
+
* matching the platform's Kubernetes pod topology.
|
|
484
486
|
*/
|
|
485
487
|
export function serviceDockerRunArgs(opts: {
|
|
486
488
|
signingKey?: string;
|
|
@@ -511,12 +513,14 @@ export function serviceDockerRunArgs(opts: {
|
|
|
511
513
|
"--name",
|
|
512
514
|
res.assistantContainer,
|
|
513
515
|
`--network=${res.network}`,
|
|
516
|
+
"-p",
|
|
517
|
+
`${gatewayPort}:${GATEWAY_INTERNAL_PORT}`,
|
|
514
518
|
"-v",
|
|
515
519
|
`${res.workspaceVolume}:/workspace`,
|
|
516
520
|
"-v",
|
|
517
521
|
`${res.socketVolume}:/run/ces-bootstrap`,
|
|
518
522
|
"-e",
|
|
519
|
-
"IS_CONTAINERIZED=
|
|
523
|
+
"IS_CONTAINERIZED=true",
|
|
520
524
|
"-e",
|
|
521
525
|
`VELLUM_ASSISTANT_NAME=${instanceName}`,
|
|
522
526
|
"-e",
|
|
@@ -526,9 +530,9 @@ export function serviceDockerRunArgs(opts: {
|
|
|
526
530
|
"-e",
|
|
527
531
|
"VELLUM_WORKSPACE_DIR=/workspace",
|
|
528
532
|
"-e",
|
|
529
|
-
|
|
533
|
+
"CES_CREDENTIAL_URL=http://localhost:8090",
|
|
530
534
|
"-e",
|
|
531
|
-
`GATEWAY_INTERNAL_URL=http
|
|
535
|
+
`GATEWAY_INTERNAL_URL=http://localhost:${GATEWAY_INTERNAL_PORT}`,
|
|
532
536
|
];
|
|
533
537
|
if (defaultWorkspaceConfigPath) {
|
|
534
538
|
const containerPath = `/tmp/vellum-default-workspace-config-${Date.now()}.json`;
|
|
@@ -567,9 +571,7 @@ export function serviceDockerRunArgs(opts: {
|
|
|
567
571
|
"-d",
|
|
568
572
|
"--name",
|
|
569
573
|
res.gatewayContainer,
|
|
570
|
-
`--network
|
|
571
|
-
"-p",
|
|
572
|
-
`${gatewayPort}:${GATEWAY_INTERNAL_PORT}`,
|
|
574
|
+
`--network=container:${res.assistantContainer}`,
|
|
573
575
|
"-v",
|
|
574
576
|
`${res.workspaceVolume}:/workspace`,
|
|
575
577
|
"-v",
|
|
@@ -581,13 +583,13 @@ export function serviceDockerRunArgs(opts: {
|
|
|
581
583
|
"-e",
|
|
582
584
|
`GATEWAY_PORT=${GATEWAY_INTERNAL_PORT}`,
|
|
583
585
|
"-e",
|
|
584
|
-
|
|
586
|
+
"ASSISTANT_HOST=localhost",
|
|
585
587
|
"-e",
|
|
586
588
|
`RUNTIME_HTTP_PORT=${ASSISTANT_INTERNAL_PORT}`,
|
|
587
589
|
"-e",
|
|
588
590
|
"RUNTIME_PROXY_ENABLED=true",
|
|
589
591
|
"-e",
|
|
590
|
-
|
|
592
|
+
"CES_CREDENTIAL_URL=http://localhost:8090",
|
|
591
593
|
...(cesServiceToken
|
|
592
594
|
? ["-e", `CES_SERVICE_TOKEN=${cesServiceToken}`]
|
|
593
595
|
: []),
|
|
@@ -605,7 +607,7 @@ export function serviceDockerRunArgs(opts: {
|
|
|
605
607
|
"-d",
|
|
606
608
|
"--name",
|
|
607
609
|
res.cesContainer,
|
|
608
|
-
`--network
|
|
610
|
+
`--network=container:${res.assistantContainer}`,
|
|
609
611
|
"-v",
|
|
610
612
|
`${res.socketVolume}:/run/ces-bootstrap`,
|
|
611
613
|
"-v",
|
|
@@ -842,6 +844,15 @@ function startFileWatcher(opts: {
|
|
|
842
844
|
const services = pendingServices;
|
|
843
845
|
pendingServices = new Set();
|
|
844
846
|
|
|
847
|
+
// Gateway and CES share the assistant's network namespace. If the
|
|
848
|
+
// assistant container is removed and recreated, the shared namespace
|
|
849
|
+
// is destroyed and the other two lose connectivity. Cascade the
|
|
850
|
+
// restart to all three services in that case.
|
|
851
|
+
if (services.has("assistant")) {
|
|
852
|
+
services.add("gateway");
|
|
853
|
+
services.add("credential-executor");
|
|
854
|
+
}
|
|
855
|
+
|
|
845
856
|
const serviceNames = [...services].join(", ");
|
|
846
857
|
console.log(`\n🔄 Changes detected — rebuilding: ${serviceNames}`);
|
|
847
858
|
|
|
@@ -854,7 +865,10 @@ function startFileWatcher(opts: {
|
|
|
854
865
|
}),
|
|
855
866
|
);
|
|
856
867
|
|
|
857
|
-
|
|
868
|
+
// Restart in dependency order (assistant first) so the network
|
|
869
|
+
// namespace owner is up before dependents try to attach.
|
|
870
|
+
for (const service of SERVICE_START_ORDER) {
|
|
871
|
+
if (!services.has(service)) continue;
|
|
858
872
|
const container = containerForService[service];
|
|
859
873
|
console.log(`🔄 Restarting ${container}...`);
|
|
860
874
|
await removeContainer(container);
|
package/src/lib/gcp.ts
CHANGED
|
@@ -4,11 +4,8 @@ import { join } from "path";
|
|
|
4
4
|
|
|
5
5
|
import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
|
|
6
6
|
import type { AssistantEntry } from "./assistant-config";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
GATEWAY_PORT,
|
|
10
|
-
PROVIDER_ENV_VAR_NAMES,
|
|
11
|
-
} from "./constants";
|
|
7
|
+
import { FIREWALL_TAG, GATEWAY_PORT } from "./constants";
|
|
8
|
+
import { PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
|
|
12
9
|
import type { Species } from "./constants";
|
|
13
10
|
import { leaseGuardianToken } from "./guardian-token";
|
|
14
11
|
import { getPlatformUrl } from "./platform-client";
|
|
@@ -87,14 +87,20 @@ export interface HatchedAssistant {
|
|
|
87
87
|
status: string;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
export async function hatchAssistant(
|
|
91
|
-
|
|
90
|
+
export async function hatchAssistant(
|
|
91
|
+
token: string,
|
|
92
|
+
orgId: string,
|
|
93
|
+
platformUrl?: string,
|
|
94
|
+
): Promise<HatchedAssistant> {
|
|
95
|
+
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
96
|
+
const url = `${resolvedUrl}/v1/assistants/hatch/`;
|
|
92
97
|
|
|
93
98
|
const response = await fetch(url, {
|
|
94
99
|
method: "POST",
|
|
95
100
|
headers: {
|
|
96
101
|
"Content-Type": "application/json",
|
|
97
102
|
...authHeaders(token),
|
|
103
|
+
"Vellum-Organization-Id": orgId,
|
|
98
104
|
},
|
|
99
105
|
body: JSON.stringify({}),
|
|
100
106
|
});
|
|
@@ -143,7 +149,7 @@ export async function fetchOrganizationId(
|
|
|
143
149
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
144
150
|
const url = `${resolvedUrl}/v1/organizations/`;
|
|
145
151
|
const response = await fetch(url, {
|
|
146
|
-
headers: {
|
|
152
|
+
headers: { ...authHeaders(token) },
|
|
147
153
|
});
|
|
148
154
|
|
|
149
155
|
if (!response.ok) {
|
|
@@ -213,7 +219,7 @@ export async function rollbackPlatformAssistant(
|
|
|
213
219
|
method: "POST",
|
|
214
220
|
headers: {
|
|
215
221
|
"Content-Type": "application/json",
|
|
216
|
-
|
|
222
|
+
...authHeaders(token),
|
|
217
223
|
"Vellum-Organization-Id": orgId,
|
|
218
224
|
},
|
|
219
225
|
body: JSON.stringify(version ? { version } : {}),
|
|
@@ -258,7 +264,7 @@ export async function platformInitiateExport(
|
|
|
258
264
|
method: "POST",
|
|
259
265
|
headers: {
|
|
260
266
|
"Content-Type": "application/json",
|
|
261
|
-
|
|
267
|
+
...authHeaders(token),
|
|
262
268
|
"Vellum-Organization-Id": orgId,
|
|
263
269
|
},
|
|
264
270
|
body: JSON.stringify({ description: description ?? "CLI backup" }),
|
|
@@ -292,7 +298,7 @@ export async function platformPollExportStatus(
|
|
|
292
298
|
`${resolvedUrl}/v1/migrations/export/${jobId}/status/`,
|
|
293
299
|
{
|
|
294
300
|
headers: {
|
|
295
|
-
|
|
301
|
+
...authHeaders(token),
|
|
296
302
|
"Vellum-Organization-Id": orgId,
|
|
297
303
|
},
|
|
298
304
|
},
|
|
@@ -349,7 +355,7 @@ export async function platformImportPreflight(
|
|
|
349
355
|
method: "POST",
|
|
350
356
|
headers: {
|
|
351
357
|
"Content-Type": "application/octet-stream",
|
|
352
|
-
|
|
358
|
+
...authHeaders(token),
|
|
353
359
|
"Vellum-Organization-Id": orgId,
|
|
354
360
|
},
|
|
355
361
|
body: new Blob([bundleData]),
|
|
@@ -375,7 +381,7 @@ export async function platformImportBundle(
|
|
|
375
381
|
method: "POST",
|
|
376
382
|
headers: {
|
|
377
383
|
"Content-Type": "application/octet-stream",
|
|
378
|
-
|
|
384
|
+
...authHeaders(token),
|
|
379
385
|
"Vellum-Organization-Id": orgId,
|
|
380
386
|
},
|
|
381
387
|
body: new Blob([bundleData]),
|
|
@@ -388,3 +394,131 @@ export async function platformImportBundle(
|
|
|
388
394
|
>;
|
|
389
395
|
return { statusCode: response.status, body };
|
|
390
396
|
}
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Signed-URL upload flow
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
export async function platformRequestUploadUrl(
|
|
403
|
+
token: string,
|
|
404
|
+
orgId: string,
|
|
405
|
+
platformUrl?: string,
|
|
406
|
+
): Promise<{ uploadUrl: string; bundleKey: string; expiresAt: string }> {
|
|
407
|
+
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
408
|
+
const response = await fetch(`${resolvedUrl}/v1/migrations/upload-url/`, {
|
|
409
|
+
method: "POST",
|
|
410
|
+
headers: {
|
|
411
|
+
"Content-Type": "application/json",
|
|
412
|
+
...authHeaders(token),
|
|
413
|
+
"Vellum-Organization-Id": orgId,
|
|
414
|
+
},
|
|
415
|
+
body: JSON.stringify({ content_type: "application/octet-stream" }),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (response.status === 201) {
|
|
419
|
+
const body = (await response.json()) as {
|
|
420
|
+
upload_url: string;
|
|
421
|
+
bundle_key: string;
|
|
422
|
+
expires_at: string;
|
|
423
|
+
};
|
|
424
|
+
return {
|
|
425
|
+
uploadUrl: body.upload_url,
|
|
426
|
+
bundleKey: body.bundle_key,
|
|
427
|
+
expiresAt: body.expires_at,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (response.status === 404 || response.status === 503) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
"Signed uploads are not available on this platform instance",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const errorBody = (await response.json().catch(() => ({}))) as {
|
|
438
|
+
detail?: string;
|
|
439
|
+
};
|
|
440
|
+
throw new Error(
|
|
441
|
+
errorBody.detail ??
|
|
442
|
+
`Failed to request upload URL: ${response.status} ${response.statusText}`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export async function platformUploadToSignedUrl(
|
|
447
|
+
uploadUrl: string,
|
|
448
|
+
bundleData: Uint8Array<ArrayBuffer>,
|
|
449
|
+
): Promise<void> {
|
|
450
|
+
const response = await fetch(uploadUrl, {
|
|
451
|
+
method: "PUT",
|
|
452
|
+
headers: {
|
|
453
|
+
"Content-Type": "application/octet-stream",
|
|
454
|
+
},
|
|
455
|
+
body: new Blob([bundleData]),
|
|
456
|
+
signal: AbortSignal.timeout(600_000),
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (!response.ok) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`Upload to signed URL failed: ${response.status} ${response.statusText}`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export async function platformImportPreflightFromGcs(
|
|
467
|
+
bundleKey: string,
|
|
468
|
+
token: string,
|
|
469
|
+
orgId: string,
|
|
470
|
+
platformUrl?: string,
|
|
471
|
+
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
472
|
+
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
473
|
+
const response = await fetch(
|
|
474
|
+
`${resolvedUrl}/v1/migrations/import-preflight-from-gcs/`,
|
|
475
|
+
{
|
|
476
|
+
method: "POST",
|
|
477
|
+
headers: {
|
|
478
|
+
"Content-Type": "application/json",
|
|
479
|
+
...authHeaders(token),
|
|
480
|
+
"Vellum-Organization-Id": orgId,
|
|
481
|
+
},
|
|
482
|
+
body: JSON.stringify({ bundle_key: bundleKey }),
|
|
483
|
+
signal: AbortSignal.timeout(120_000),
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
const body = (await response.json().catch(() => ({}))) as Record<
|
|
488
|
+
string,
|
|
489
|
+
unknown
|
|
490
|
+
>;
|
|
491
|
+
return { statusCode: response.status, body };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export async function platformImportBundleFromGcs(
|
|
495
|
+
bundleKey: string,
|
|
496
|
+
token: string,
|
|
497
|
+
orgId: string,
|
|
498
|
+
platformUrl?: string,
|
|
499
|
+
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
500
|
+
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
501
|
+
const response = await fetch(
|
|
502
|
+
`${resolvedUrl}/v1/migrations/import-from-gcs/`,
|
|
503
|
+
{
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: {
|
|
506
|
+
"Content-Type": "application/json",
|
|
507
|
+
...authHeaders(token),
|
|
508
|
+
"Vellum-Organization-Id": orgId,
|
|
509
|
+
},
|
|
510
|
+
body: JSON.stringify({ bundle_key: bundleKey }),
|
|
511
|
+
signal: AbortSignal.timeout(120_000),
|
|
512
|
+
},
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
if (response.status === 413) {
|
|
516
|
+
throw new Error("Bundle too large to import");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const body = (await response.json().catch(() => ({}))) as Record<
|
|
520
|
+
string,
|
|
521
|
+
unknown
|
|
522
|
+
>;
|
|
523
|
+
return { statusCode: response.status, body };
|
|
524
|
+
}
|
|
@@ -90,6 +90,7 @@ export function buildUpgradeCommitMessage(options: {
|
|
|
90
90
|
*/
|
|
91
91
|
export const CONTAINER_ENV_EXCLUDE_KEYS: ReadonlySet<string> = new Set([
|
|
92
92
|
"CES_SERVICE_TOKEN",
|
|
93
|
+
"GUARDIAN_BOOTSTRAP_SECRET",
|
|
93
94
|
"VELLUM_ASSISTANT_NAME",
|
|
94
95
|
"RUNTIME_HTTP_HOST",
|
|
95
96
|
"PATH",
|
|
@@ -467,6 +468,11 @@ export async function performDockerRollback(
|
|
|
467
468
|
` Captured ${Object.keys(capturedEnv).length} env var(s) from ${res.assistantContainer}\n`,
|
|
468
469
|
);
|
|
469
470
|
|
|
471
|
+
// Capture GUARDIAN_BOOTSTRAP_SECRET from the gateway container (it is only
|
|
472
|
+
// set on gateway, not assistant) so it persists across container restarts.
|
|
473
|
+
const gatewayEnv = await captureContainerEnv(res.gatewayContainer);
|
|
474
|
+
const bootstrapSecret = gatewayEnv["GUARDIAN_BOOTSTRAP_SECRET"];
|
|
475
|
+
|
|
470
476
|
const cesServiceToken =
|
|
471
477
|
capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
|
|
472
478
|
|
|
@@ -575,6 +581,7 @@ export async function performDockerRollback(
|
|
|
575
581
|
await startContainers(
|
|
576
582
|
{
|
|
577
583
|
signingKey,
|
|
584
|
+
bootstrapSecret,
|
|
578
585
|
cesServiceToken,
|
|
579
586
|
extraAssistantEnv,
|
|
580
587
|
gatewayPort,
|
|
@@ -695,6 +702,7 @@ export async function performDockerRollback(
|
|
|
695
702
|
await startContainers(
|
|
696
703
|
{
|
|
697
704
|
signingKey,
|
|
705
|
+
bootstrapSecret,
|
|
698
706
|
cesServiceToken,
|
|
699
707
|
extraAssistantEnv,
|
|
700
708
|
gatewayPort,
|