@vellumai/cli 0.7.1 → 0.7.3

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 (39) hide show
  1. package/AGENTS.md +3 -11
  2. package/bun.lock +0 -15
  3. package/package.json +1 -6
  4. package/src/__tests__/backup.test.ts +121 -5
  5. package/src/__tests__/teleport.test.ts +515 -10
  6. package/src/commands/backup.ts +35 -2
  7. package/src/commands/client.ts +90 -7
  8. package/src/commands/exec.ts +13 -4
  9. package/src/commands/hatch.ts +1 -1
  10. package/src/commands/login.ts +11 -0
  11. package/src/commands/restore.ts +7 -1
  12. package/src/commands/rollback.ts +1 -1
  13. package/src/commands/setup.ts +38 -73
  14. package/src/commands/teleport.ts +122 -12
  15. package/src/commands/upgrade.ts +8 -2
  16. package/src/commands/wake.ts +5 -16
  17. package/src/components/DefaultMainScreen.tsx +42 -130
  18. package/src/index.ts +1 -7
  19. package/src/lib/__tests__/docker.test.ts +53 -35
  20. package/src/lib/__tests__/local-runtime-client.test.ts +186 -0
  21. package/src/lib/__tests__/platform-client-signed-url.test.ts +235 -0
  22. package/src/lib/__tests__/runtime-url.test.ts +39 -1
  23. package/src/lib/assistant-client.ts +13 -5
  24. package/src/lib/assistant-config.ts +0 -25
  25. package/src/lib/backup-ops.ts +43 -17
  26. package/src/lib/client-identity.ts +9 -5
  27. package/src/lib/docker.ts +6 -267
  28. package/src/lib/environments/paths.ts +20 -0
  29. package/src/lib/guardian-token.ts +56 -6
  30. package/src/lib/hatch-local.ts +3 -26
  31. package/src/lib/local-runtime-client.ts +82 -1
  32. package/src/lib/local.ts +9 -7
  33. package/src/lib/ngrok.ts +36 -26
  34. package/src/lib/platform-client.ts +100 -1
  35. package/src/lib/retire-local.ts +2 -2
  36. package/src/lib/runtime-url.ts +22 -0
  37. package/src/lib/statefulset.ts +375 -0
  38. package/src/lib/upgrade-lifecycle.ts +97 -1
  39. package/src/commands/pair.ts +0 -212
@@ -196,6 +196,15 @@ const localRuntimeImportFromGcsMock = spyOn(
196
196
  "localRuntimeImportFromGcs",
197
197
  ).mockResolvedValue({ jobId: "local-import-job-1" });
198
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
+
199
208
  const localRuntimePollJobStatusMock = spyOn(
200
209
  localRuntimeClient,
201
210
  "localRuntimePollJobStatus",
@@ -297,6 +306,7 @@ afterAll(() => {
297
306
  computeDeviceIdMock.mockRestore();
298
307
  localRuntimeExportToGcsMock.mockRestore();
299
308
  localRuntimeImportFromGcsMock.mockRestore();
309
+ localRuntimeIdentityMock.mockRestore();
300
310
  localRuntimePollJobStatusMock.mockRestore();
301
311
  rmSync(testDir, { recursive: true, force: true });
302
312
  delete process.env.VELLUM_LOCKFILE_DIR;
@@ -446,6 +456,8 @@ beforeEach(() => {
446
456
  localRuntimeImportFromGcsMock.mockResolvedValue({
447
457
  jobId: "local-import-job-1",
448
458
  });
459
+ localRuntimeIdentityMock.mockReset();
460
+ localRuntimeIdentityMock.mockResolvedValue({ version: "0.6.5" });
449
461
  localRuntimePollJobStatusMock.mockReset();
450
462
  localRuntimePollJobStatusMock.mockImplementation(defaultLocalRuntimePollImpl);
451
463
 
@@ -850,7 +862,11 @@ describe("unified GCS flow — four directions", () => {
850
862
  // Signed-URL request for upload — pinned to the platform target's URL
851
863
  // so upload and download land on the same platform.
852
864
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
853
- expect.objectContaining({ operation: "upload" }),
865
+ expect.objectContaining({
866
+ operation: "upload",
867
+ minRuntimeVersion: "0.6.5",
868
+ maxRuntimeVersion: null,
869
+ }),
854
870
  "platform-token",
855
871
  "https://platform.vellum.ai",
856
872
  );
@@ -937,7 +953,11 @@ describe("unified GCS flow — four directions", () => {
937
953
  // Platform side: requested an upload URL, kicked off a runtime export to
938
954
  // GCS, and polled the unified job status.
939
955
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
940
- expect.objectContaining({ operation: "upload" }),
956
+ expect.objectContaining({
957
+ operation: "upload",
958
+ minRuntimeVersion: "0.6.5",
959
+ maxRuntimeVersion: null,
960
+ }),
941
961
  "platform-token",
942
962
  "https://platform.vellum.ai",
943
963
  );
@@ -974,14 +994,24 @@ describe("unified GCS flow — four directions", () => {
974
994
  // platform's bundle_key. The URL must target the SOURCE platform
975
995
  // (where the bundle was written) — pinned so a lockfile change
976
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.
977
1002
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
978
- {
1003
+ expect.objectContaining({
979
1004
  operation: "download",
980
1005
  bundleKey: "platform-exports/org-1/bundle-abc.vbundle",
981
- },
1006
+ targetRuntimeVersion: "0.6.5",
1007
+ }),
982
1008
  "platform-token",
983
1009
  "https://platform.vellum.ai",
984
1010
  );
1011
+ expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
1012
+ expect.objectContaining({ cloud: "local" }),
1013
+ expect.any(String),
1014
+ );
985
1015
 
986
1016
  // Runtime import-from-gcs was kicked off with that URL.
987
1017
  expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
@@ -1032,21 +1062,32 @@ describe("unified GCS flow — four directions", () => {
1032
1062
  // lives in one place end-to-end. For local→docker neither side is
1033
1063
  // platform, so we default to getPlatformUrl() (resolved once).
1034
1064
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1035
- expect.objectContaining({ operation: "upload" }),
1065
+ expect.objectContaining({
1066
+ operation: "upload",
1067
+ minRuntimeVersion: "0.6.5",
1068
+ maxRuntimeVersion: null,
1069
+ }),
1036
1070
  "platform-token",
1037
1071
  "https://platform.vellum.ai",
1038
1072
  );
1039
1073
  expect(localRuntimeExportToGcsMock).toHaveBeenCalled();
1040
1074
 
1041
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.
1042
1078
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1043
- {
1079
+ expect.objectContaining({
1044
1080
  operation: "download",
1045
1081
  bundleKey: "bundle-key-123",
1046
- },
1082
+ targetRuntimeVersion: "0.6.5",
1083
+ }),
1047
1084
  "platform-token",
1048
1085
  "https://platform.vellum.ai",
1049
1086
  );
1087
+ expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
1088
+ expect.objectContaining({ cloud: "docker" }),
1089
+ expect.any(String),
1090
+ );
1050
1091
  expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
1051
1092
  expect.objectContaining({
1052
1093
  cloud: "docker",
@@ -1099,7 +1140,11 @@ describe("unified GCS flow — four directions", () => {
1099
1140
  // Export leg: upload-URL (pinned to the same platform as import),
1100
1141
  // then runtime export.
1101
1142
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1102
- expect.objectContaining({ operation: "upload" }),
1143
+ expect.objectContaining({
1144
+ operation: "upload",
1145
+ minRuntimeVersion: "0.6.5",
1146
+ maxRuntimeVersion: null,
1147
+ }),
1103
1148
  "platform-token",
1104
1149
  "https://platform.vellum.ai",
1105
1150
  );
@@ -1163,7 +1208,11 @@ describe("signed-URL request targets the bundle-owning platform", () => {
1163
1208
  // The signed-URL request for upload MUST target the existing
1164
1209
  // platform assistant's runtimeUrl, not the default platform URL.
1165
1210
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1166
- expect.objectContaining({ operation: "upload" }),
1211
+ expect.objectContaining({
1212
+ operation: "upload",
1213
+ minRuntimeVersion: "0.6.5",
1214
+ maxRuntimeVersion: null,
1215
+ }),
1167
1216
  "platform-token",
1168
1217
  "https://staging-platform.vellum.ai",
1169
1218
  );
@@ -1224,8 +1273,14 @@ describe("signed-URL request targets the bundle-owning platform", () => {
1224
1273
 
1225
1274
  // The download URL must be requested from the SOURCE platform (where
1226
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.
1227
1278
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1228
- { operation: "download", bundleKey: "dev-bundle-key" },
1279
+ expect.objectContaining({
1280
+ operation: "download",
1281
+ bundleKey: "dev-bundle-key",
1282
+ targetRuntimeVersion: "0.6.5",
1283
+ }),
1229
1284
  "platform-token",
1230
1285
  "https://dev-platform.vellum.ai",
1231
1286
  );
@@ -1524,6 +1579,274 @@ describe("MigrationInProgressError handling", () => {
1524
1579
  });
1525
1580
  });
1526
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
+
1527
1850
  // ---------------------------------------------------------------------------
1528
1851
  // Dry-run behavior
1529
1852
  // ---------------------------------------------------------------------------
@@ -1878,6 +2201,9 @@ describe("platform credential injection", () => {
1878
2201
  "device-id-123",
1879
2202
  "my-local",
1880
2203
  "cli",
2204
+ undefined, // assistantVersion (gateway unreachable in test)
2205
+ expect.any(String), // platformUrl from getPlatformUrl()
2206
+ undefined, // ingressUrl (gateway unreachable in test)
1881
2207
  );
1882
2208
  expect(injectCredentialsIntoAssistantMock).toHaveBeenCalledWith({
1883
2209
  gatewayUrl: "http://localhost:7821",
@@ -2148,6 +2474,185 @@ describe("auth + transient-error resilience", () => {
2148
2474
  });
2149
2475
  });
2150
2476
 
2477
+ // ---------------------------------------------------------------------------
2478
+ // Source-runtime version is sourced from the daemon, not the CLI
2479
+ // (Codex P1 regression guard for PR #29436)
2480
+ // ---------------------------------------------------------------------------
2481
+
2482
+ describe("upload signed-URL records source runtime version (not CLI version)", () => {
2483
+ test("local source: identity is fetched BEFORE the upload signed-URL request", async () => {
2484
+ setArgv("--from", "my-local", "--platform");
2485
+
2486
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2487
+ findAssistantByNameMock.mockImplementation((name: string) =>
2488
+ name === "my-local" ? localEntry : null,
2489
+ );
2490
+
2491
+ // Distinguish the daemon's version from anything else hardcoded.
2492
+ localRuntimeIdentityMock.mockResolvedValue({ version: "0.5.9" });
2493
+
2494
+ // Order tracker: capture which mock was called first.
2495
+ const callOrder: string[] = [];
2496
+ localRuntimeIdentityMock.mockImplementationOnce(async () => {
2497
+ callOrder.push("identity");
2498
+ return { version: "0.5.9" };
2499
+ });
2500
+ platformRequestSignedUrlMock.mockImplementationOnce(async (params) => {
2501
+ callOrder.push("signed-url");
2502
+ return {
2503
+ url: "https://storage.googleapis.com/bucket/signed-upload",
2504
+ bundleKey: params.bundleKey ?? "bundle-key-123",
2505
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
2506
+ };
2507
+ });
2508
+
2509
+ const restoreFetch = installTrackingFetch();
2510
+ try {
2511
+ await teleport();
2512
+
2513
+ expect(callOrder[0]).toBe("identity");
2514
+ expect(callOrder[1]).toBe("signed-url");
2515
+
2516
+ // Upload request stamps minRuntimeVersion with the daemon's version,
2517
+ // NOT cliPkg.version.
2518
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
2519
+ expect.objectContaining({
2520
+ operation: "upload",
2521
+ minRuntimeVersion: "0.5.9",
2522
+ maxRuntimeVersion: null,
2523
+ }),
2524
+ "platform-token",
2525
+ "https://platform.vellum.ai",
2526
+ );
2527
+ } finally {
2528
+ restoreFetch();
2529
+ }
2530
+ });
2531
+
2532
+ test("local source: identity fetch failure aborts before signed-URL request", async () => {
2533
+ setArgv("--from", "my-local", "--platform");
2534
+
2535
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2536
+ findAssistantByNameMock.mockImplementation((name: string) =>
2537
+ name === "my-local" ? localEntry : null,
2538
+ );
2539
+
2540
+ localRuntimeIdentityMock.mockRejectedValue(
2541
+ new Error("Failed to fetch runtime identity: 503 Service Unavailable"),
2542
+ );
2543
+
2544
+ const restoreFetch = installTrackingFetch();
2545
+ try {
2546
+ await expect(teleport()).rejects.toThrow("process.exit:1");
2547
+
2548
+ // Must NOT have proceeded to signed URL or export.
2549
+ expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
2550
+ expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
2551
+
2552
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
2553
+ expect.stringContaining("Could not fetch runtime identity"),
2554
+ );
2555
+ } finally {
2556
+ restoreFetch();
2557
+ }
2558
+ });
2559
+
2560
+ test("platform source: managed runtime's identity is fetched and recorded", async () => {
2561
+ setArgv("--from", "my-platform", "--local", "my-local");
2562
+
2563
+ const platformEntry = makeEntry("my-platform", {
2564
+ cloud: "vellum",
2565
+ runtimeUrl: "https://platform.vellum.ai",
2566
+ });
2567
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2568
+
2569
+ findAssistantByNameMock.mockImplementation((name: string) => {
2570
+ if (name === "my-platform") return platformEntry;
2571
+ if (name === "my-local") return localEntry;
2572
+ return null;
2573
+ });
2574
+
2575
+ platformPollJobStatusMock.mockResolvedValue({
2576
+ jobId: "platform-export-job-1",
2577
+ type: "export",
2578
+ status: "complete",
2579
+ bundleKey: "platform-bundle",
2580
+ });
2581
+
2582
+ localRuntimeIdentityMock.mockResolvedValue({ version: "0.7.2" });
2583
+
2584
+ const restoreFetch = installTrackingFetch();
2585
+ try {
2586
+ await teleport();
2587
+
2588
+ // Identity was fetched against the platform-managed runtime entry
2589
+ // (cloud=vellum) with the platform token — not via guardian token.
2590
+ expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
2591
+ expect.objectContaining({
2592
+ cloud: "vellum",
2593
+ runtimeUrl: "https://platform.vellum.ai",
2594
+ assistantId: "my-platform",
2595
+ }),
2596
+ "platform-token",
2597
+ );
2598
+
2599
+ // The recorded version came from the platform runtime, not the CLI.
2600
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
2601
+ expect.objectContaining({
2602
+ operation: "upload",
2603
+ minRuntimeVersion: "0.7.2",
2604
+ maxRuntimeVersion: null,
2605
+ }),
2606
+ "platform-token",
2607
+ "https://platform.vellum.ai",
2608
+ );
2609
+ } finally {
2610
+ restoreFetch();
2611
+ }
2612
+ });
2613
+
2614
+ test("platform source: identity fetch failure aborts before signed-URL request", async () => {
2615
+ setArgv("--from", "my-platform", "--local", "my-local");
2616
+
2617
+ const platformEntry = makeEntry("my-platform", {
2618
+ cloud: "vellum",
2619
+ runtimeUrl: "https://platform.vellum.ai",
2620
+ });
2621
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2622
+
2623
+ findAssistantByNameMock.mockImplementation((name: string) => {
2624
+ if (name === "my-platform") return platformEntry;
2625
+ if (name === "my-local") return localEntry;
2626
+ return null;
2627
+ });
2628
+
2629
+ localRuntimeIdentityMock.mockRejectedValue(
2630
+ new Error("Failed to fetch runtime identity: 502 Bad Gateway"),
2631
+ );
2632
+
2633
+ const restoreFetch = installTrackingFetch();
2634
+ try {
2635
+ await expect(teleport()).rejects.toThrow("process.exit:1");
2636
+
2637
+ // Signed-URL upload request must not have happened.
2638
+ const uploadCalls = platformRequestSignedUrlMock.mock.calls.filter(
2639
+ (call: unknown[]) =>
2640
+ (call[0] as { operation: string }).operation === "upload",
2641
+ );
2642
+ expect(uploadCalls.length).toBe(0);
2643
+
2644
+ // Runtime export was never kicked off.
2645
+ expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
2646
+
2647
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
2648
+ expect.stringContaining("Could not fetch runtime identity"),
2649
+ );
2650
+ } finally {
2651
+ restoreFetch();
2652
+ }
2653
+ });
2654
+ });
2655
+
2151
2656
  // ---------------------------------------------------------------------------
2152
2657
  // Misc: legacy --to deprecation
2153
2658
  // ---------------------------------------------------------------------------
@@ -9,6 +9,7 @@ import { pollJobUntilDone } from "../lib/job-polling.js";
9
9
  import {
10
10
  MigrationInProgressError,
11
11
  localRuntimeExportToGcs,
12
+ localRuntimeIdentity,
12
13
  localRuntimePollJobStatus,
13
14
  } from "../lib/local-runtime-client.js";
14
15
  import {
@@ -232,9 +233,30 @@ async function backupPlatform(
232
233
  // signed-download request.
233
234
  let exportPlatformToken = platformToken;
234
235
 
236
+ // Step 0 — Ask the source runtime which version it's running. The bundle
237
+ // is produced by the daemon (not the CLI), and the CLI version can drift
238
+ // from the daemon version, so the daemon's version is the authoritative
239
+ // value to record as the bundle's `min_runtime_version`. Stamping with
240
+ // `cliPkg.version` here would record an inaccurate compatibility band on
241
+ // the signed-URL request.
242
+ let runtimeIdentity: { version: string };
243
+ try {
244
+ runtimeIdentity = await localRuntimeIdentity(entry, exportPlatformToken);
245
+ } catch (err) {
246
+ const msg = err instanceof Error ? err.message : String(err);
247
+ console.error(
248
+ `Error: Could not fetch runtime identity from '${name}': ${msg}`,
249
+ );
250
+ process.exit(1);
251
+ }
252
+
235
253
  // Step 1 — Request a signed upload URL.
236
254
  const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
237
- { operation: "upload" },
255
+ {
256
+ operation: "upload",
257
+ minRuntimeVersion: runtimeIdentity.version,
258
+ maxRuntimeVersion: null,
259
+ },
238
260
  exportPlatformToken,
239
261
  platformUrl,
240
262
  );
@@ -289,8 +311,19 @@ async function backupPlatform(
289
311
  // poll-loop 401 refresh doesn't get clobbered here — otherwise a long
290
312
  // export that recovered mid-poll via re-auth would still 401 on the
291
313
  // download-URL request and abort an otherwise successful run.
314
+ //
315
+ // We deliberately do NOT send `targetRuntimeVersion` here. This flow
316
+ // saves the bundle to disk for offline storage; there is no target
317
+ // runtime to gate against, and the user can later restore the file
318
+ // into any compatible runtime. Sending the CLI's version would
319
+ // incorrectly block older CLIs from backing up newer assistants.
320
+ // The platform treats `target_runtime_version` as optional and skips
321
+ // the version check when it's omitted.
292
322
  const { url: bundleUrl } = await platformRequestSignedUrl(
293
- { operation: "download", bundleKey },
323
+ {
324
+ operation: "download",
325
+ bundleKey,
326
+ },
294
327
  exportPlatformToken,
295
328
  platformUrl,
296
329
  );