@vellumai/assistant 0.12.7 → 0.12.8

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/openapi.yaml CHANGED
@@ -3,7 +3,7 @@
3
3
  openapi: 3.1.0
4
4
  info:
5
5
  title: Vellum Assistant API
6
- version: 0.12.7
6
+ version: 0.12.8
7
7
  description: Auto-generated OpenAPI specification for the Vellum Assistant runtime HTTP server.
8
8
  servers:
9
9
  - url: http://127.0.0.1:7821
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.7",
3
+ "version": "0.12.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -7,12 +7,11 @@
7
7
  * - Neither → "self-hosted-local".
8
8
  * - Regression: a logged-in local assistant holding managed-proxy
9
9
  * credentials (platform URL + assistant_api_key) must NOT be classified
10
- * "managed" — every logged-in local has those credentials, and a
11
- * "managed" stamp trips the importer's `secrets_redacted must be true
12
- * when origin.mode is 'managed'` refine, breaking local→platform
13
- * teleport. The helper keys on the IS_PLATFORM deployment env only —
14
- * nothing here consults the credential store, so the regression case is
15
- * simply "no env flags set".
10
+ * "managed": every logged-in local has those credentials, and importers
11
+ * drop every credential from a "managed" bundle, which would break
12
+ * local→platform teleport. The helper keys on the IS_PLATFORM deployment
13
+ * env only: nothing here consults the credential store, so the regression
14
+ * case is simply "no env flags set".
16
15
  */
17
16
 
18
17
  import { afterEach, describe, expect, test } from "bun:test";
@@ -79,6 +79,24 @@ describe("extractCredentialsFromBundle", () => {
79
79
  });
80
80
  });
81
81
 
82
+ test("returns no credentials from a managed-origin bundle", () => {
83
+ const entries = new Map<string, VBundleTarEntry>();
84
+ entries.set("credentials/openai-key", makeTarEntry("sk-test-123"));
85
+ entries.set(
86
+ "credentials/credential/vellum/assistant_api_key",
87
+ makeTarEntry("vak-test-456"),
88
+ );
89
+ const manifest = {
90
+ ...makeManifest([
91
+ "credentials/openai-key",
92
+ "credentials/credential/vellum/assistant_api_key",
93
+ ]),
94
+ origin: { mode: "managed" },
95
+ } as ManifestType;
96
+
97
+ expect(extractCredentialsFromBundle(entries, manifest)).toEqual([]);
98
+ });
99
+
82
100
  test("returns empty array when no credential entries exist", () => {
83
101
  const entries = new Map<string, VBundleTarEntry>();
84
102
  entries.set("workspace/config.json", makeTarEntry('{"test": true}'));
@@ -372,6 +372,49 @@ describe("streamCommitImport — happy path", () => {
372
372
  // Credentials must NOT appear on disk.
373
373
  expect(existsSync(join(workspaceDir, "credentials"))).toBe(false);
374
374
  });
375
+
376
+ test("imports a managed bundle that still carries credentials, without forwarding them", async () => {
377
+ // Hosted runtimes older than 0.10.8 packed credentials into managed
378
+ // exports and stamped secrets_redacted: false.
379
+ const { archive } = buildVBundle({
380
+ files: [
381
+ {
382
+ path: "data/db/assistant.db",
383
+ data: new Uint8Array(),
384
+ },
385
+ {
386
+ path: "workspace/config.json",
387
+ data: new TextEncoder().encode("{}"),
388
+ },
389
+ {
390
+ path: "credentials/openai-key",
391
+ data: new TextEncoder().encode("sk-test-1"),
392
+ },
393
+ {
394
+ path: "credentials/credential/vellum/assistant_api_key",
395
+ data: new TextEncoder().encode("vak-test-2"),
396
+ },
397
+ ],
398
+ ...defaultV1Options(),
399
+ origin: { mode: "managed" },
400
+ secretsRedacted: false,
401
+ });
402
+
403
+ const received: Array<{ account: string; value: string }> = [];
404
+ const result = await streamCommitImport({
405
+ source: readableFrom(archive),
406
+ pathResolver: new DefaultPathResolver(workspaceDir),
407
+ workspaceDir,
408
+ importCredentials: async (creds) => {
409
+ received.push(...creds);
410
+ },
411
+ });
412
+
413
+ expect(result.ok).toBe(true);
414
+ expect(received).toEqual([]);
415
+ expect(existsSync(join(workspaceDir, "credentials"))).toBe(false);
416
+ expect(existsSync(join(workspaceDir, "config.json"))).toBe(true);
417
+ });
375
418
  });
376
419
 
377
420
  // ---------------------------------------------------------------------------
@@ -174,6 +174,21 @@ describe("ManifestSchema — v1 acceptance", () => {
174
174
  const result = validateVBundle(archive);
175
175
  expect(result.is_valid).toBe(true);
176
176
  });
177
+
178
+ test("accepts when origin.mode === 'managed' and secrets_redacted === false", () => {
179
+ // Hosted runtimes older than 0.10.8 stamp managed exports this way.
180
+ // Importers drop the credentials of any managed bundle instead.
181
+ const manifest = withChecksum({
182
+ ...v1Skeleton(),
183
+ origin: { mode: "managed" },
184
+ secrets_redacted: false,
185
+ });
186
+ const archive = gzipTarOf(manifest, [
187
+ { name: "data/db/assistant.db", data: DB_BYTES },
188
+ ]);
189
+ const result = validateVBundle(archive);
190
+ expect(result.is_valid).toBe(true);
191
+ });
177
192
  });
178
193
 
179
194
  describe("validateVBundle: streaming manifest limits", () => {
@@ -232,19 +247,6 @@ describe("ManifestSchema — v1 rejection", () => {
232
247
  expect(result.is_valid).toBe(false);
233
248
  });
234
249
 
235
- test("rejects managed mode with secrets_redacted=false", () => {
236
- const manifest = withChecksum({
237
- ...v1Skeleton(),
238
- origin: { mode: "managed" },
239
- secrets_redacted: false,
240
- });
241
- const archive = gzipTarOf(manifest, [
242
- { name: "data/db/assistant.db", data: DB_BYTES },
243
- ]);
244
- const result = validateVBundle(archive);
245
- expect(result.is_valid).toBe(false);
246
- });
247
-
248
250
  test("rejects manifest whose contents lacks data/db/assistant.db", () => {
249
251
  const manifest = withChecksum({
250
252
  ...v1Skeleton(),
@@ -12,10 +12,10 @@
12
12
  * Deliberately NOT keyed on managed-proxy prerequisites: any logged-in
13
13
  * local assistant holds a platform URL and `assistant_api_key` (to use the
14
14
  * platform LLM proxy), so proxy-prereq presence does not distinguish a
15
- * managed deployment from a local one. Stamping a local export as
16
- * "managed" would trip the importer's `secrets_redacted must be true when
17
- * origin.mode is 'managed'` rule and break local→platform teleport for
18
- * every logged-in local assistant.
15
+ * managed deployment from a local one. Importers never apply a managed
16
+ * bundle's credentials (`importsBundledCredentials`), so stamping a local
17
+ * export as "managed" would drop every credential on local→platform
18
+ * teleport for every logged-in local assistant.
19
19
  */
20
20
 
21
21
  import {
@@ -36,7 +36,10 @@ import type { RuntimeCompatibility } from "./vbundle-import-policy.js";
36
36
  import * as policy from "./vbundle-import-policy.js";
37
37
  import { mergeMetadataPreservingVellum } from "./vbundle-metadata-merge.js";
38
38
  import type { ManifestType, VBundleTarEntry } from "./vbundle-validator.js";
39
- import { validateVBundle } from "./vbundle-validator.js";
39
+ import {
40
+ importsBundledCredentials,
41
+ validateVBundle,
42
+ } from "./vbundle-validator.js";
40
43
 
41
44
  const log = getLogger("vbundle-importer");
42
45
 
@@ -780,12 +783,16 @@ export function commitImport(options: ImportCommitOptions): ImportCommitResult {
780
783
  *
781
784
  * Credentials are stored under the `credentials/` prefix in the archive,
782
785
  * where the remainder of the path is the account name and the entry data
783
- * is the credential value.
786
+ * is the credential value. Returns none for a bundle whose credentials an
787
+ * import never applies (see `importsBundledCredentials`).
784
788
  */
785
789
  export function extractCredentialsFromBundle(
786
790
  entries: Map<string, VBundleTarEntry>,
787
791
  manifest: ManifestType,
788
792
  ): Array<{ account: string; value: string }> {
793
+ if (!importsBundledCredentials(manifest)) {
794
+ return [];
795
+ }
789
796
  const manifestPaths = new Set(manifest.contents.map((f) => f.path));
790
797
  const credentials: Array<{ account: string; value: string }> = [];
791
798
  for (const [path, entry] of entries) {
@@ -70,6 +70,7 @@ import {
70
70
  } from "./vbundle-streaming-validator.js";
71
71
  import { parseVBundleStream } from "./vbundle-tar-stream.js";
72
72
  import {
73
+ importsBundledCredentials,
73
74
  MANIFEST_MAX_ENTRIES,
74
75
  type ManifestType,
75
76
  } from "./vbundle-validator.js";
@@ -648,14 +649,16 @@ export async function streamCommitImport(
648
649
  if (archivePath.startsWith("credentials/")) {
649
650
  // Credentials are hash-verified against the manifest but collected
650
651
  // in memory rather than written to disk. DefaultPathResolver
651
- // deliberately returns null for these paths.
652
+ // deliberately returns null for these paths. A bundle whose
653
+ // credentials an import never applies is still verified, so a
654
+ // corrupt entry fails the import either way.
652
655
  const buffered = await collectHashVerified(entry.body, {
653
656
  sha256: expectedEntry.sha256,
654
657
  size: expectedEntry.size,
655
658
  archivePath,
656
659
  });
657
660
  const account = archivePath.slice("credentials/".length);
658
- if (account) {
661
+ if (account && manifest && importsBundledCredentials(manifest)) {
659
662
  bufferedCredentials.push({
660
663
  account,
661
664
  value: new TextDecoder().decode(buffered),
@@ -69,10 +69,6 @@ export const ManifestSchema = z
69
69
  secrets_redacted: z.boolean(),
70
70
  export_options: ExportOptions,
71
71
  })
72
- .refine((m) => m.origin.mode !== "managed" || m.secrets_redacted === true, {
73
- message: "secrets_redacted must be true when origin.mode is 'managed'",
74
- path: ["secrets_redacted"],
75
- })
76
72
  .refine(
77
73
  (m) =>
78
74
  m.contents.some(
@@ -90,6 +86,19 @@ export const ManifestSchema = z
90
86
  export type ManifestFileEntryType = z.infer<typeof ManifestFileEntry>;
91
87
  export type ManifestType = z.infer<typeof ManifestSchema>;
92
88
 
89
+ /**
90
+ * Whether an import applies the bundle's `credentials/` entries.
91
+ *
92
+ * A managed-origin bundle's credentials never reach the target. They are
93
+ * platform-issued or bound to the managed deployment, and the target
94
+ * re-provisions its own platform identity. Managed exports from hosted
95
+ * runtimes older than 0.10.8 carry credentials (`secrets_redacted: false`);
96
+ * dropping them here keeps those bundles restorable.
97
+ */
98
+ export function importsBundledCredentials(manifest: ManifestType): boolean {
99
+ return manifest.origin.mode !== "managed";
100
+ }
101
+
93
102
  // ---------------------------------------------------------------------------
94
103
  // Legacy manifest schema (pre-v1, six-field shape)
95
104
  // ---------------------------------------------------------------------------
@@ -272,9 +272,9 @@ async function resolveAssistantId(): Promise<string> {
272
272
  *
273
273
  * Managed-mode exports never reach this with credentials: both export
274
274
  * handlers skip credential collection entirely when `origin.mode` is
275
- * "managed" (the validator's cross-field refine rejects a managed bundle
276
- * with `secrets_redacted: false`, and teleport clients re-provision
277
- * platform identity after a platform→local import).
275
+ * "managed" (importers never apply a managed bundle's credentials, and
276
+ * teleport clients re-provision platform identity after a platform→local
277
+ * import).
278
278
  */
279
279
  export function computeSecretsRedacted(
280
280
  credentialCount: number,
@@ -380,11 +380,10 @@ export async function handleMigrationExport(
380
380
  try {
381
381
  const manifestInputs = await buildExportManifestInputs();
382
382
 
383
- // Managed deployments never ship credentials in bundles — the manifest
384
- // schema forbids it (`secrets_redacted` must be true when `origin.mode`
385
- // is "managed"), and teleport clients re-provision platform identity
386
- // after a platform→local import. Redact instead of emitting a bundle
387
- // the importer is guaranteed to reject.
383
+ // Managed deployments never ship credentials in bundles: importers
384
+ // never apply a managed bundle's credentials (`importsBundledCredentials`),
385
+ // and teleport clients re-provision platform identity after a
386
+ // platform→local import.
388
387
  let credentials: Array<{ account: string; value: string }> = [];
389
388
  let secretsRedacted = true;
390
389
  if (manifestInputs.origin.mode !== "managed") {
@@ -639,11 +638,9 @@ export async function handleMigrationExportToGcs({ body }: RouteHandlerArgs) {
639
638
  }
640
639
 
641
640
  // ── 3. Collect credentials up front. Fail closed → 500. Managed
642
- // deployments never ship credentials in bundles — the manifest schema
643
- // forbids it (`secrets_redacted` must be true when `origin.mode` is
644
- // "managed"), and teleport clients re-provision platform identity after
645
- // a platform→local import — so redact instead of emitting a bundle the
646
- // importer is guaranteed to reject.
641
+ // deployments never ship credentials in bundles: importers never apply a
642
+ // managed bundle's credentials (`importsBundledCredentials`), and teleport
643
+ // clients re-provision platform identity after a platform→local import.
647
644
  //
648
645
  // A debug bundle goes to Vellum staff, so it never carries credentials
649
646
  // either, whatever the deployment mode.