@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,515 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for streaming-side symlink support in vbundle:
|
|
3
|
-
*
|
|
4
|
-
* - `parseVBundleStream` surfaces a hand-crafted typeflag-2 entry as
|
|
5
|
-
* `{ type: "symlink", linkname }`.
|
|
6
|
-
* - `verifySymlinkEntry` accepts a well-formed symlink entry whose manifest
|
|
7
|
-
* declared a matching `link_target`.
|
|
8
|
-
* - `verifySymlinkEntry` rejects a target that escapes the archive root with
|
|
9
|
-
* code `"symlink_target_escapes_archive"`.
|
|
10
|
-
* - `verifySymlinkEntry` rejects a manifest/tar disagreement with code
|
|
11
|
-
* `"link_target_mismatch"`.
|
|
12
|
-
* - End-to-end round-trip via `buildVBundle({ files: [{ linkTarget }] })`:
|
|
13
|
-
* the in-memory builder emits a typeflag-2 record that survives
|
|
14
|
-
* `parseVBundleStream` and is accepted by `verifySymlinkEntry`.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
18
|
-
import { Readable } from "node:stream";
|
|
19
|
-
import { gzipSync } from "node:zlib";
|
|
20
|
-
import { describe, expect, test } from "bun:test";
|
|
21
|
-
|
|
22
|
-
import { buildVBundle } from "../vbundle-builder.js";
|
|
23
|
-
import {
|
|
24
|
-
readAndValidateManifest,
|
|
25
|
-
StreamingValidationError,
|
|
26
|
-
verifySymlinkEntry,
|
|
27
|
-
} from "../vbundle-streaming-validator.js";
|
|
28
|
-
import {
|
|
29
|
-
parseVBundleStream,
|
|
30
|
-
type StreamedTarEntry,
|
|
31
|
-
} from "../vbundle-tar-stream.js";
|
|
32
|
-
import { defaultV1Options } from "./v1-test-helpers.js";
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Hand-crafted tar fixture helpers
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
const BLOCK_SIZE = 512;
|
|
39
|
-
|
|
40
|
-
function writeOctal(
|
|
41
|
-
buf: Uint8Array,
|
|
42
|
-
offset: number,
|
|
43
|
-
length: number,
|
|
44
|
-
value: number,
|
|
45
|
-
): void {
|
|
46
|
-
const str = value.toString(8).padStart(length - 1, "0");
|
|
47
|
-
for (let i = 0; i < str.length; i++) {
|
|
48
|
-
buf[offset + i] = str.charCodeAt(i);
|
|
49
|
-
}
|
|
50
|
-
buf[offset + length - 1] = 0;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Build a single 512-byte tar header block with typeflag-2 (symlink). The
|
|
55
|
-
* link target is written into the linkname field (header[157..256]). Body
|
|
56
|
-
* size is always 0 for symlink entries so no content blocks follow.
|
|
57
|
-
*/
|
|
58
|
-
function buildSymlinkHeaderBlock(name: string, linkTarget: string): Uint8Array {
|
|
59
|
-
const encoder = new TextEncoder();
|
|
60
|
-
const nameBytes = encoder.encode(name);
|
|
61
|
-
const linkBytes = encoder.encode(linkTarget);
|
|
62
|
-
if (nameBytes.length > 100) {
|
|
63
|
-
throw new Error(`fixture: name too long (${nameBytes.length} bytes)`);
|
|
64
|
-
}
|
|
65
|
-
if (linkBytes.length > 100) {
|
|
66
|
-
throw new Error(`fixture: linkTarget too long (${linkBytes.length} bytes)`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const header = new Uint8Array(BLOCK_SIZE);
|
|
70
|
-
header.set(nameBytes, 0);
|
|
71
|
-
writeOctal(header, 100, 8, 0o644);
|
|
72
|
-
writeOctal(header, 108, 8, 0);
|
|
73
|
-
writeOctal(header, 116, 8, 0);
|
|
74
|
-
writeOctal(header, 124, 12, 0); // size=0
|
|
75
|
-
writeOctal(header, 136, 12, Math.floor(Date.now() / 1000));
|
|
76
|
-
header[156] = "2".charCodeAt(0); // typeflag = symlink
|
|
77
|
-
header.set(linkBytes, 157); // linkname
|
|
78
|
-
|
|
79
|
-
const magic = encoder.encode("ustar\0");
|
|
80
|
-
header.set(magic, 257);
|
|
81
|
-
header[263] = "0".charCodeAt(0);
|
|
82
|
-
header[264] = "0".charCodeAt(0);
|
|
83
|
-
|
|
84
|
-
// Checksum: sum of all bytes with the checksum field treated as 8 ASCII spaces.
|
|
85
|
-
let sum = 0;
|
|
86
|
-
for (let i = 0; i < BLOCK_SIZE; i++) {
|
|
87
|
-
sum += i >= 148 && i < 156 ? 0x20 : header[i];
|
|
88
|
-
}
|
|
89
|
-
writeOctal(header, 148, 7, sum);
|
|
90
|
-
header[155] = 0x20;
|
|
91
|
-
|
|
92
|
-
return header;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Concatenate a list of byte chunks into a single Uint8Array. */
|
|
96
|
-
function concatBytes(parts: Uint8Array[]): Uint8Array {
|
|
97
|
-
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
98
|
-
const out = new Uint8Array(total);
|
|
99
|
-
let offset = 0;
|
|
100
|
-
for (const p of parts) {
|
|
101
|
-
out.set(p, offset);
|
|
102
|
-
offset += p.length;
|
|
103
|
-
}
|
|
104
|
-
return out;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Build a minimal gzipped tar archive containing a single typeflag-2 entry
|
|
109
|
-
* followed by the standard two-block end-of-archive marker. Used for
|
|
110
|
-
* exercising the parser without depending on `buildVBundle` symlink emit
|
|
111
|
-
* (which lands in PR 2).
|
|
112
|
-
*/
|
|
113
|
-
function buildGzippedSymlinkOnlyTar(
|
|
114
|
-
name: string,
|
|
115
|
-
linkTarget: string,
|
|
116
|
-
): Uint8Array {
|
|
117
|
-
const tar = concatBytes([
|
|
118
|
-
buildSymlinkHeaderBlock(name, linkTarget),
|
|
119
|
-
new Uint8Array(BLOCK_SIZE * 2), // EOA marker
|
|
120
|
-
]);
|
|
121
|
-
return gzipSync(tar);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function readableFromBuffer(buf: Uint8Array): Readable {
|
|
125
|
-
return Readable.from([Buffer.from(buf)]);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function sha256Hex(input: string): string {
|
|
129
|
-
return createHash("sha256").update(input).digest("hex");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Build a synthetic `StreamedTarEntry` for use with `verifySymlinkEntry`.
|
|
134
|
-
* The body is empty (`Readable.from([])`) since symlink entries declare
|
|
135
|
-
* size=0 and never carry payload bytes.
|
|
136
|
-
*/
|
|
137
|
-
function makeSymlinkEntry(input: {
|
|
138
|
-
name: string;
|
|
139
|
-
linkname: string;
|
|
140
|
-
size?: number;
|
|
141
|
-
}): StreamedTarEntry {
|
|
142
|
-
return {
|
|
143
|
-
header: {
|
|
144
|
-
name: input.name,
|
|
145
|
-
size: input.size ?? 0,
|
|
146
|
-
type: "symlink",
|
|
147
|
-
linkname: input.linkname,
|
|
148
|
-
},
|
|
149
|
-
body: Readable.from([]),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
|
-
// parseVBundleStream — surfaces typeflag-2 entries
|
|
155
|
-
// ---------------------------------------------------------------------------
|
|
156
|
-
|
|
157
|
-
describe("parseVBundleStream — symlink entries", () => {
|
|
158
|
-
test("surfaces a typeflag-2 entry as { type: 'symlink', linkname }", async () => {
|
|
159
|
-
const archive = buildGzippedSymlinkOnlyTar("workspace/foo.md", "bar.md");
|
|
160
|
-
|
|
161
|
-
const seen: { name: string; type: string; linkname?: string }[] = [];
|
|
162
|
-
for await (const entry of parseVBundleStream(readableFromBuffer(archive))) {
|
|
163
|
-
seen.push({
|
|
164
|
-
name: entry.header.name,
|
|
165
|
-
type: entry.header.type,
|
|
166
|
-
linkname: entry.header.linkname,
|
|
167
|
-
});
|
|
168
|
-
// Drain the (zero-byte) body so the extractor advances.
|
|
169
|
-
entry.body.resume();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
expect(seen.length).toBe(1);
|
|
173
|
-
expect(seen[0]?.name).toBe("workspace/foo.md");
|
|
174
|
-
expect(seen[0]?.type).toBe("symlink");
|
|
175
|
-
expect(seen[0]?.linkname).toBe("bar.md");
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
// verifySymlinkEntry — happy path
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
|
|
183
|
-
describe("verifySymlinkEntry — accepts a valid symlink", () => {
|
|
184
|
-
test("does not throw when manifest and tar agree and target stays in archive", () => {
|
|
185
|
-
const linkTarget = "bar.md";
|
|
186
|
-
const entry = makeSymlinkEntry({
|
|
187
|
-
name: "workspace/foo.md",
|
|
188
|
-
linkname: linkTarget,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
expect(() =>
|
|
192
|
-
verifySymlinkEntry({
|
|
193
|
-
entry,
|
|
194
|
-
expectedEntry: {
|
|
195
|
-
sha256: sha256Hex(linkTarget),
|
|
196
|
-
size: 0,
|
|
197
|
-
linkTarget,
|
|
198
|
-
},
|
|
199
|
-
}),
|
|
200
|
-
).not.toThrow();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("accepts a target that resolves to a sibling at the archive root", () => {
|
|
204
|
-
const linkTarget = "sibling.md";
|
|
205
|
-
const entry = makeSymlinkEntry({
|
|
206
|
-
name: "workspace/notes/foo.md",
|
|
207
|
-
linkname: linkTarget,
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
expect(() =>
|
|
211
|
-
verifySymlinkEntry({
|
|
212
|
-
entry,
|
|
213
|
-
expectedEntry: {
|
|
214
|
-
sha256: sha256Hex(linkTarget),
|
|
215
|
-
size: 0,
|
|
216
|
-
linkTarget,
|
|
217
|
-
},
|
|
218
|
-
}),
|
|
219
|
-
).not.toThrow();
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// ---------------------------------------------------------------------------
|
|
224
|
-
// verifySymlinkEntry — path traversal rejection
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
|
|
227
|
-
describe("verifySymlinkEntry — path traversal", () => {
|
|
228
|
-
test("rejects a target that escapes the archive root with code symlink_target_escapes_archive", () => {
|
|
229
|
-
const escapingTarget = "../../../etc/passwd";
|
|
230
|
-
const entry = makeSymlinkEntry({
|
|
231
|
-
name: "workspace/foo.md",
|
|
232
|
-
linkname: escapingTarget,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
let err: StreamingValidationError | null = null;
|
|
236
|
-
try {
|
|
237
|
-
verifySymlinkEntry({
|
|
238
|
-
entry,
|
|
239
|
-
expectedEntry: {
|
|
240
|
-
sha256: sha256Hex(escapingTarget),
|
|
241
|
-
size: 0,
|
|
242
|
-
linkTarget: escapingTarget,
|
|
243
|
-
},
|
|
244
|
-
});
|
|
245
|
-
} catch (e) {
|
|
246
|
-
err = e as StreamingValidationError;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
250
|
-
expect(err?.code).toBe("symlink_target_escapes_archive");
|
|
251
|
-
expect(err?.archivePath).toBe("workspace/foo.md");
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("verifySymlinkEntry rejects absolute symlink targets", () => {
|
|
255
|
-
// Regression: `posix.join("workspace", "/etc/passwd")` normalizes the
|
|
256
|
-
// leading slash away and returns `"workspace/etc/passwd"`, so without
|
|
257
|
-
// an explicit absolute-target guard an attacker could ship a manifest
|
|
258
|
-
// with `link_target: "/etc/passwd"` that passes the `..` traversal
|
|
259
|
-
// check and is later realized by `fs.symlink("/etc/passwd", ...)`.
|
|
260
|
-
const absoluteTarget = "/etc/passwd";
|
|
261
|
-
const entry = makeSymlinkEntry({
|
|
262
|
-
name: "workspace/skills/foo.md",
|
|
263
|
-
linkname: absoluteTarget,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
let err: StreamingValidationError | null = null;
|
|
267
|
-
try {
|
|
268
|
-
verifySymlinkEntry({
|
|
269
|
-
entry,
|
|
270
|
-
expectedEntry: {
|
|
271
|
-
sha256: sha256Hex(absoluteTarget),
|
|
272
|
-
size: 0,
|
|
273
|
-
linkTarget: absoluteTarget,
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
} catch (e) {
|
|
277
|
-
err = e as StreamingValidationError;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
281
|
-
expect(err?.code).toBe("symlink_target_escapes_archive");
|
|
282
|
-
expect(err?.archivePath).toBe("workspace/skills/foo.md");
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test("rejects a target that escapes the archive root from a top-level symlink", () => {
|
|
286
|
-
// Symlink at "foo.md" (no parent dir inside the archive); a `..` target
|
|
287
|
-
// resolves above the archive root.
|
|
288
|
-
const escapingTarget = "..";
|
|
289
|
-
const entry = makeSymlinkEntry({
|
|
290
|
-
name: "foo.md",
|
|
291
|
-
linkname: escapingTarget,
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
let err: StreamingValidationError | null = null;
|
|
295
|
-
try {
|
|
296
|
-
verifySymlinkEntry({
|
|
297
|
-
entry,
|
|
298
|
-
expectedEntry: {
|
|
299
|
-
sha256: sha256Hex(escapingTarget),
|
|
300
|
-
size: 0,
|
|
301
|
-
linkTarget: escapingTarget,
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
|
-
} catch (e) {
|
|
305
|
-
err = e as StreamingValidationError;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
309
|
-
expect(err?.code).toBe("symlink_target_escapes_archive");
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// ---------------------------------------------------------------------------
|
|
314
|
-
// verifySymlinkEntry — manifest/tar disagreement
|
|
315
|
-
// ---------------------------------------------------------------------------
|
|
316
|
-
|
|
317
|
-
describe("verifySymlinkEntry — disagreement", () => {
|
|
318
|
-
test("throws link_target_mismatch when tar linkname differs from manifest link_target", () => {
|
|
319
|
-
const entry = makeSymlinkEntry({
|
|
320
|
-
name: "workspace/foo.md",
|
|
321
|
-
linkname: "bar.md",
|
|
322
|
-
});
|
|
323
|
-
const manifestTarget = "qux.md";
|
|
324
|
-
|
|
325
|
-
let err: StreamingValidationError | null = null;
|
|
326
|
-
try {
|
|
327
|
-
verifySymlinkEntry({
|
|
328
|
-
entry,
|
|
329
|
-
expectedEntry: {
|
|
330
|
-
sha256: sha256Hex(manifestTarget),
|
|
331
|
-
size: 0,
|
|
332
|
-
linkTarget: manifestTarget,
|
|
333
|
-
},
|
|
334
|
-
});
|
|
335
|
-
} catch (e) {
|
|
336
|
-
err = e as StreamingValidationError;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
340
|
-
expect(err?.code).toBe("link_target_mismatch");
|
|
341
|
-
expect(err?.archivePath).toBe("workspace/foo.md");
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
test("throws symlink_not_declared when manifest entry has no link_target", () => {
|
|
345
|
-
const entry = makeSymlinkEntry({
|
|
346
|
-
name: "workspace/foo.md",
|
|
347
|
-
linkname: "bar.md",
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
let err: StreamingValidationError | null = null;
|
|
351
|
-
try {
|
|
352
|
-
verifySymlinkEntry({
|
|
353
|
-
entry,
|
|
354
|
-
expectedEntry: {
|
|
355
|
-
sha256: sha256Hex("bar.md"),
|
|
356
|
-
size: 0,
|
|
357
|
-
linkTarget: null,
|
|
358
|
-
},
|
|
359
|
-
});
|
|
360
|
-
} catch (e) {
|
|
361
|
-
err = e as StreamingValidationError;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
365
|
-
expect(err?.code).toBe("symlink_not_declared");
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
test("throws entry_size when symlink header declares non-zero size", () => {
|
|
369
|
-
const entry = makeSymlinkEntry({
|
|
370
|
-
name: "workspace/foo.md",
|
|
371
|
-
linkname: "bar.md",
|
|
372
|
-
size: 5,
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
let err: StreamingValidationError | null = null;
|
|
376
|
-
try {
|
|
377
|
-
verifySymlinkEntry({
|
|
378
|
-
entry,
|
|
379
|
-
expectedEntry: {
|
|
380
|
-
sha256: sha256Hex("bar.md"),
|
|
381
|
-
size: 0,
|
|
382
|
-
linkTarget: "bar.md",
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
} catch (e) {
|
|
386
|
-
err = e as StreamingValidationError;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
390
|
-
expect(err?.code).toBe("entry_size");
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
test("throws entry_size when manifest declares non-zero size_bytes for a symlink", () => {
|
|
394
|
-
// The buffered validator catches manifest size_bytes != 0 for symlink
|
|
395
|
-
// entries via FILE_SIZE_MISMATCH. Streaming side must reject the same
|
|
396
|
-
// crafted shape — a manifest with `link_target` plus `size_bytes: 100`
|
|
397
|
-
// would otherwise slip past `verifySymlinkEntry` (which only checked
|
|
398
|
-
// tar header.size).
|
|
399
|
-
const linkTarget = "bar.md";
|
|
400
|
-
const entry = makeSymlinkEntry({
|
|
401
|
-
name: "workspace/foo.md",
|
|
402
|
-
linkname: linkTarget,
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
let err: StreamingValidationError | null = null;
|
|
406
|
-
try {
|
|
407
|
-
verifySymlinkEntry({
|
|
408
|
-
entry,
|
|
409
|
-
expectedEntry: {
|
|
410
|
-
sha256: sha256Hex(linkTarget),
|
|
411
|
-
size: 100,
|
|
412
|
-
linkTarget,
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
} catch (e) {
|
|
416
|
-
err = e as StreamingValidationError;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
420
|
-
expect(err?.code).toBe("entry_size");
|
|
421
|
-
expect(err?.archivePath).toBe("workspace/foo.md");
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
test("throws entry_hash when manifest sha256 doesn't match link target digest", () => {
|
|
425
|
-
const linkTarget = "bar.md";
|
|
426
|
-
const entry = makeSymlinkEntry({
|
|
427
|
-
name: "workspace/foo.md",
|
|
428
|
-
linkname: linkTarget,
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
let err: StreamingValidationError | null = null;
|
|
432
|
-
try {
|
|
433
|
-
verifySymlinkEntry({
|
|
434
|
-
entry,
|
|
435
|
-
expectedEntry: {
|
|
436
|
-
// Wrong digest — manifest declared a different content than what's
|
|
437
|
-
// hashable from the link target.
|
|
438
|
-
sha256:
|
|
439
|
-
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
440
|
-
size: 0,
|
|
441
|
-
linkTarget,
|
|
442
|
-
},
|
|
443
|
-
});
|
|
444
|
-
} catch (e) {
|
|
445
|
-
err = e as StreamingValidationError;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
449
|
-
expect(err?.code).toBe("entry_hash");
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
// ---------------------------------------------------------------------------
|
|
454
|
-
// Round-trip via buildVBundle
|
|
455
|
-
// ---------------------------------------------------------------------------
|
|
456
|
-
|
|
457
|
-
describe("round-trip: buildVBundle -> parseVBundleStream -> verifySymlinkEntry", () => {
|
|
458
|
-
test("a symlink entry survives the full streaming pipeline", async () => {
|
|
459
|
-
const linkTarget = "bar.md";
|
|
460
|
-
const regularFileBytes = new TextEncoder().encode("regular file payload\n");
|
|
461
|
-
|
|
462
|
-
const { archive } = buildVBundle({
|
|
463
|
-
files: [
|
|
464
|
-
// The streaming importer requires data/db/assistant.db to be present;
|
|
465
|
-
// include a synthetic empty one so this fixture mirrors what
|
|
466
|
-
// production exports always carry.
|
|
467
|
-
{ path: "data/db/assistant.db", data: new Uint8Array() },
|
|
468
|
-
// A regular file entry so we exercise both shapes through the parser.
|
|
469
|
-
{ path: "workspace/regular.md", data: regularFileBytes },
|
|
470
|
-
// The symlink under test.
|
|
471
|
-
{ path: "workspace/foo.md", data: new Uint8Array(), linkTarget },
|
|
472
|
-
],
|
|
473
|
-
...defaultV1Options(),
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
const iter = parseVBundleStream(readableFromBuffer(archive));
|
|
477
|
-
|
|
478
|
-
// The manifest is the FIRST tar entry and must be drained before
|
|
479
|
-
// subsequent entries flow through.
|
|
480
|
-
const manifestEntry = await iter.next();
|
|
481
|
-
if (manifestEntry.done || !manifestEntry.value) {
|
|
482
|
-
throw new Error("archive contained no entries");
|
|
483
|
-
}
|
|
484
|
-
const { expected } = await readAndValidateManifest(manifestEntry.value);
|
|
485
|
-
|
|
486
|
-
// Drain the rest of the entries and capture the symlink entry.
|
|
487
|
-
let symlinkEntry: StreamedTarEntry | null = null;
|
|
488
|
-
for await (const entry of iter) {
|
|
489
|
-
if (entry.header.name === "workspace/foo.md") {
|
|
490
|
-
symlinkEntry = entry;
|
|
491
|
-
// Confirm the parser surfaced the typeflag-2 record correctly before
|
|
492
|
-
// handing it to verifySymlinkEntry (which would also resume() the
|
|
493
|
-
// body on its own paths).
|
|
494
|
-
expect(entry.header.type).toBe("symlink");
|
|
495
|
-
expect(entry.header.linkname).toBe(linkTarget);
|
|
496
|
-
|
|
497
|
-
const expectedEntry = expected.get("workspace/foo.md");
|
|
498
|
-
if (!expectedEntry) {
|
|
499
|
-
throw new Error("manifest expected map missing the symlink entry");
|
|
500
|
-
}
|
|
501
|
-
expect(expectedEntry.linkTarget).toBe(linkTarget);
|
|
502
|
-
|
|
503
|
-
// Asserts streaming-validator acceptance of a builder-produced entry.
|
|
504
|
-
expect(() =>
|
|
505
|
-
verifySymlinkEntry({ entry, expectedEntry }),
|
|
506
|
-
).not.toThrow();
|
|
507
|
-
} else {
|
|
508
|
-
// Drain non-symlink bodies so the extractor advances.
|
|
509
|
-
entry.body.resume();
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
expect(symlinkEntry).not.toBeNull();
|
|
514
|
-
});
|
|
515
|
-
});
|