@vellumai/cli 0.7.0 → 0.7.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 (54) hide show
  1. package/AGENTS.md +3 -11
  2. package/README.md +49 -0
  3. package/bun.lock +0 -15
  4. package/package.json +1 -6
  5. package/src/__tests__/backup.test.ts +591 -0
  6. package/src/__tests__/config-utils.test.ts +35 -48
  7. package/src/__tests__/teleport.test.ts +597 -37
  8. package/src/commands/backup.ts +149 -70
  9. package/src/commands/client.ts +56 -14
  10. package/src/commands/events.ts +3 -0
  11. package/src/commands/exec.ts +34 -12
  12. package/src/commands/hatch.ts +3 -7
  13. package/src/commands/login.ts +15 -33
  14. package/src/commands/logs.ts +2 -7
  15. package/src/commands/ps.ts +41 -6
  16. package/src/commands/restore.ts +32 -47
  17. package/src/commands/setup.ts +38 -73
  18. package/src/commands/ssh.ts +2 -5
  19. package/src/commands/teleport.ts +148 -34
  20. package/src/commands/tunnel.ts +2 -7
  21. package/src/commands/upgrade.ts +114 -7
  22. package/src/commands/wake.ts +5 -16
  23. package/src/components/DefaultMainScreen.tsx +65 -129
  24. package/src/index.ts +2 -13
  25. package/src/lib/__tests__/docker.test.ts +50 -32
  26. package/src/lib/__tests__/local-runtime-client.test.ts +308 -25
  27. package/src/lib/__tests__/platform-client-signed-url.test.ts +237 -2
  28. package/src/lib/__tests__/runtime-url.test.ts +125 -0
  29. package/src/lib/__tests__/terminal-session.test.ts +202 -0
  30. package/src/lib/assistant-client.ts +18 -26
  31. package/src/lib/assistant-config.ts +34 -41
  32. package/src/lib/backup-ops.ts +43 -17
  33. package/src/lib/cli-error.ts +1 -0
  34. package/src/lib/client-identity.ts +1 -1
  35. package/src/lib/config-utils.ts +1 -97
  36. package/src/lib/docker-statefulset.ts +381 -0
  37. package/src/lib/docker.ts +8 -247
  38. package/src/lib/guardian-token.ts +56 -6
  39. package/src/lib/hatch-local.ts +3 -26
  40. package/src/lib/job-polling.ts +1 -1
  41. package/src/lib/local-runtime-client.ts +162 -28
  42. package/src/lib/local.ts +35 -64
  43. package/src/lib/ngrok.ts +36 -26
  44. package/src/lib/platform-client.ts +97 -221
  45. package/src/lib/platform-releases.ts +23 -0
  46. package/src/lib/retire-local.ts +2 -2
  47. package/src/lib/runtime-url.ts +52 -0
  48. package/src/lib/sync-cloud-assistants.ts +126 -0
  49. package/src/lib/terminal-client.ts +6 -1
  50. package/src/lib/terminal-session.ts +127 -48
  51. package/src/lib/tui-log.ts +60 -0
  52. package/src/lib/upgrade-lifecycle.ts +65 -0
  53. package/src/lib/xdg-log.ts +10 -4
  54. package/src/commands/pair.ts +0 -212
@@ -91,14 +91,6 @@ const hatchAssistantMock = spyOn(
91
91
  reusedExisting: false,
92
92
  });
93
93
 
94
- const platformInitiateExportMock = spyOn(
95
- platformClient,
96
- "platformInitiateExport",
97
- ).mockResolvedValue({
98
- jobId: "platform-export-job-1",
99
- status: "pending",
100
- });
101
-
102
94
  const platformPollJobStatusMock = spyOn(
103
95
  platformClient,
104
96
  "platformPollJobStatus",
@@ -204,6 +196,15 @@ const localRuntimeImportFromGcsMock = spyOn(
204
196
  "localRuntimeImportFromGcs",
205
197
  ).mockResolvedValue({ jobId: "local-import-job-1" });
206
198
 
199
+ // Default to a fixed version string. Tests that exercise the version-gate
200
+ // surface override this mock per-case to assert the value flows from the
201
+ // target runtime's `/v1/identity` (NOT from `cliPkg.version`) into the
202
+ // download signed-URL request.
203
+ const localRuntimeIdentityMock = spyOn(
204
+ localRuntimeClient,
205
+ "localRuntimeIdentity",
206
+ ).mockResolvedValue({ version: "0.6.5" });
207
+
207
208
  const localRuntimePollJobStatusMock = spyOn(
208
209
  localRuntimeClient,
209
210
  "localRuntimePollJobStatus",
@@ -294,7 +295,6 @@ afterAll(() => {
294
295
  getPlatformUrlMock.mockRestore();
295
296
  hatchAssistantMock.mockRestore();
296
297
  checkExistingPlatformAssistantMock.mockRestore();
297
- platformInitiateExportMock.mockRestore();
298
298
  platformPollJobStatusMock.mockRestore();
299
299
  platformRequestSignedUrlMock.mockRestore();
300
300
  platformImportBundleFromGcsMock.mockRestore();
@@ -306,6 +306,7 @@ afterAll(() => {
306
306
  computeDeviceIdMock.mockRestore();
307
307
  localRuntimeExportToGcsMock.mockRestore();
308
308
  localRuntimeImportFromGcsMock.mockRestore();
309
+ localRuntimeIdentityMock.mockRestore();
309
310
  localRuntimePollJobStatusMock.mockRestore();
310
311
  rmSync(testDir, { recursive: true, force: true });
311
312
  delete process.env.VELLUM_LOCKFILE_DIR;
@@ -319,7 +320,7 @@ let consoleErrorSpy: ReturnType<typeof spyOn>;
319
320
  let fetchCalls: Array<{ url: string; body: unknown }>;
320
321
 
321
322
  function defaultLocalRuntimePollImpl(
322
- _runtimeUrl: string,
323
+ _entry: unknown,
323
324
  _token: string,
324
325
  jobId: string,
325
326
  ): Promise<{
@@ -378,11 +379,6 @@ beforeEach(() => {
378
379
  },
379
380
  reusedExisting: false,
380
381
  });
381
- platformInitiateExportMock.mockReset();
382
- platformInitiateExportMock.mockResolvedValue({
383
- jobId: "platform-export-job-1",
384
- status: "pending",
385
- });
386
382
  platformPollJobStatusMock.mockReset();
387
383
  platformPollJobStatusMock.mockResolvedValue({
388
384
  jobId: "platform-job-1",
@@ -460,6 +456,8 @@ beforeEach(() => {
460
456
  localRuntimeImportFromGcsMock.mockResolvedValue({
461
457
  jobId: "local-import-job-1",
462
458
  });
459
+ localRuntimeIdentityMock.mockReset();
460
+ localRuntimeIdentityMock.mockResolvedValue({ version: "0.6.5" });
463
461
  localRuntimePollJobStatusMock.mockReset();
464
462
  localRuntimePollJobStatusMock.mockImplementation(defaultLocalRuntimePollImpl);
465
463
 
@@ -864,14 +862,23 @@ describe("unified GCS flow — four directions", () => {
864
862
  // Signed-URL request for upload — pinned to the platform target's URL
865
863
  // so upload and download land on the same platform.
866
864
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
867
- expect.objectContaining({ operation: "upload" }),
865
+ expect.objectContaining({
866
+ operation: "upload",
867
+ minRuntimeVersion: "0.6.5",
868
+ maxRuntimeVersion: null,
869
+ }),
868
870
  "platform-token",
869
871
  "https://platform.vellum.ai",
870
872
  );
871
873
 
872
- // Runtime export-to-gcs kicked off with the signed upload URL
874
+ // Runtime export-to-gcs kicked off with the signed upload URL.
875
+ // Helper takes an entry, not a bare URL — the entry's cloud drives
876
+ // URL construction (local → gateway loopback path).
873
877
  expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
874
- "http://localhost:7821",
878
+ expect.objectContaining({
879
+ cloud: "local",
880
+ runtimeUrl: "http://localhost:7821",
881
+ }),
875
882
  "local-token",
876
883
  expect.objectContaining({
877
884
  uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
@@ -926,30 +933,92 @@ describe("unified GCS flow — four directions", () => {
926
933
  bundleKey: "platform-exports/org-1/bundle-abc.vbundle",
927
934
  });
928
935
 
936
+ // The bundle key now flows from the upload signed-URL request rather than
937
+ // the job-status payload — pin it so the download-URL assertion below
938
+ // still uses the same expected key.
939
+ platformRequestSignedUrlMock.mockImplementation(async (params) => ({
940
+ url:
941
+ params.operation === "upload"
942
+ ? "https://storage.googleapis.com/bucket/signed-upload"
943
+ : "https://storage.googleapis.com/bucket/signed-download",
944
+ bundleKey:
945
+ params.bundleKey ?? "platform-exports/org-1/bundle-abc.vbundle",
946
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
947
+ }));
948
+
929
949
  const restoreFetch = installTrackingFetch();
930
950
  try {
931
951
  await teleport();
932
952
 
933
- // Platform side: initiated server export and polled the unified status.
934
- expect(platformInitiateExportMock).toHaveBeenCalled();
935
- expect(platformPollJobStatusMock).toHaveBeenCalled();
953
+ // Platform side: requested an upload URL, kicked off a runtime export to
954
+ // GCS, and polled the unified job status.
955
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
956
+ expect.objectContaining({
957
+ operation: "upload",
958
+ minRuntimeVersion: "0.6.5",
959
+ maxRuntimeVersion: null,
960
+ }),
961
+ "platform-token",
962
+ "https://platform.vellum.ai",
963
+ );
964
+ // For platform sources, export-to-gcs is reached via the platform's
965
+ // wildcard runtime proxy. The helper builds the assistant-scoped URL
966
+ // from the entry (`/v1/assistants/<id>/migrations/export-to-gcs`) and
967
+ // sends platform-token auth — no guardian-token bootstrap.
968
+ expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
969
+ expect.objectContaining({
970
+ cloud: "vellum",
971
+ runtimeUrl: "https://platform.vellum.ai",
972
+ assistantId: "my-platform",
973
+ }),
974
+ "platform-token",
975
+ expect.objectContaining({
976
+ uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
977
+ description: "teleport export",
978
+ }),
979
+ );
980
+ // Polling for platform sources also goes through the wildcard via
981
+ // localRuntimePollJobStatus(entry, ...) — the dedicated
982
+ // `/v1/migrations/jobs/{id}/` endpoint queries platform-side
983
+ // ImportJob records and would 404 on runtime-created job IDs.
984
+ expect(localRuntimePollJobStatusMock).toHaveBeenCalledWith(
985
+ expect.objectContaining({
986
+ cloud: "vellum",
987
+ runtimeUrl: "https://platform.vellum.ai",
988
+ }),
989
+ "platform-token",
990
+ "local-export-job-1",
991
+ );
936
992
 
937
993
  // For the local target we request a download URL keyed by the
938
994
  // platform's bundle_key. The URL must target the SOURCE platform
939
995
  // (where the bundle was written) — pinned so a lockfile change
940
996
  // can't split upload and download across instances.
997
+ //
998
+ // `targetRuntimeVersion` MUST come from the target runtime's
999
+ // `/v1/identity` (mocked to "0.6.5"), NOT from the CLI's package
1000
+ // version. The local target's daemon can be on a different version
1001
+ // than the CLI orchestrating the teleport.
941
1002
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
942
- {
1003
+ expect.objectContaining({
943
1004
  operation: "download",
944
1005
  bundleKey: "platform-exports/org-1/bundle-abc.vbundle",
945
- },
1006
+ targetRuntimeVersion: "0.6.5",
1007
+ }),
946
1008
  "platform-token",
947
1009
  "https://platform.vellum.ai",
948
1010
  );
1011
+ expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
1012
+ expect.objectContaining({ cloud: "local" }),
1013
+ expect.any(String),
1014
+ );
949
1015
 
950
1016
  // Runtime import-from-gcs was kicked off with that URL.
951
1017
  expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
952
- "http://localhost:7821",
1018
+ expect.objectContaining({
1019
+ cloud: "local",
1020
+ runtimeUrl: "http://localhost:7821",
1021
+ }),
953
1022
  "local-token",
954
1023
  expect.objectContaining({
955
1024
  bundleUrl: "https://storage.googleapis.com/bucket/signed-download",
@@ -993,23 +1062,37 @@ describe("unified GCS flow — four directions", () => {
993
1062
  // lives in one place end-to-end. For local→docker neither side is
994
1063
  // platform, so we default to getPlatformUrl() (resolved once).
995
1064
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
996
- expect.objectContaining({ operation: "upload" }),
1065
+ expect.objectContaining({
1066
+ operation: "upload",
1067
+ minRuntimeVersion: "0.6.5",
1068
+ maxRuntimeVersion: null,
1069
+ }),
997
1070
  "platform-token",
998
1071
  "https://platform.vellum.ai",
999
1072
  );
1000
1073
  expect(localRuntimeExportToGcsMock).toHaveBeenCalled();
1001
1074
 
1002
1075
  // Import: download-URL for the docker target, then runtime import.
1076
+ // targetRuntimeVersion comes from the docker target's runtime
1077
+ // identity (mocked to "0.6.5"), not from cliPkg.version.
1003
1078
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1004
- {
1079
+ expect.objectContaining({
1005
1080
  operation: "download",
1006
1081
  bundleKey: "bundle-key-123",
1007
- },
1082
+ targetRuntimeVersion: "0.6.5",
1083
+ }),
1008
1084
  "platform-token",
1009
1085
  "https://platform.vellum.ai",
1010
1086
  );
1087
+ expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
1088
+ expect.objectContaining({ cloud: "docker" }),
1089
+ expect.any(String),
1090
+ );
1011
1091
  expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
1012
- "http://localhost:7822",
1092
+ expect.objectContaining({
1093
+ cloud: "docker",
1094
+ runtimeUrl: "http://localhost:7822",
1095
+ }),
1013
1096
  "local-token",
1014
1097
  expect.objectContaining({
1015
1098
  bundleUrl: "https://storage.googleapis.com/bucket/signed-download",
@@ -1057,12 +1140,19 @@ describe("unified GCS flow — four directions", () => {
1057
1140
  // Export leg: upload-URL (pinned to the same platform as import),
1058
1141
  // then runtime export.
1059
1142
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1060
- expect.objectContaining({ operation: "upload" }),
1143
+ expect.objectContaining({
1144
+ operation: "upload",
1145
+ minRuntimeVersion: "0.6.5",
1146
+ maxRuntimeVersion: null,
1147
+ }),
1061
1148
  "platform-token",
1062
1149
  "https://platform.vellum.ai",
1063
1150
  );
1064
1151
  expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
1065
- "http://localhost:7822",
1152
+ expect.objectContaining({
1153
+ cloud: "docker",
1154
+ runtimeUrl: "http://localhost:7822",
1155
+ }),
1066
1156
  "local-token",
1067
1157
  expect.objectContaining({
1068
1158
  uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
@@ -1071,7 +1161,10 @@ describe("unified GCS flow — four directions", () => {
1071
1161
 
1072
1162
  // Import leg: download-URL targets the new local runtime
1073
1163
  expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
1074
- "http://localhost:7823",
1164
+ expect.objectContaining({
1165
+ cloud: "local",
1166
+ runtimeUrl: "http://localhost:7823",
1167
+ }),
1075
1168
  "local-token",
1076
1169
  expect.anything(),
1077
1170
  );
@@ -1115,7 +1208,11 @@ describe("signed-URL request targets the bundle-owning platform", () => {
1115
1208
  // The signed-URL request for upload MUST target the existing
1116
1209
  // platform assistant's runtimeUrl, not the default platform URL.
1117
1210
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1118
- expect.objectContaining({ operation: "upload" }),
1211
+ expect.objectContaining({
1212
+ operation: "upload",
1213
+ minRuntimeVersion: "0.6.5",
1214
+ maxRuntimeVersion: null,
1215
+ }),
1119
1216
  "platform-token",
1120
1217
  "https://staging-platform.vellum.ai",
1121
1218
  );
@@ -1159,14 +1256,31 @@ describe("signed-URL request targets the bundle-owning platform", () => {
1159
1256
  bundleKey: "dev-bundle-key",
1160
1257
  });
1161
1258
 
1259
+ // Bundle key flows from the upload signed-URL request now; pin it so the
1260
+ // download-URL assertion below uses the same key.
1261
+ platformRequestSignedUrlMock.mockImplementation(async (params) => ({
1262
+ url:
1263
+ params.operation === "upload"
1264
+ ? "https://storage.googleapis.com/bucket/signed-upload"
1265
+ : "https://storage.googleapis.com/bucket/signed-download",
1266
+ bundleKey: params.bundleKey ?? "dev-bundle-key",
1267
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
1268
+ }));
1269
+
1162
1270
  const restoreFetch = installTrackingFetch();
1163
1271
  try {
1164
1272
  await teleport();
1165
1273
 
1166
1274
  // The download URL must be requested from the SOURCE platform (where
1167
- // the bundle was written by the server-side export), not the default.
1275
+ // the bundle was written by the runtime export), not the default.
1276
+ // targetRuntimeVersion comes from the local target's `/v1/identity`
1277
+ // (mocked to "0.6.5"), not from cliPkg.version.
1168
1278
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1169
- { operation: "download", bundleKey: "dev-bundle-key" },
1279
+ expect.objectContaining({
1280
+ operation: "download",
1281
+ bundleKey: "dev-bundle-key",
1282
+ targetRuntimeVersion: "0.6.5",
1283
+ }),
1170
1284
  "platform-token",
1171
1285
  "https://dev-platform.vellum.ai",
1172
1286
  );
@@ -1465,6 +1579,274 @@ describe("MigrationInProgressError handling", () => {
1465
1579
  });
1466
1580
  });
1467
1581
 
1582
+ // ---------------------------------------------------------------------------
1583
+ // VersionMismatchError handling — 422 from platformRequestSignedUrl on the
1584
+ // download leg is terminal: surface a friendly message + exit 1, no retry.
1585
+ // ---------------------------------------------------------------------------
1586
+
1587
+ describe("VersionMismatchError handling", () => {
1588
+ test("local target: 422 on download signed-URL exits 1 with friendly message", async () => {
1589
+ setArgv("--from", "my-platform", "--local", "my-local");
1590
+
1591
+ const platformEntry = makeEntry("my-platform", {
1592
+ cloud: "vellum",
1593
+ runtimeUrl: "https://platform.vellum.ai",
1594
+ });
1595
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1596
+
1597
+ findAssistantByNameMock.mockImplementation((name: string) => {
1598
+ if (name === "my-platform") return platformEntry;
1599
+ if (name === "my-local") return localEntry;
1600
+ return null;
1601
+ });
1602
+
1603
+ platformPollJobStatusMock.mockResolvedValue({
1604
+ jobId: "platform-export-job-1",
1605
+ type: "export",
1606
+ status: "complete",
1607
+ bundleKey: "platform-bundle-key-abc",
1608
+ });
1609
+
1610
+ // Upload signed-URL succeeds; download signed-URL throws version-mismatch.
1611
+ platformRequestSignedUrlMock.mockImplementation(async (params) => {
1612
+ if (params.operation === "download") {
1613
+ throw new platformClient.VersionMismatchError(
1614
+ { min_runtime_version: "99.0.0", max_runtime_version: null },
1615
+ "0.7.1",
1616
+ );
1617
+ }
1618
+ return {
1619
+ url: "https://storage.googleapis.com/bucket/signed-upload",
1620
+ bundleKey: params.bundleKey ?? "bundle-key-123",
1621
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
1622
+ };
1623
+ });
1624
+
1625
+ const restoreFetch = installTrackingFetch();
1626
+ try {
1627
+ await expect(teleport()).rejects.toThrow("process.exit:1");
1628
+
1629
+ // Friendly message uses the prefix from VersionMismatchError.formatMessage.
1630
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
1631
+ expect.stringContaining("Cannot import: bundle requires runtime"),
1632
+ );
1633
+
1634
+ // Terminal: no retry of the download signed-URL request.
1635
+ const downloadCalls = platformRequestSignedUrlMock.mock.calls.filter(
1636
+ (c) => (c[0] as { operation: string }).operation === "download",
1637
+ );
1638
+ expect(downloadCalls).toHaveLength(1);
1639
+
1640
+ // Runtime import-from-gcs must NOT be kicked off after the 422.
1641
+ expect(localRuntimeImportFromGcsMock).not.toHaveBeenCalled();
1642
+ } finally {
1643
+ restoreFetch();
1644
+ }
1645
+ });
1646
+
1647
+ test("docker target: 422 on download signed-URL exits 1 with friendly message", async () => {
1648
+ setArgv("--from", "my-local", "--docker");
1649
+
1650
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1651
+ const dockerEntry = makeEntry("new-docker", {
1652
+ cloud: "docker",
1653
+ runtimeUrl: "http://localhost:7822",
1654
+ });
1655
+
1656
+ findAssistantByNameMock.mockImplementation((name: string) =>
1657
+ name === "my-local" ? localEntry : null,
1658
+ );
1659
+
1660
+ loadAllAssistantsMock.mockImplementation(() => {
1661
+ if (hatchDockerMock.mock.calls.length > 0) {
1662
+ return [localEntry, dockerEntry];
1663
+ }
1664
+ return [localEntry];
1665
+ });
1666
+
1667
+ platformRequestSignedUrlMock.mockImplementation(async (params) => {
1668
+ if (params.operation === "download") {
1669
+ throw new platformClient.VersionMismatchError(
1670
+ { min_runtime_version: "99.0.0", max_runtime_version: null },
1671
+ "0.7.1",
1672
+ );
1673
+ }
1674
+ return {
1675
+ url: "https://storage.googleapis.com/bucket/signed-upload",
1676
+ bundleKey: params.bundleKey ?? "bundle-key-123",
1677
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
1678
+ };
1679
+ });
1680
+
1681
+ const restoreFetch = installTrackingFetch();
1682
+ try {
1683
+ await expect(teleport()).rejects.toThrow("process.exit:1");
1684
+
1685
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
1686
+ expect.stringContaining("Cannot import: bundle requires runtime"),
1687
+ );
1688
+
1689
+ expect(localRuntimeImportFromGcsMock).not.toHaveBeenCalled();
1690
+ } finally {
1691
+ restoreFetch();
1692
+ }
1693
+ });
1694
+
1695
+ test("non-VersionMismatchError on download signed-URL re-raises", async () => {
1696
+ setArgv("--from", "my-platform", "--local", "my-local");
1697
+
1698
+ const platformEntry = makeEntry("my-platform", {
1699
+ cloud: "vellum",
1700
+ runtimeUrl: "https://platform.vellum.ai",
1701
+ });
1702
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1703
+
1704
+ findAssistantByNameMock.mockImplementation((name: string) => {
1705
+ if (name === "my-platform") return platformEntry;
1706
+ if (name === "my-local") return localEntry;
1707
+ return null;
1708
+ });
1709
+
1710
+ platformPollJobStatusMock.mockResolvedValue({
1711
+ jobId: "platform-export-job-1",
1712
+ type: "export",
1713
+ status: "complete",
1714
+ bundleKey: "platform-bundle-key-abc",
1715
+ });
1716
+
1717
+ platformRequestSignedUrlMock.mockImplementation(async (params) => {
1718
+ if (params.operation === "download") {
1719
+ throw new Error("network blew up");
1720
+ }
1721
+ return {
1722
+ url: "https://storage.googleapis.com/bucket/signed-upload",
1723
+ bundleKey: params.bundleKey ?? "bundle-key-123",
1724
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
1725
+ };
1726
+ });
1727
+
1728
+ const restoreFetch = installTrackingFetch();
1729
+ try {
1730
+ await expect(teleport()).rejects.toThrow("network blew up");
1731
+ } finally {
1732
+ restoreFetch();
1733
+ }
1734
+ });
1735
+ });
1736
+
1737
+ // ---------------------------------------------------------------------------
1738
+ // Target-runtime version fetch — the download signed-URL request must use
1739
+ // the TARGET runtime's reported version, not the orchestrating CLI's
1740
+ // version (which can diverge when the target was upgraded independently).
1741
+ // ---------------------------------------------------------------------------
1742
+
1743
+ describe("target runtime version fetch", () => {
1744
+ test("local target: targetRuntimeVersion comes from /v1/identity, not cliPkg", async () => {
1745
+ setArgv("--from", "my-platform", "--local", "my-local");
1746
+
1747
+ const platformEntry = makeEntry("my-platform", {
1748
+ cloud: "vellum",
1749
+ runtimeUrl: "https://platform.vellum.ai",
1750
+ });
1751
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1752
+
1753
+ findAssistantByNameMock.mockImplementation((name: string) => {
1754
+ if (name === "my-platform") return platformEntry;
1755
+ if (name === "my-local") return localEntry;
1756
+ return null;
1757
+ });
1758
+
1759
+ platformPollJobStatusMock.mockResolvedValue({
1760
+ jobId: "platform-export-job-1",
1761
+ type: "export",
1762
+ status: "complete",
1763
+ bundleKey: "platform-bundle-key-abc",
1764
+ });
1765
+
1766
+ // Choose a version unlikely to match cliPkg.version so a regression
1767
+ // would be obvious.
1768
+ localRuntimeIdentityMock.mockResolvedValue({ version: "1.2.3-runtime" });
1769
+
1770
+ const restoreFetch = installTrackingFetch();
1771
+ try {
1772
+ await teleport();
1773
+
1774
+ const downloadCall = platformRequestSignedUrlMock.mock.calls.find(
1775
+ (c) => (c[0] as { operation: string }).operation === "download",
1776
+ );
1777
+ expect(downloadCall).toBeDefined();
1778
+ expect(downloadCall![0]).toMatchObject({
1779
+ targetRuntimeVersion: "1.2.3-runtime",
1780
+ });
1781
+
1782
+ // Identity was fetched against the TARGET (local) entry, not the
1783
+ // platform source.
1784
+ expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
1785
+ expect.objectContaining({
1786
+ cloud: "local",
1787
+ assistantId: "my-local",
1788
+ }),
1789
+ expect.any(String),
1790
+ );
1791
+ } finally {
1792
+ restoreFetch();
1793
+ }
1794
+ });
1795
+
1796
+ test("identity fetch failure aborts before requesting download URL", async () => {
1797
+ setArgv("--from", "my-platform", "--local", "my-local");
1798
+
1799
+ const platformEntry = makeEntry("my-platform", {
1800
+ cloud: "vellum",
1801
+ runtimeUrl: "https://platform.vellum.ai",
1802
+ });
1803
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1804
+
1805
+ findAssistantByNameMock.mockImplementation((name: string) => {
1806
+ if (name === "my-platform") return platformEntry;
1807
+ if (name === "my-local") return localEntry;
1808
+ return null;
1809
+ });
1810
+
1811
+ platformPollJobStatusMock.mockResolvedValue({
1812
+ jobId: "platform-export-job-1",
1813
+ type: "export",
1814
+ status: "complete",
1815
+ bundleKey: "platform-bundle-key-abc",
1816
+ });
1817
+
1818
+ // Identity is fetched twice in this flow: once on the source (platform)
1819
+ // for `min_runtime_version` on the upload signed-URL, then once on the
1820
+ // target (local) for `targetRuntimeVersion` on the download signed-URL.
1821
+ // Succeed on the source call so the flow gets as far as the target
1822
+ // identity fetch — that's the failure this test is exercising.
1823
+ localRuntimeIdentityMock.mockImplementation(async (entry) => {
1824
+ if (entry.cloud === "local") {
1825
+ throw new Error("Local runtime identity failed (502): bad gateway");
1826
+ }
1827
+ return { version: "0.6.5" };
1828
+ });
1829
+
1830
+ const restoreFetch = installTrackingFetch();
1831
+ try {
1832
+ await expect(teleport()).rejects.toThrow("process.exit:1");
1833
+
1834
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
1835
+ expect.stringContaining("Could not read target runtime version"),
1836
+ );
1837
+
1838
+ // No download signed-URL request was made — we aborted before that.
1839
+ const downloadCalls = platformRequestSignedUrlMock.mock.calls.filter(
1840
+ (c) => (c[0] as { operation: string }).operation === "download",
1841
+ );
1842
+ expect(downloadCalls).toHaveLength(0);
1843
+ expect(localRuntimeImportFromGcsMock).not.toHaveBeenCalled();
1844
+ } finally {
1845
+ restoreFetch();
1846
+ }
1847
+ });
1848
+ });
1849
+
1468
1850
  // ---------------------------------------------------------------------------
1469
1851
  // Dry-run behavior
1470
1852
  // ---------------------------------------------------------------------------
@@ -1555,10 +1937,9 @@ describe("dry-run", () => {
1555
1937
  ),
1556
1938
  );
1557
1939
 
1558
- // Must fail BEFORE any export work — no signed URL request, no platform
1559
- // export initiation, nothing that costs time or bandwidth.
1940
+ // Must fail BEFORE any export work — no signed URL request, no runtime
1941
+ // export kickoff, nothing that costs time or bandwidth.
1560
1942
  expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
1561
- expect(platformInitiateExportMock).not.toHaveBeenCalled();
1562
1943
  expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
1563
1944
  } finally {
1564
1945
  restoreFetch();
@@ -2090,6 +2471,185 @@ describe("auth + transient-error resilience", () => {
2090
2471
  });
2091
2472
  });
2092
2473
 
2474
+ // ---------------------------------------------------------------------------
2475
+ // Source-runtime version is sourced from the daemon, not the CLI
2476
+ // (Codex P1 regression guard for PR #29436)
2477
+ // ---------------------------------------------------------------------------
2478
+
2479
+ describe("upload signed-URL records source runtime version (not CLI version)", () => {
2480
+ test("local source: identity is fetched BEFORE the upload signed-URL request", async () => {
2481
+ setArgv("--from", "my-local", "--platform");
2482
+
2483
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2484
+ findAssistantByNameMock.mockImplementation((name: string) =>
2485
+ name === "my-local" ? localEntry : null,
2486
+ );
2487
+
2488
+ // Distinguish the daemon's version from anything else hardcoded.
2489
+ localRuntimeIdentityMock.mockResolvedValue({ version: "0.5.9" });
2490
+
2491
+ // Order tracker: capture which mock was called first.
2492
+ const callOrder: string[] = [];
2493
+ localRuntimeIdentityMock.mockImplementationOnce(async () => {
2494
+ callOrder.push("identity");
2495
+ return { version: "0.5.9" };
2496
+ });
2497
+ platformRequestSignedUrlMock.mockImplementationOnce(async (params) => {
2498
+ callOrder.push("signed-url");
2499
+ return {
2500
+ url: "https://storage.googleapis.com/bucket/signed-upload",
2501
+ bundleKey: params.bundleKey ?? "bundle-key-123",
2502
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
2503
+ };
2504
+ });
2505
+
2506
+ const restoreFetch = installTrackingFetch();
2507
+ try {
2508
+ await teleport();
2509
+
2510
+ expect(callOrder[0]).toBe("identity");
2511
+ expect(callOrder[1]).toBe("signed-url");
2512
+
2513
+ // Upload request stamps minRuntimeVersion with the daemon's version,
2514
+ // NOT cliPkg.version.
2515
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
2516
+ expect.objectContaining({
2517
+ operation: "upload",
2518
+ minRuntimeVersion: "0.5.9",
2519
+ maxRuntimeVersion: null,
2520
+ }),
2521
+ "platform-token",
2522
+ "https://platform.vellum.ai",
2523
+ );
2524
+ } finally {
2525
+ restoreFetch();
2526
+ }
2527
+ });
2528
+
2529
+ test("local source: identity fetch failure aborts before signed-URL request", async () => {
2530
+ setArgv("--from", "my-local", "--platform");
2531
+
2532
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2533
+ findAssistantByNameMock.mockImplementation((name: string) =>
2534
+ name === "my-local" ? localEntry : null,
2535
+ );
2536
+
2537
+ localRuntimeIdentityMock.mockRejectedValue(
2538
+ new Error("Failed to fetch runtime identity: 503 Service Unavailable"),
2539
+ );
2540
+
2541
+ const restoreFetch = installTrackingFetch();
2542
+ try {
2543
+ await expect(teleport()).rejects.toThrow("process.exit:1");
2544
+
2545
+ // Must NOT have proceeded to signed URL or export.
2546
+ expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
2547
+ expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
2548
+
2549
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
2550
+ expect.stringContaining("Could not fetch runtime identity"),
2551
+ );
2552
+ } finally {
2553
+ restoreFetch();
2554
+ }
2555
+ });
2556
+
2557
+ test("platform source: managed runtime's identity is fetched and recorded", async () => {
2558
+ setArgv("--from", "my-platform", "--local", "my-local");
2559
+
2560
+ const platformEntry = makeEntry("my-platform", {
2561
+ cloud: "vellum",
2562
+ runtimeUrl: "https://platform.vellum.ai",
2563
+ });
2564
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2565
+
2566
+ findAssistantByNameMock.mockImplementation((name: string) => {
2567
+ if (name === "my-platform") return platformEntry;
2568
+ if (name === "my-local") return localEntry;
2569
+ return null;
2570
+ });
2571
+
2572
+ platformPollJobStatusMock.mockResolvedValue({
2573
+ jobId: "platform-export-job-1",
2574
+ type: "export",
2575
+ status: "complete",
2576
+ bundleKey: "platform-bundle",
2577
+ });
2578
+
2579
+ localRuntimeIdentityMock.mockResolvedValue({ version: "0.7.2" });
2580
+
2581
+ const restoreFetch = installTrackingFetch();
2582
+ try {
2583
+ await teleport();
2584
+
2585
+ // Identity was fetched against the platform-managed runtime entry
2586
+ // (cloud=vellum) with the platform token — not via guardian token.
2587
+ expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
2588
+ expect.objectContaining({
2589
+ cloud: "vellum",
2590
+ runtimeUrl: "https://platform.vellum.ai",
2591
+ assistantId: "my-platform",
2592
+ }),
2593
+ "platform-token",
2594
+ );
2595
+
2596
+ // The recorded version came from the platform runtime, not the CLI.
2597
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
2598
+ expect.objectContaining({
2599
+ operation: "upload",
2600
+ minRuntimeVersion: "0.7.2",
2601
+ maxRuntimeVersion: null,
2602
+ }),
2603
+ "platform-token",
2604
+ "https://platform.vellum.ai",
2605
+ );
2606
+ } finally {
2607
+ restoreFetch();
2608
+ }
2609
+ });
2610
+
2611
+ test("platform source: identity fetch failure aborts before signed-URL request", async () => {
2612
+ setArgv("--from", "my-platform", "--local", "my-local");
2613
+
2614
+ const platformEntry = makeEntry("my-platform", {
2615
+ cloud: "vellum",
2616
+ runtimeUrl: "https://platform.vellum.ai",
2617
+ });
2618
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2619
+
2620
+ findAssistantByNameMock.mockImplementation((name: string) => {
2621
+ if (name === "my-platform") return platformEntry;
2622
+ if (name === "my-local") return localEntry;
2623
+ return null;
2624
+ });
2625
+
2626
+ localRuntimeIdentityMock.mockRejectedValue(
2627
+ new Error("Failed to fetch runtime identity: 502 Bad Gateway"),
2628
+ );
2629
+
2630
+ const restoreFetch = installTrackingFetch();
2631
+ try {
2632
+ await expect(teleport()).rejects.toThrow("process.exit:1");
2633
+
2634
+ // Signed-URL upload request must not have happened.
2635
+ const uploadCalls = platformRequestSignedUrlMock.mock.calls.filter(
2636
+ (call: unknown[]) =>
2637
+ (call[0] as { operation: string }).operation === "upload",
2638
+ );
2639
+ expect(uploadCalls.length).toBe(0);
2640
+
2641
+ // Runtime export was never kicked off.
2642
+ expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
2643
+
2644
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
2645
+ expect.stringContaining("Could not fetch runtime identity"),
2646
+ );
2647
+ } finally {
2648
+ restoreFetch();
2649
+ }
2650
+ });
2651
+ });
2652
+
2093
2653
  // ---------------------------------------------------------------------------
2094
2654
  // Misc: legacy --to deprecation
2095
2655
  // ---------------------------------------------------------------------------