@mycadet/assistant 0.12.11 → 0.13.0
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/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/openapi.yaml +2 -708
- package/package.json +2 -3
- package/scripts/sync-web-search-catalog.ts +1 -2
- package/src/__tests__/cloud-profile-guard.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +6 -38
- package/src/__tests__/conversation-queue.test.ts +45 -6
- package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
- package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
- package/src/__tests__/persona-resolver.test.ts +0 -51
- package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
- package/src/__tests__/run-due-schedules.test.ts +0 -4
- package/src/__tests__/schedule-store.test.ts +0 -12
- package/src/__tests__/stt-stream-session.test.ts +4 -4
- package/src/__tests__/telephony-tts-guard.test.ts +2 -50
- package/src/__tests__/voice-config-update.test.ts +12 -167
- package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
- package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
- package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
- package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
- package/src/calls/resolve-call-tts-provider.ts +6 -25
- package/src/calls/telephony-credential-preflight.ts +0 -10
- package/src/calls/telephony-tts-capability.ts +1 -17
- package/src/cli/commands/tts.ts +1 -2
- package/src/cli/index.help.ts +0 -2
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
- package/src/config/schemas/__tests__/stt.test.ts +10 -14
- package/src/config/schemas/services.ts +5 -6
- package/src/config/schemas/stt.ts +1 -3
- package/src/config/schemas/tts.ts +1 -30
- package/src/daemon/daemon-readiness.ts +4 -29
- package/src/daemon/lifecycle.ts +0 -31
- package/src/daemon/providers-setup.ts +0 -8
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -66
- package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
- package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
- package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
- package/src/live-voice/live-voice-credential-preflight.ts +9 -33
- package/src/live-voice/live-voice-session.ts +10 -6
- package/src/live-voice/live-voice-tts.ts +1 -8
- package/src/plugins/defaults/memory/worker.ts +3 -3
- package/src/prompts/persona-resolver.ts +0 -35
- package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
- package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
- package/src/providers/search-provider-catalog.ts +7 -39
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
- package/src/providers/speech-to-text/provider-catalog.ts +2 -73
- package/src/providers/speech-to-text/resolve.ts +3 -120
- package/src/runtime/http-router-types.ts +1 -3
- package/src/runtime/http-server.ts +1 -1
- package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
- package/src/runtime/routes/index.ts +0 -4
- package/src/runtime/routes/live-voice-routes.test.ts +4 -11
- package/src/runtime/routes/live-voice-routes.ts +8 -11
- package/src/runtime/routes/secret-routes.ts +0 -6
- package/src/runtime/routes/tts-routes.ts +0 -38
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
- package/src/schedule/schedule-store.ts +0 -7
- package/src/schedule/scheduler.ts +0 -5
- package/src/schedule/worker.ts +1 -1
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
- package/src/stt/daemon-batch-transcriber.ts +4 -38
- package/src/stt/types.ts +1 -9
- package/src/tools/network/__tests__/web-search.test.ts +3 -250
- package/src/tools/network/web-search.ts +4 -165
- package/src/tts/__tests__/provider-resolution.test.ts +0 -5
- package/src/tts/provider-catalog.ts +0 -4
- package/src/tts/types.ts +0 -1
- package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
- package/src/__tests__/migration-export-http.test.ts +0 -557
- package/src/__tests__/migration-export-streaming.test.ts +0 -350
- package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
- package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
- package/src/__tests__/migration-import-from-path.test.ts +0 -324
- package/src/__tests__/migration-import-from-url.test.ts +0 -588
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
- package/src/__tests__/migration-jobs-status.test.ts +0 -139
- package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
- package/src/__tests__/migration-transport.test.ts +0 -1089
- package/src/__tests__/migration-validate-http.test.ts +0 -634
- package/src/__tests__/migration-wizard.test.ts +0 -1395
- package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
- package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
- package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
- package/src/__tests__/validation-results-screen.test.ts +0 -1130
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
- package/src/background-wake/background-wake-routes.test.ts +0 -906
- package/src/background-wake/cloud-client.test.ts +0 -328
- package/src/background-wake/cloud-client.ts +0 -196
- package/src/background-wake/next-wake.test.ts +0 -335
- package/src/background-wake/next-wake.ts +0 -188
- package/src/background-wake/publisher.ts +0 -93
- package/src/background-wake/wake-intent-hooks.test.ts +0 -234
- package/src/cli/commands/__tests__/backup.test.ts +0 -363
- package/src/cli/commands/backup.help.ts +0 -180
- package/src/cli/commands/backup.ts +0 -398
- package/src/cloud/cloud-speech.test.ts +0 -335
- package/src/cloud/cloud-speech.ts +0 -300
- package/src/config/cloud-speech-defaults.ts +0 -300
- package/src/config/sanitize-for-transfer.ts +0 -111
- package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
- package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
- package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
- package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
- package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
- package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
- package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
- package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
- package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
- package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
- package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
- package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
- package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
- package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
- package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
- package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
- package/src/runtime/migrations/gcs-signed-url.ts +0 -190
- package/src/runtime/migrations/job-registry.ts +0 -297
- package/src/runtime/migrations/migration-transport.ts +0 -612
- package/src/runtime/migrations/migration-wizard.ts +0 -747
- package/src/runtime/migrations/origin-mode.ts +0 -42
- package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
- package/src/runtime/migrations/staged-import-path.ts +0 -116
- package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
- package/src/runtime/migrations/validation-results-screen.ts +0 -469
- package/src/runtime/migrations/vbundle-builder.ts +0 -1382
- package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
- package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
- package/src/runtime/migrations/vbundle-importer.ts +0 -826
- package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
- package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
- package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
- package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
- package/src/runtime/migrations/vbundle-validator.ts +0 -724
- package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
- package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
- package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
- package/src/runtime/routes/background-wake-routes.ts +0 -387
- package/src/runtime/routes/migration-routes.ts +0 -2584
- package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
- package/src/tools/network/cloud-search-proxy.ts +0 -211
- package/src/tts/__tests__/cadet-provider.test.ts +0 -253
- package/src/tts/providers/cadet-provider.ts +0 -244
- package/src/types/tar-stream.d.ts +0 -66
|
@@ -1,598 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the streaming validator primitives:
|
|
3
|
-
* - `readAndValidateManifest` consumes the first tar entry and runs the
|
|
4
|
-
* full manifest validation pipeline.
|
|
5
|
-
* - `createHashVerifier` is a passthrough Transform that aborts with a
|
|
6
|
-
* typed error on digest/size mismatch.
|
|
7
|
-
*
|
|
8
|
-
* Happy-path fixtures are built with the existing `buildVBundle` helper.
|
|
9
|
-
* Negative-path fixtures (non-manifest first, oversize manifest, malformed
|
|
10
|
-
* JSON, schema fail, sha mismatch) are hand-constructed because
|
|
11
|
-
* `buildVBundle` always produces valid, manifest-first archives.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { createHash } from "node:crypto";
|
|
15
|
-
import { Readable } from "node:stream";
|
|
16
|
-
import { pipeline } from "node:stream/promises";
|
|
17
|
-
import { gzipSync } from "node:zlib";
|
|
18
|
-
import { describe, expect, test } from "bun:test";
|
|
19
|
-
|
|
20
|
-
import { buildVBundle } from "../vbundle-builder.js";
|
|
21
|
-
import {
|
|
22
|
-
createHashVerifier,
|
|
23
|
-
readAndValidateManifest,
|
|
24
|
-
StreamingValidationError,
|
|
25
|
-
} from "../vbundle-streaming-validator.js";
|
|
26
|
-
import {
|
|
27
|
-
parseVBundleStream,
|
|
28
|
-
type StreamedTarEntry,
|
|
29
|
-
} from "../vbundle-tar-stream.js";
|
|
30
|
-
import {
|
|
31
|
-
computeLegacyManifestSha256,
|
|
32
|
-
computeManifestChecksum,
|
|
33
|
-
MANIFEST_MAX_BYTES,
|
|
34
|
-
} from "../vbundle-validator.js";
|
|
35
|
-
import { defaultV1Options } from "./v1-test-helpers.js";
|
|
36
|
-
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Helpers
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
const BLOCK_SIZE = 512;
|
|
42
|
-
|
|
43
|
-
function readableFromBuffer(buf: Uint8Array): Readable {
|
|
44
|
-
return Readable.from([Buffer.from(buf)]);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Minimal hand-rolled tar entry builder (ustar regular file). */
|
|
48
|
-
function buildTarEntry(name: string, data: Uint8Array): Uint8Array {
|
|
49
|
-
const encoder = new TextEncoder();
|
|
50
|
-
const nameBytes = encoder.encode(name);
|
|
51
|
-
if (nameBytes.length > 100) {
|
|
52
|
-
throw new Error(`test helper: name too long (${nameBytes.length} bytes)`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const header = new Uint8Array(BLOCK_SIZE);
|
|
56
|
-
header.set(nameBytes, 0);
|
|
57
|
-
|
|
58
|
-
const writeOctal = (offset: number, length: number, value: number) => {
|
|
59
|
-
const str = value.toString(8).padStart(length - 1, "0");
|
|
60
|
-
for (let i = 0; i < str.length; i++) {
|
|
61
|
-
header[offset + i] = str.charCodeAt(i);
|
|
62
|
-
}
|
|
63
|
-
header[offset + length - 1] = 0;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
writeOctal(100, 8, 0o644); // mode
|
|
67
|
-
writeOctal(108, 8, 0); // uid
|
|
68
|
-
writeOctal(116, 8, 0); // gid
|
|
69
|
-
writeOctal(124, 12, data.length); // size
|
|
70
|
-
writeOctal(136, 12, Math.floor(Date.now() / 1000)); // mtime
|
|
71
|
-
header[156] = "0".charCodeAt(0); // typeflag = regular file
|
|
72
|
-
|
|
73
|
-
const magic = encoder.encode("ustar\0");
|
|
74
|
-
header.set(magic, 257);
|
|
75
|
-
header[263] = "0".charCodeAt(0);
|
|
76
|
-
header[264] = "0".charCodeAt(0);
|
|
77
|
-
|
|
78
|
-
// Header checksum: sum of all bytes with the checksum field treated
|
|
79
|
-
// as 8 ASCII spaces.
|
|
80
|
-
let sum = 0;
|
|
81
|
-
for (let i = 0; i < BLOCK_SIZE; i++) {
|
|
82
|
-
sum += i >= 148 && i < 156 ? 0x20 : header[i];
|
|
83
|
-
}
|
|
84
|
-
writeOctal(148, 7, sum);
|
|
85
|
-
header[155] = 0x20;
|
|
86
|
-
|
|
87
|
-
// Pad data out to the next 512-byte boundary.
|
|
88
|
-
const remainder = data.length % BLOCK_SIZE;
|
|
89
|
-
const padded =
|
|
90
|
-
remainder === 0
|
|
91
|
-
? data
|
|
92
|
-
: (() => {
|
|
93
|
-
const out = new Uint8Array(data.length + (BLOCK_SIZE - remainder));
|
|
94
|
-
out.set(data, 0);
|
|
95
|
-
return out;
|
|
96
|
-
})();
|
|
97
|
-
|
|
98
|
-
const entry = new Uint8Array(header.length + padded.length);
|
|
99
|
-
entry.set(header, 0);
|
|
100
|
-
entry.set(padded, header.length);
|
|
101
|
-
return entry;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Hand-build a gzipped tar archive from the given entries (no manifest injected). */
|
|
105
|
-
function buildRawVBundle(
|
|
106
|
-
entries: Array<{ name: string; data: Uint8Array }>,
|
|
107
|
-
): Uint8Array {
|
|
108
|
-
const parts: Uint8Array[] = entries.map((e) => buildTarEntry(e.name, e.data));
|
|
109
|
-
// End-of-archive marker: two zero blocks.
|
|
110
|
-
parts.push(new Uint8Array(BLOCK_SIZE * 2));
|
|
111
|
-
|
|
112
|
-
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
113
|
-
const tar = new Uint8Array(total);
|
|
114
|
-
let offset = 0;
|
|
115
|
-
for (const p of parts) {
|
|
116
|
-
tar.set(p, offset);
|
|
117
|
-
offset += p.length;
|
|
118
|
-
}
|
|
119
|
-
return gzipSync(tar);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Fetch the first entry of a streaming archive; drain+close the iterator after. */
|
|
123
|
-
async function firstEntryOf(
|
|
124
|
-
archive: Uint8Array,
|
|
125
|
-
): Promise<{ entry: StreamedTarEntry; drainRest: () => Promise<void> }> {
|
|
126
|
-
const iter = parseVBundleStream(readableFromBuffer(archive));
|
|
127
|
-
const first = await iter.next();
|
|
128
|
-
if (first.done || !first.value) {
|
|
129
|
-
throw new Error("archive contained no entries");
|
|
130
|
-
}
|
|
131
|
-
const drainRest = async () => {
|
|
132
|
-
for await (const rest of iter) {
|
|
133
|
-
rest.body.resume();
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
return { entry: first.value, drainRest };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
// readAndValidateManifest — happy path
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
|
|
143
|
-
describe("readAndValidateManifest — happy path", () => {
|
|
144
|
-
test("parses manifest and populates expected map from manifest.contents", async () => {
|
|
145
|
-
const fileA = new TextEncoder().encode("alpha payload\n");
|
|
146
|
-
const fileB = new TextEncoder().encode("beta payload\n");
|
|
147
|
-
const { archive, manifest } = buildVBundle({
|
|
148
|
-
files: [
|
|
149
|
-
{
|
|
150
|
-
path: "data/db/assistant.db",
|
|
151
|
-
data: new Uint8Array(),
|
|
152
|
-
},
|
|
153
|
-
{ path: "workspace/a.txt", data: fileA },
|
|
154
|
-
{ path: "workspace/b.txt", data: fileB },
|
|
155
|
-
],
|
|
156
|
-
...defaultV1Options(),
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const { entry, drainRest } = await firstEntryOf(archive);
|
|
160
|
-
const result = await readAndValidateManifest(entry);
|
|
161
|
-
await drainRest();
|
|
162
|
-
|
|
163
|
-
expect(result.manifest.schema_version).toBe(manifest.schema_version);
|
|
164
|
-
// Includes the synthetic data/db/assistant.db entry plus a/b.txt.
|
|
165
|
-
expect(result.manifest.contents).toHaveLength(3);
|
|
166
|
-
expect(result.manifest.checksum).toBe(manifest.checksum);
|
|
167
|
-
|
|
168
|
-
expect(result.expected.size).toBe(3);
|
|
169
|
-
const expectA = result.expected.get("workspace/a.txt");
|
|
170
|
-
expect(expectA?.size).toBe(fileA.length);
|
|
171
|
-
expect(expectA?.sha256).toBe(
|
|
172
|
-
manifest.contents.find((f) => f.path === "workspace/a.txt")?.sha256,
|
|
173
|
-
);
|
|
174
|
-
const expectB = result.expected.get("workspace/b.txt");
|
|
175
|
-
expect(expectB?.size).toBe(fileB.length);
|
|
176
|
-
expect(expectB?.sha256).toBe(
|
|
177
|
-
manifest.contents.find((f) => f.path === "workspace/b.txt")?.sha256,
|
|
178
|
-
);
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
// readAndValidateManifest — negative paths
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
|
|
186
|
-
describe("readAndValidateManifest — negative paths", () => {
|
|
187
|
-
test("throws manifest_not_first when first entry is not manifest.json", async () => {
|
|
188
|
-
const archive = buildRawVBundle([
|
|
189
|
-
{ name: "workspace/a.txt", data: new TextEncoder().encode("hello") },
|
|
190
|
-
]);
|
|
191
|
-
const { entry, drainRest } = await firstEntryOf(archive);
|
|
192
|
-
|
|
193
|
-
let err: StreamingValidationError | null = null;
|
|
194
|
-
try {
|
|
195
|
-
await readAndValidateManifest(entry);
|
|
196
|
-
} catch (e) {
|
|
197
|
-
err = e as StreamingValidationError;
|
|
198
|
-
}
|
|
199
|
-
await drainRest();
|
|
200
|
-
|
|
201
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
202
|
-
expect(err?.code).toBe("manifest_not_first");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test("throws manifest_too_large and fails fast before draining the whole body", async () => {
|
|
206
|
-
// Fake a tar entry whose body would emit 10x the cap if fully drained.
|
|
207
|
-
// The validator must destroy() the stream and throw the moment the
|
|
208
|
-
// running byte count crosses MANIFEST_MAX_BYTES; it must NOT keep
|
|
209
|
-
// pulling chunks.
|
|
210
|
-
//
|
|
211
|
-
// We count bytes emitted via a _read implementation, and after the
|
|
212
|
-
// throw assert both that destroy() fired and that far less than the
|
|
213
|
-
// full body was ever pulled out of the stream.
|
|
214
|
-
const CHUNK = MANIFEST_MAX_BYTES / 2;
|
|
215
|
-
const TOTAL_CHUNKS = 20;
|
|
216
|
-
let chunksEmitted = 0;
|
|
217
|
-
let bytesEmitted = 0;
|
|
218
|
-
const body = new Readable({
|
|
219
|
-
read() {
|
|
220
|
-
if (chunksEmitted >= TOTAL_CHUNKS) {
|
|
221
|
-
this.push(null);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const buf = Buffer.alloc(CHUNK, 0x20);
|
|
225
|
-
chunksEmitted += 1;
|
|
226
|
-
bytesEmitted += buf.length;
|
|
227
|
-
this.push(buf);
|
|
228
|
-
},
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
const entry: StreamedTarEntry = {
|
|
232
|
-
header: {
|
|
233
|
-
name: "manifest.json",
|
|
234
|
-
size: CHUNK * TOTAL_CHUNKS,
|
|
235
|
-
type: "file",
|
|
236
|
-
},
|
|
237
|
-
body,
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
let err: StreamingValidationError | null = null;
|
|
241
|
-
try {
|
|
242
|
-
await readAndValidateManifest(entry);
|
|
243
|
-
} catch (e) {
|
|
244
|
-
err = e as StreamingValidationError;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
248
|
-
expect(err?.code).toBe("manifest_too_large");
|
|
249
|
-
// Fail-fast assertions: destroy() was called, and we didn't drain past
|
|
250
|
-
// the cap + one chunk. If the validator had drained to EOF we'd see all
|
|
251
|
-
// 20 chunks here.
|
|
252
|
-
expect(body.destroyed).toBe(true);
|
|
253
|
-
expect(chunksEmitted).toBeLessThanOrEqual(3);
|
|
254
|
-
expect(bytesEmitted).toBeLessThan(2 * MANIFEST_MAX_BYTES);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test("throws bundle_too_many_entries when manifest.contents exceeds maxEntries", async () => {
|
|
258
|
-
const { archive } = buildVBundle({
|
|
259
|
-
files: [
|
|
260
|
-
{ path: "data/db/assistant.db", data: new Uint8Array() },
|
|
261
|
-
{ path: "workspace/a.txt", data: new TextEncoder().encode("a") },
|
|
262
|
-
{ path: "workspace/b.txt", data: new TextEncoder().encode("b") },
|
|
263
|
-
],
|
|
264
|
-
...defaultV1Options(),
|
|
265
|
-
});
|
|
266
|
-
const { entry, drainRest } = await firstEntryOf(archive);
|
|
267
|
-
|
|
268
|
-
let err: StreamingValidationError | null = null;
|
|
269
|
-
try {
|
|
270
|
-
await readAndValidateManifest(entry, { maxEntries: 2 });
|
|
271
|
-
} catch (e) {
|
|
272
|
-
err = e as StreamingValidationError;
|
|
273
|
-
}
|
|
274
|
-
await drainRest();
|
|
275
|
-
|
|
276
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
277
|
-
expect(err?.code).toBe("bundle_too_many_entries");
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test("throws manifest_malformed when manifest body is not valid JSON", async () => {
|
|
281
|
-
const archive = buildRawVBundle([
|
|
282
|
-
{ name: "manifest.json", data: new TextEncoder().encode("{not-json") },
|
|
283
|
-
]);
|
|
284
|
-
const { entry, drainRest } = await firstEntryOf(archive);
|
|
285
|
-
|
|
286
|
-
let err: StreamingValidationError | null = null;
|
|
287
|
-
try {
|
|
288
|
-
await readAndValidateManifest(entry);
|
|
289
|
-
} catch (e) {
|
|
290
|
-
err = e as StreamingValidationError;
|
|
291
|
-
}
|
|
292
|
-
await drainRest();
|
|
293
|
-
|
|
294
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
295
|
-
expect(err?.code).toBe("manifest_malformed");
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test("throws manifest_schema when a required field is missing", async () => {
|
|
299
|
-
// Valid JSON but missing `files`, `manifest_sha256`, etc.
|
|
300
|
-
const bogus = new TextEncoder().encode(
|
|
301
|
-
JSON.stringify({ schema_version: "1.0", created_at: "now" }),
|
|
302
|
-
);
|
|
303
|
-
const archive = buildRawVBundle([{ name: "manifest.json", data: bogus }]);
|
|
304
|
-
const { entry, drainRest } = await firstEntryOf(archive);
|
|
305
|
-
|
|
306
|
-
let err: StreamingValidationError | null = null;
|
|
307
|
-
try {
|
|
308
|
-
await readAndValidateManifest(entry);
|
|
309
|
-
} catch (e) {
|
|
310
|
-
err = e as StreamingValidationError;
|
|
311
|
-
}
|
|
312
|
-
await drainRest();
|
|
313
|
-
|
|
314
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
315
|
-
expect(err?.code).toBe("manifest_schema");
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test("throws manifest_sha256 when the declared digest doesn't match canonical JSON", async () => {
|
|
319
|
-
const badManifest = {
|
|
320
|
-
schema_version: 1,
|
|
321
|
-
bundle_id: "00000000-0000-4000-8000-000000000000",
|
|
322
|
-
created_at: new Date().toISOString(),
|
|
323
|
-
assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
|
|
324
|
-
origin: { mode: "self-hosted-local" },
|
|
325
|
-
compatibility: {
|
|
326
|
-
min_runtime_version: "0.0.0-test",
|
|
327
|
-
max_runtime_version: null,
|
|
328
|
-
},
|
|
329
|
-
contents: [
|
|
330
|
-
{
|
|
331
|
-
path: "data/db/assistant.db",
|
|
332
|
-
sha256:
|
|
333
|
-
"1111111111111111111111111111111111111111111111111111111111111111",
|
|
334
|
-
size_bytes: 10,
|
|
335
|
-
},
|
|
336
|
-
],
|
|
337
|
-
// Deliberately wrong digest. The canonical hash of a v1 manifest
|
|
338
|
-
// won't match this, regardless of ordering.
|
|
339
|
-
checksum:
|
|
340
|
-
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
341
|
-
secrets_redacted: false,
|
|
342
|
-
export_options: {
|
|
343
|
-
include_logs: false,
|
|
344
|
-
include_browser_state: false,
|
|
345
|
-
include_memory_vectors: false,
|
|
346
|
-
},
|
|
347
|
-
};
|
|
348
|
-
const archive = buildRawVBundle([
|
|
349
|
-
{
|
|
350
|
-
name: "manifest.json",
|
|
351
|
-
data: new TextEncoder().encode(JSON.stringify(badManifest)),
|
|
352
|
-
},
|
|
353
|
-
]);
|
|
354
|
-
const { entry, drainRest } = await firstEntryOf(archive);
|
|
355
|
-
|
|
356
|
-
let err: StreamingValidationError | null = null;
|
|
357
|
-
try {
|
|
358
|
-
await readAndValidateManifest(entry);
|
|
359
|
-
} catch (e) {
|
|
360
|
-
err = e as StreamingValidationError;
|
|
361
|
-
}
|
|
362
|
-
await drainRest();
|
|
363
|
-
|
|
364
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
365
|
-
expect(err?.code).toBe("manifest_sha256");
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
test("throws manifest_duplicate_path when the same archive path appears twice", async () => {
|
|
369
|
-
const baseManifest = {
|
|
370
|
-
schema_version: 1,
|
|
371
|
-
bundle_id: "00000000-0000-4000-8000-000000000001",
|
|
372
|
-
created_at: new Date().toISOString(),
|
|
373
|
-
assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
|
|
374
|
-
origin: { mode: "self-hosted-local" },
|
|
375
|
-
compatibility: {
|
|
376
|
-
min_runtime_version: "0.0.0-test",
|
|
377
|
-
max_runtime_version: null,
|
|
378
|
-
},
|
|
379
|
-
contents: [
|
|
380
|
-
{
|
|
381
|
-
path: "data/db/assistant.db",
|
|
382
|
-
sha256:
|
|
383
|
-
"1111111111111111111111111111111111111111111111111111111111111111",
|
|
384
|
-
size_bytes: 10,
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
// Deliberately duplicate path — malicious bundle could exploit
|
|
388
|
-
// this to bypass per-entry integrity checks if we silently
|
|
389
|
-
// collapsed.
|
|
390
|
-
path: "data/db/assistant.db",
|
|
391
|
-
sha256:
|
|
392
|
-
"2222222222222222222222222222222222222222222222222222222222222222",
|
|
393
|
-
size_bytes: 20,
|
|
394
|
-
},
|
|
395
|
-
],
|
|
396
|
-
checksum: "",
|
|
397
|
-
secrets_redacted: false,
|
|
398
|
-
export_options: {
|
|
399
|
-
include_logs: false,
|
|
400
|
-
include_browser_state: false,
|
|
401
|
-
include_memory_vectors: false,
|
|
402
|
-
},
|
|
403
|
-
};
|
|
404
|
-
baseManifest.checksum = computeManifestChecksum(baseManifest);
|
|
405
|
-
|
|
406
|
-
const archive = buildRawVBundle([
|
|
407
|
-
{
|
|
408
|
-
name: "manifest.json",
|
|
409
|
-
data: new TextEncoder().encode(JSON.stringify(baseManifest)),
|
|
410
|
-
},
|
|
411
|
-
]);
|
|
412
|
-
const { entry, drainRest } = await firstEntryOf(archive);
|
|
413
|
-
|
|
414
|
-
let err: StreamingValidationError | null = null;
|
|
415
|
-
try {
|
|
416
|
-
await readAndValidateManifest(entry);
|
|
417
|
-
} catch (e) {
|
|
418
|
-
err = e as StreamingValidationError;
|
|
419
|
-
}
|
|
420
|
-
await drainRest();
|
|
421
|
-
|
|
422
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
423
|
-
expect(err?.code).toBe("manifest_duplicate_path");
|
|
424
|
-
expect(err?.message).toContain("data/db/assistant.db");
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
// ---------------------------------------------------------------------------
|
|
429
|
-
// createHashVerifier
|
|
430
|
-
// ---------------------------------------------------------------------------
|
|
431
|
-
|
|
432
|
-
describe("createHashVerifier — identity + integrity", () => {
|
|
433
|
-
const payload = Buffer.from(
|
|
434
|
-
"the quick brown fox jumps over the lazy dog".repeat(100),
|
|
435
|
-
"utf8",
|
|
436
|
-
);
|
|
437
|
-
// Precomputed digest for the payload above.
|
|
438
|
-
const payloadSha = createHash("sha256").update(payload).digest("hex");
|
|
439
|
-
|
|
440
|
-
test("is an identity Transform for correct inputs", async () => {
|
|
441
|
-
const verifier = createHashVerifier({
|
|
442
|
-
sha256: payloadSha,
|
|
443
|
-
size: payload.length,
|
|
444
|
-
archivePath: "workspace/ok.txt",
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
// Feed the payload in two chunks to exercise multi-call _transform.
|
|
448
|
-
const half = payload.length >>> 1;
|
|
449
|
-
const source = Readable.from([
|
|
450
|
-
payload.subarray(0, half),
|
|
451
|
-
payload.subarray(half),
|
|
452
|
-
]);
|
|
453
|
-
|
|
454
|
-
const collected: Buffer[] = [];
|
|
455
|
-
verifier.on("data", (chunk: Buffer) => collected.push(chunk));
|
|
456
|
-
|
|
457
|
-
await pipeline(source, verifier);
|
|
458
|
-
|
|
459
|
-
const out = Buffer.concat(collected);
|
|
460
|
-
expect(out.length).toBe(payload.length);
|
|
461
|
-
expect(out.equals(payload)).toBe(true);
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
test("errors with code entry_hash on digest mismatch", async () => {
|
|
465
|
-
const verifier = createHashVerifier({
|
|
466
|
-
sha256:
|
|
467
|
-
// Wrong digest.
|
|
468
|
-
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
|
469
|
-
size: payload.length,
|
|
470
|
-
archivePath: "workspace/bad-hash.txt",
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
let err: StreamingValidationError | null = null;
|
|
474
|
-
try {
|
|
475
|
-
await pipeline(
|
|
476
|
-
Readable.from([payload]),
|
|
477
|
-
verifier,
|
|
478
|
-
async function* (source: AsyncIterable<Buffer>) {
|
|
479
|
-
// Drain the transform so _flush runs.
|
|
480
|
-
for await (const _chunk of source) {
|
|
481
|
-
// discard
|
|
482
|
-
}
|
|
483
|
-
},
|
|
484
|
-
);
|
|
485
|
-
} catch (e) {
|
|
486
|
-
err = e as StreamingValidationError;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
490
|
-
expect(err?.code).toBe("entry_hash");
|
|
491
|
-
expect(err?.archivePath).toBe("workspace/bad-hash.txt");
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
test("errors with code entry_size on byte-count mismatch", async () => {
|
|
495
|
-
// Right digest, wrong declared size.
|
|
496
|
-
const verifier = createHashVerifier({
|
|
497
|
-
sha256: payloadSha,
|
|
498
|
-
size: payload.length + 1,
|
|
499
|
-
archivePath: "workspace/bad-size.txt",
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
let err: StreamingValidationError | null = null;
|
|
503
|
-
try {
|
|
504
|
-
await pipeline(
|
|
505
|
-
Readable.from([payload]),
|
|
506
|
-
verifier,
|
|
507
|
-
async function* (source: AsyncIterable<Buffer>) {
|
|
508
|
-
for await (const _chunk of source) {
|
|
509
|
-
// discard
|
|
510
|
-
}
|
|
511
|
-
},
|
|
512
|
-
);
|
|
513
|
-
} catch (e) {
|
|
514
|
-
err = e as StreamingValidationError;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
518
|
-
expect(err?.code).toBe("entry_size");
|
|
519
|
-
expect(err?.archivePath).toBe("workspace/bad-size.txt");
|
|
520
|
-
});
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
// ---------------------------------------------------------------------------
|
|
524
|
-
// readAndValidateManifest — legacy fallback (backwards compatibility)
|
|
525
|
-
// ---------------------------------------------------------------------------
|
|
526
|
-
|
|
527
|
-
describe("readAndValidateManifest — legacy fallback", () => {
|
|
528
|
-
test("accepts a valid legacy six-field manifest and translates to v1 shape", async () => {
|
|
529
|
-
const dbBytes = new TextEncoder().encode("legacy-db");
|
|
530
|
-
const dbSha = createHash("sha256").update(dbBytes).digest("hex");
|
|
531
|
-
|
|
532
|
-
const legacyManifest: Record<string, unknown> = {
|
|
533
|
-
schema_version: "1.0",
|
|
534
|
-
created_at: new Date().toISOString(),
|
|
535
|
-
source: "runtime-export",
|
|
536
|
-
description: "legacy fixture",
|
|
537
|
-
files: [
|
|
538
|
-
{ path: "data/db/assistant.db", sha256: dbSha, size: dbBytes.length },
|
|
539
|
-
],
|
|
540
|
-
manifest_sha256: "",
|
|
541
|
-
};
|
|
542
|
-
legacyManifest.manifest_sha256 =
|
|
543
|
-
computeLegacyManifestSha256(legacyManifest);
|
|
544
|
-
|
|
545
|
-
const archive = buildRawVBundle([
|
|
546
|
-
{
|
|
547
|
-
name: "manifest.json",
|
|
548
|
-
data: new TextEncoder().encode(JSON.stringify(legacyManifest)),
|
|
549
|
-
},
|
|
550
|
-
{ name: "data/db/assistant.db", data: dbBytes },
|
|
551
|
-
]);
|
|
552
|
-
|
|
553
|
-
const { entry, drainRest } = await firstEntryOf(archive);
|
|
554
|
-
const result = await readAndValidateManifest(entry);
|
|
555
|
-
await drainRest();
|
|
556
|
-
|
|
557
|
-
expect(result.manifest.schema_version).toBe(1);
|
|
558
|
-
expect(result.manifest.contents).toHaveLength(1);
|
|
559
|
-
expect(result.manifest.contents[0]?.path).toBe("data/db/assistant.db");
|
|
560
|
-
expect(result.manifest.contents[0]?.size_bytes).toBe(dbBytes.length);
|
|
561
|
-
expect(result.expected.get("data/db/assistant.db")?.sha256).toBe(dbSha);
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
test("throws manifest_sha256 on a legacy manifest with a wrong checksum", async () => {
|
|
565
|
-
const dbBytes = new TextEncoder().encode("legacy-db");
|
|
566
|
-
const dbSha = createHash("sha256").update(dbBytes).digest("hex");
|
|
567
|
-
|
|
568
|
-
const badLegacy = {
|
|
569
|
-
schema_version: "1.0",
|
|
570
|
-
created_at: new Date().toISOString(),
|
|
571
|
-
files: [
|
|
572
|
-
{ path: "data/db/assistant.db", sha256: dbSha, size: dbBytes.length },
|
|
573
|
-
],
|
|
574
|
-
// Deliberately wrong checksum.
|
|
575
|
-
manifest_sha256:
|
|
576
|
-
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
const archive = buildRawVBundle([
|
|
580
|
-
{
|
|
581
|
-
name: "manifest.json",
|
|
582
|
-
data: new TextEncoder().encode(JSON.stringify(badLegacy)),
|
|
583
|
-
},
|
|
584
|
-
]);
|
|
585
|
-
const { entry, drainRest } = await firstEntryOf(archive);
|
|
586
|
-
|
|
587
|
-
let err: StreamingValidationError | null = null;
|
|
588
|
-
try {
|
|
589
|
-
await readAndValidateManifest(entry);
|
|
590
|
-
} catch (e) {
|
|
591
|
-
err = e as StreamingValidationError;
|
|
592
|
-
}
|
|
593
|
-
await drainRest();
|
|
594
|
-
|
|
595
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
596
|
-
expect(err?.code).toBe("manifest_sha256");
|
|
597
|
-
});
|
|
598
|
-
});
|