@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
package/AGENTS.md CHANGED
@@ -57,20 +57,12 @@ For example, the signing key used for JWT auth between the daemon and gateway is
57
57
 
58
58
  The CLI creates and manages Docker volumes for containerized instances. See the root `AGENTS.md` § Docker Volume Architecture for the full volume layout.
59
59
 
60
- **Volume creation** (`hatch`): Creates five volumes per instance — workspace, gateway-security, ces-security, socket, and dockerd-data (the last backs the inner Docker engine used for Meet; see below). The legacy data volume is no longer created.
60
+ **Volume creation** (`hatch`): Creates six volumes per instance — workspace, gateway-security, ces-security, socket, assistant-ipc, and gateway-ipc. The legacy data volume is no longer created.
61
61
 
62
62
  **Volume migration** (`wake`/`hatch`): On startup, existing instances that still have a legacy data volume are migrated. `migrateGatewaySecurityFiles()` and `migrateCesSecurityFiles()` in `lib/docker.ts` copy security files from the data volume to their respective security volumes. Migrations are idempotent and non-fatal.
63
63
 
64
64
  **Volume cleanup** (`retire`): All volumes (including the legacy data volume if it exists) are removed when an instance is retired.
65
65
 
66
- **Volume mount rules**: Each service container receives only the volumes it needs. The assistant never mounts `gateway-security` or `ces-security`. The gateway never mounts `ces-security`. The CES mounts the workspace volume as read-only. The `dockerd-data` volume is mounted only on the assistant container.
66
+ **Volume mount rules**: Each service container receives only the volumes it needs. The assistant never mounts `gateway-security` or `ces-security`. The gateway never mounts `ces-security`. The CES mounts the workspace volume as read-only.
67
67
 
68
- **Meet Docker-in-Docker support** (assistant container only): The assistant container runs an inner `dockerd` that hosts the Meet-bot containers as nested children. The CLI supports this by:
69
-
70
- - Creating a dedicated `<name>-dockerd-data` volume mounted at `/var/lib/docker` so pulled images and container state persist across assistant restarts.
71
- - Running the assistant container with `CAP_SYS_ADMIN` + `CAP_NET_ADMIN` plus `--security-opt seccomp=unconfined` + `--security-opt apparmor=unconfined` so the inner dockerd can configure cgroups, overlay mounts, and container networking without the default seccomp profile blocking clone/unshare/pivot_root syscalls or the default AppArmor profile denying its mount operations. `--privileged` is deliberately avoided — dropping it shrinks the escape surface by withholding the rest of the host capability set and access to host device nodes.
72
- - No longer bind-mounting the host's `/var/run/docker.sock`; Meet-bot spawning happens entirely inside the assistant container.
73
-
74
- Both are wired in `serviceDockerRunArgs()` in `lib/docker.ts`.
75
-
76
- This capability + security-opt set is acceptable for single-user local deployments. Managed/multi-tenant mode needs a different spawn model (e.g. a Kubernetes job runner) and is out of scope for this CLI.
68
+ **Container security posture**: The assistant container runs as a non-root user (UID 1001) with no elevated capabilities. `--privileged`, `--cap-add`, and `--security-opt` overrides are NOT used. The host Docker socket is NOT bind-mounted. Do NOT re-add elevated capabilities without a concrete runtime requirement — the Docker Engine packages and inner `dockerd` supervisor were reverted (PR #26028) and the capabilities they required are no longer needed.
package/bun.lock CHANGED
@@ -7,17 +7,12 @@
7
7
  "dependencies": {
8
8
  "chalk": "5.6.2",
9
9
  "ink": "6.8.0",
10
- "jsqr": "1.4.0",
11
10
  "nanoid": "5.1.7",
12
- "pngjs": "7.0.0",
13
- "qrcode-terminal": "0.12.0",
14
11
  "react": "19.2.4",
15
12
  "react-devtools-core": "6.1.5",
16
13
  },
17
14
  "devDependencies": {
18
15
  "@types/bun": "1.3.11",
19
- "@types/pngjs": "6.0.5",
20
- "@types/qrcode-terminal": "0.12.2",
21
16
  "@types/react": "19.2.14",
22
17
  "eslint": "10.1.0",
23
18
  "knip": "5.88.1",
@@ -118,10 +113,6 @@
118
113
 
119
114
  "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
120
115
 
121
- "@types/pngjs": ["@types/pngjs@6.0.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ=="],
122
-
123
- "@types/qrcode-terminal": ["@types/qrcode-terminal@0.12.2", "", {}, "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q=="],
124
-
125
116
  "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
126
117
 
127
118
  "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="],
@@ -268,8 +259,6 @@
268
259
 
269
260
  "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
270
261
 
271
- "jsqr": ["jsqr@1.4.0", "", {}, "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="],
272
-
273
262
  "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
274
263
 
275
264
  "knip": ["knip@5.88.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.5.2", "strip-json-comments": "5.0.3", "unbash": "^2.2.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-tpy5o7zu1MjawVkLPuahymVJekYY3kYjvzcoInhIchgePxTlo+api90tBv2KfhAIe5uXh+mez1tAfmbv8/TiZg=="],
@@ -314,16 +303,12 @@
314
303
 
315
304
  "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
316
305
 
317
- "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
318
-
319
306
  "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
320
307
 
321
308
  "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
322
309
 
323
310
  "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
324
311
 
325
- "qrcode-terminal": ["qrcode-terminal@0.12.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ=="],
326
-
327
312
  "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
328
313
 
329
314
  "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -26,17 +26,12 @@
26
26
  "dependencies": {
27
27
  "chalk": "5.6.2",
28
28
  "ink": "6.8.0",
29
- "jsqr": "1.4.0",
30
29
  "nanoid": "5.1.7",
31
- "pngjs": "7.0.0",
32
- "qrcode-terminal": "0.12.0",
33
30
  "react": "19.2.4",
34
31
  "react-devtools-core": "6.1.5"
35
32
  },
36
33
  "devDependencies": {
37
34
  "@types/bun": "1.3.11",
38
- "@types/pngjs": "6.0.5",
39
- "@types/qrcode-terminal": "0.12.2",
40
35
  "@types/react": "19.2.14",
41
36
  "eslint": "10.1.0",
42
37
  "knip": "5.88.1",
@@ -64,6 +64,11 @@ const localRuntimeExportToGcsMock = spyOn(
64
64
  "localRuntimeExportToGcs",
65
65
  ).mockResolvedValue({ jobId: "platform-export-job-1" });
66
66
 
67
+ const localRuntimeIdentityMock = spyOn(
68
+ localRuntimeClient,
69
+ "localRuntimeIdentity",
70
+ ).mockResolvedValue({ version: "0.6.5" });
71
+
67
72
  const localRuntimePollJobStatusMock = spyOn(
68
73
  localRuntimeClient,
69
74
  "localRuntimePollJobStatus",
@@ -138,6 +143,8 @@ beforeEach(() => {
138
143
  localRuntimeExportToGcsMock.mockResolvedValue({
139
144
  jobId: "platform-export-job-1",
140
145
  });
146
+ localRuntimeIdentityMock.mockReset();
147
+ localRuntimeIdentityMock.mockResolvedValue({ version: "0.6.5" });
141
148
  localRuntimePollJobStatusMock.mockReset();
142
149
  localRuntimePollJobStatusMock.mockResolvedValue({
143
150
  jobId: "platform-export-job-1",
@@ -165,6 +172,7 @@ afterAll(() => {
165
172
  getPlatformUrlMock.mockRestore();
166
173
  platformRequestSignedUrlMock.mockRestore();
167
174
  localRuntimeExportToGcsMock.mockRestore();
175
+ localRuntimeIdentityMock.mockRestore();
168
176
  localRuntimePollJobStatusMock.mockRestore();
169
177
  getBackupsDirMock.mockRestore();
170
178
  mkdirSyncMock.mockRestore();
@@ -201,7 +209,11 @@ describe("vellum backup <platform-managed>: GCS happy path", () => {
201
209
 
202
210
  // Upload-URL request to the platform.
203
211
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
204
- expect.objectContaining({ operation: "upload" }),
212
+ expect.objectContaining({
213
+ operation: "upload",
214
+ minRuntimeVersion: "0.6.5",
215
+ maxRuntimeVersion: null,
216
+ }),
205
217
  "platform-token",
206
218
  "https://platform.vellum.ai",
207
219
  );
@@ -230,15 +242,24 @@ describe("vellum backup <platform-managed>: GCS happy path", () => {
230
242
  "platform-export-job-1",
231
243
  );
232
244
 
233
- // Download URL keyed off the upload's bundleKey.
245
+ // Download URL keyed off the upload's bundleKey. We deliberately do
246
+ // NOT send `targetRuntimeVersion` here — this backup downloads the
247
+ // bundle to disk for offline storage; there is no target runtime to
248
+ // gate against, and an older CLI must be able to download newer
249
+ // assistant backups.
234
250
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
235
- {
251
+ expect.objectContaining({
236
252
  operation: "download",
237
253
  bundleKey: "uploads/org-1/bundle-abc.vbundle",
238
- },
254
+ }),
239
255
  "platform-token",
240
256
  "https://platform.vellum.ai",
241
257
  );
258
+ const downloadCall = platformRequestSignedUrlMock.mock.calls.find(
259
+ (c) => (c[0] as { operation: string }).operation === "download",
260
+ );
261
+ expect(downloadCall).toBeDefined();
262
+ expect(downloadCall![0]).not.toHaveProperty("targetRuntimeVersion");
242
263
 
243
264
  // GCS fetch went directly to the signed download URL with no auth.
244
265
  const gcsFetch = globalThis.fetch as unknown as ReturnType<typeof mock>;
@@ -306,7 +327,11 @@ describe("vellum backup <platform-managed>: GCS happy path", () => {
306
327
  // runtimeUrl. The signed URLs returned by the platform target the
307
328
  // GCS bucket the runtime can reach, not the default platform's.
308
329
  expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
309
- expect.objectContaining({ operation: "upload" }),
330
+ expect.objectContaining({
331
+ operation: "upload",
332
+ minRuntimeVersion: "0.6.5",
333
+ maxRuntimeVersion: null,
334
+ }),
310
335
  "platform-token",
311
336
  "https://staging-platform.vellum.ai",
312
337
  );
@@ -473,3 +498,94 @@ describe("vellum backup <platform-managed>: failure cases", () => {
473
498
  }
474
499
  });
475
500
  });
501
+
502
+ // NOTE: The `VersionMismatchError handling` describe block was removed when
503
+ // backup stopped sending `targetRuntimeVersion` on the download signed-URL
504
+ // request — without that field the platform doesn't run the version gate,
505
+ // so 422 `version_mismatch` is no longer reachable from this code path.
506
+
507
+ // ---------------------------------------------------------------------------
508
+ // Source-runtime version is sourced from the daemon, not the CLI
509
+ // (Codex P1 regression guard for PR #29436)
510
+ // ---------------------------------------------------------------------------
511
+ describe("upload signed-URL records source runtime version (not CLI version)", () => {
512
+ test("identity is fetched BEFORE the upload signed-URL request", async () => {
513
+ findAssistantByNameMock.mockReturnValue(VELLUM_ENTRY);
514
+ setArgv("my-platform");
515
+
516
+ const callOrder: string[] = [];
517
+ localRuntimeIdentityMock.mockImplementationOnce(async () => {
518
+ callOrder.push("identity");
519
+ return { version: "0.5.9" };
520
+ });
521
+ platformRequestSignedUrlMock.mockImplementationOnce(async (params) => {
522
+ callOrder.push("signed-url");
523
+ return {
524
+ url: "https://storage.googleapis.com/bucket/signed-upload",
525
+ bundleKey: params.bundleKey ?? "uploads/org-1/bundle-abc.vbundle",
526
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
527
+ };
528
+ });
529
+
530
+ mockGcsDownload(new Uint8Array([1]));
531
+
532
+ await backup();
533
+
534
+ expect(callOrder[0]).toBe("identity");
535
+ expect(callOrder[1]).toBe("signed-url");
536
+
537
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
538
+ expect.objectContaining({
539
+ operation: "upload",
540
+ minRuntimeVersion: "0.5.9",
541
+ maxRuntimeVersion: null,
542
+ }),
543
+ "platform-token",
544
+ "https://platform.vellum.ai",
545
+ );
546
+ });
547
+
548
+ test("identity is fetched against the platform-managed runtime entry with the platform token", async () => {
549
+ findAssistantByNameMock.mockReturnValue(VELLUM_ENTRY);
550
+ setArgv("my-platform");
551
+
552
+ mockGcsDownload(new Uint8Array([1]));
553
+
554
+ await backup();
555
+
556
+ expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
557
+ expect.objectContaining({
558
+ cloud: "vellum",
559
+ runtimeUrl: "https://platform.vellum.ai",
560
+ assistantId: "11111111-2222-3333-4444-555555555555",
561
+ }),
562
+ "platform-token",
563
+ );
564
+ });
565
+
566
+ test("identity fetch failure aborts before signed-URL request", async () => {
567
+ findAssistantByNameMock.mockReturnValue(VELLUM_ENTRY);
568
+ setArgv("my-platform");
569
+
570
+ localRuntimeIdentityMock.mockRejectedValue(
571
+ new Error("Failed to fetch runtime identity: 503 Service Unavailable"),
572
+ );
573
+
574
+ const consoleErrorSpy = spyOn(console, "error").mockImplementation(
575
+ () => undefined,
576
+ );
577
+ try {
578
+ await expect(backup()).rejects.toThrow("process.exit:1");
579
+
580
+ // Signed-URL must NOT have been requested.
581
+ expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
582
+ expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
583
+
584
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
585
+ expect.stringContaining("Could not fetch runtime identity"),
586
+ );
587
+ } finally {
588
+ consoleErrorSpy.mockRestore();
589
+ }
590
+ });
591
+ });