@vellumai/assistant 0.12.7-staging.1 → 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 +1 -1
- package/package.json +1 -1
- package/src/runtime/migrations/__tests__/origin-mode.test.ts +5 -6
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +18 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +43 -0
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +15 -13
- package/src/runtime/migrations/origin-mode.ts +4 -4
- package/src/runtime/migrations/vbundle-importer.ts +9 -2
- package/src/runtime/migrations/vbundle-streaming-importer.ts +5 -2
- package/src/runtime/migrations/vbundle-validator.ts +13 -4
- package/src/runtime/routes/migration-routes.ts +10 -13
package/openapi.yaml
CHANGED
package/package.json
CHANGED
|
@@ -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"
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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 {
|
|
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" (
|
|
276
|
-
*
|
|
277
|
-
*
|
|
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
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
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
|
|
643
|
-
//
|
|
644
|
-
//
|
|
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.
|