@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.
Files changed (176) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  3. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/openapi.yaml +2 -708
  9. package/package.json +2 -3
  10. package/scripts/sync-web-search-catalog.ts +1 -2
  11. package/src/__tests__/cloud-profile-guard.test.ts +2 -2
  12. package/src/__tests__/config-schema.test.ts +6 -38
  13. package/src/__tests__/conversation-queue.test.ts +45 -6
  14. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  15. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  16. package/src/__tests__/persona-resolver.test.ts +0 -51
  17. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  18. package/src/__tests__/run-due-schedules.test.ts +0 -4
  19. package/src/__tests__/schedule-store.test.ts +0 -12
  20. package/src/__tests__/stt-stream-session.test.ts +4 -4
  21. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  22. package/src/__tests__/voice-config-update.test.ts +12 -167
  23. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  24. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  25. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  26. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  27. package/src/calls/resolve-call-tts-provider.ts +6 -25
  28. package/src/calls/telephony-credential-preflight.ts +0 -10
  29. package/src/calls/telephony-tts-capability.ts +1 -17
  30. package/src/cli/commands/tts.ts +1 -2
  31. package/src/cli/index.help.ts +0 -2
  32. package/src/cli/program.ts +0 -2
  33. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  34. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  35. package/src/config/schemas/services.ts +5 -6
  36. package/src/config/schemas/stt.ts +1 -3
  37. package/src/config/schemas/tts.ts +1 -30
  38. package/src/daemon/daemon-readiness.ts +4 -29
  39. package/src/daemon/lifecycle.ts +0 -31
  40. package/src/daemon/providers-setup.ts +0 -8
  41. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  42. package/src/heartbeat/heartbeat-service.ts +1 -66
  43. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  44. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  45. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  46. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  47. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  48. package/src/live-voice/live-voice-session.ts +10 -6
  49. package/src/live-voice/live-voice-tts.ts +1 -8
  50. package/src/plugins/defaults/memory/worker.ts +3 -3
  51. package/src/prompts/persona-resolver.ts +0 -35
  52. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  53. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  54. package/src/providers/search-provider-catalog.ts +7 -39
  55. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  56. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  57. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  58. package/src/providers/speech-to-text/resolve.ts +3 -120
  59. package/src/runtime/http-router-types.ts +1 -3
  60. package/src/runtime/http-server.ts +1 -1
  61. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  62. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  63. package/src/runtime/routes/index.ts +0 -4
  64. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  65. package/src/runtime/routes/live-voice-routes.ts +8 -11
  66. package/src/runtime/routes/secret-routes.ts +0 -6
  67. package/src/runtime/routes/tts-routes.ts +0 -38
  68. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  69. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  70. package/src/schedule/schedule-store.ts +0 -7
  71. package/src/schedule/scheduler.ts +0 -5
  72. package/src/schedule/worker.ts +1 -1
  73. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  74. package/src/stt/daemon-batch-transcriber.ts +4 -38
  75. package/src/stt/types.ts +1 -9
  76. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  77. package/src/tools/network/web-search.ts +4 -165
  78. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  79. package/src/tts/provider-catalog.ts +0 -4
  80. package/src/tts/types.ts +0 -1
  81. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  82. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  83. package/src/__tests__/migration-export-http.test.ts +0 -557
  84. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  85. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  86. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  87. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  88. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  89. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  90. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  91. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  92. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  93. package/src/__tests__/migration-transport.test.ts +0 -1089
  94. package/src/__tests__/migration-validate-http.test.ts +0 -634
  95. package/src/__tests__/migration-wizard.test.ts +0 -1395
  96. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  97. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  98. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  99. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  100. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  101. package/src/background-wake/background-wake-routes.test.ts +0 -906
  102. package/src/background-wake/cloud-client.test.ts +0 -328
  103. package/src/background-wake/cloud-client.ts +0 -196
  104. package/src/background-wake/next-wake.test.ts +0 -335
  105. package/src/background-wake/next-wake.ts +0 -188
  106. package/src/background-wake/publisher.ts +0 -93
  107. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  108. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  109. package/src/cli/commands/backup.help.ts +0 -180
  110. package/src/cli/commands/backup.ts +0 -398
  111. package/src/cloud/cloud-speech.test.ts +0 -335
  112. package/src/cloud/cloud-speech.ts +0 -300
  113. package/src/config/cloud-speech-defaults.ts +0 -300
  114. package/src/config/sanitize-for-transfer.ts +0 -111
  115. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  116. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  117. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  118. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  119. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  120. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  121. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  122. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  123. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  124. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  125. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  126. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  127. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  128. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  129. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  130. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  131. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  132. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  133. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  134. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  135. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  136. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  137. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  138. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  139. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  140. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  141. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  142. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  143. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  144. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  146. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  147. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  148. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  149. package/src/runtime/migrations/job-registry.ts +0 -297
  150. package/src/runtime/migrations/migration-transport.ts +0 -612
  151. package/src/runtime/migrations/migration-wizard.ts +0 -747
  152. package/src/runtime/migrations/origin-mode.ts +0 -42
  153. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  154. package/src/runtime/migrations/staged-import-path.ts +0 -116
  155. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  156. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  157. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  158. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  159. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  160. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  161. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  162. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  163. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  164. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  165. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  166. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  167. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  168. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  169. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  170. package/src/runtime/routes/background-wake-routes.ts +0 -387
  171. package/src/runtime/routes/migration-routes.ts +0 -2584
  172. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  173. package/src/tools/network/cloud-search-proxy.ts +0 -211
  174. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  175. package/src/tts/providers/cadet-provider.ts +0 -244
  176. package/src/types/tar-stream.d.ts +0 -66
@@ -1,405 +0,0 @@
1
- /**
2
- * Analyzes a validated .vbundle archive to produce a dry-run import report.
3
- *
4
- * Given a valid .vbundle archive (already validated), this module inspects
5
- * its manifest and contents to determine what would happen if the bundle
6
- * were imported. It compares the bundle's files against the current
7
- * assistant state on disk and reports:
8
- * - Which files would be written or overwritten
9
- * - Size changes for each file
10
- * - Whether existing data would be replaced
11
- * - Any potential conflicts
12
- *
13
- * This is a read-only analysis — no files are written or modified.
14
- */
15
-
16
- import { createHash } from "node:crypto";
17
- import { existsSync, readFileSync, statSync } from "node:fs";
18
- import { join, resolve } from "node:path";
19
-
20
- import { resolveGuardianPersonaPath } from "../../prompts/persona-resolver.js";
21
- import { getLogger } from "../../util/logger.js";
22
- import {
23
- isGatewayArchivePath,
24
- isRetiredArchivePath,
25
- } from "./vbundle-import-policy.js";
26
- import type { ManifestType } from "./vbundle-validator.js";
27
-
28
- const log = getLogger("vbundle-import-analyzer");
29
-
30
- /**
31
- * Only these prompt filenames are accepted during import.
32
- *
33
- * `USER.md` is retained for backward compatibility with legacy bundles —
34
- * on import, its content is translated to `users/<slug>.md` at the
35
- * current guardian's location (see `DefaultPathResolver.resolve`).
36
- */
37
- const ALLOWED_PROMPT_FILENAMES = new Set(["IDENTITY.md", "SOUL.md", "USER.md"]);
38
-
39
- /** Archive path for the legacy guardian user persona file. */
40
- const LEGACY_USER_MD_ARCHIVE_PATH = "prompts/USER.md";
41
-
42
- // ---------------------------------------------------------------------------
43
- // Public types
44
- // ---------------------------------------------------------------------------
45
-
46
- type ImportAction = "create" | "overwrite" | "unchanged" | "skip";
47
-
48
- interface ImportFileReport {
49
- /** Archive path (e.g. "data/db/assistant.db") */
50
- path: string;
51
- /** What would happen to this file on import */
52
- action: ImportAction;
53
- /** Size of the file in the bundle (bytes) */
54
- bundle_size: number;
55
- /** Size of the existing file on disk, or null if it does not exist */
56
- current_size: number | null;
57
- /** SHA-256 of the file in the bundle */
58
- bundle_sha256: string;
59
- /** SHA-256 of the existing file on disk, or null if it does not exist */
60
- current_sha256: string | null;
61
- }
62
-
63
- interface ImportConflict {
64
- code: string;
65
- message: string;
66
- path?: string;
67
- }
68
-
69
- export interface ImportDryRunReport {
70
- /** Whether the import can proceed (bundle is valid and no blocking conflicts) */
71
- can_import: boolean;
72
- /** Summary of what would happen */
73
- summary: {
74
- total_files: number;
75
- files_to_create: number;
76
- files_to_overwrite: number;
77
- files_unchanged: number;
78
- files_to_skip: number;
79
- };
80
- /** Per-file analysis of what would change */
81
- files: ImportFileReport[];
82
- /** Any conflicts or warnings that might block or complicate import */
83
- conflicts: ImportConflict[];
84
- /** The manifest from the bundle */
85
- manifest: ManifestType;
86
- }
87
-
88
- // ---------------------------------------------------------------------------
89
- // Path mapping
90
- // ---------------------------------------------------------------------------
91
-
92
- /**
93
- * Maps archive paths to their corresponding locations on disk.
94
- * This is the canonical mapping used during actual import (PR-5) —
95
- * dry-run uses the same mapping for consistency.
96
- */
97
- export interface PathResolver {
98
- resolve(archivePath: string): string | null;
99
- }
100
-
101
- export class DefaultPathResolver implements PathResolver {
102
- /**
103
- * @param workspaceDir absolute path to the workspace directory.
104
- * @param hooksDir absolute path to the hooks directory.
105
- * @param guardianPersonaPathResolver function that returns the
106
- * current guardian's persona path (`users/<slug>.md`) or `null`
107
- * when no guardian exists. Defaults to the production
108
- * `resolveGuardianPersonaPath` helper; tests inject a stub so they
109
- * don't need to mock the contact store.
110
- */
111
- constructor(
112
- private workspaceDir?: string,
113
- private hooksDir?: string,
114
- private guardianPersonaPathResolver: () =>
115
- | string
116
- | null = resolveGuardianPersonaPath,
117
- ) {}
118
-
119
- resolve(archivePath: string): string | null {
120
- // Skip credential entries — handled separately by the credential import step
121
- if (archivePath.startsWith("credentials/")) {
122
- return null;
123
- }
124
-
125
- // Retired-feature files (both legacy and workspace-format spellings)
126
- // must never resolve to a disk target — resolving would write them
127
- // back into the workspace on import. The analyzer and both importers
128
- // turn the resulting null into a silent non-blocking skip.
129
- if (
130
- isRetiredArchivePath(archivePath) ||
131
- isGatewayArchivePath(archivePath)
132
- ) {
133
- return null;
134
- }
135
-
136
- // New format: workspace/ prefix — maps directly into the workspace dir
137
- if (archivePath.startsWith("workspace/") && this.workspaceDir) {
138
- const relPath = archivePath.slice("workspace/".length);
139
- if (!relPath) {
140
- return null;
141
- }
142
- const resolved = resolve(this.workspaceDir, relPath);
143
- const wsRoot = resolve(this.workspaceDir);
144
- // Path traversal containment
145
- if (resolved !== wsRoot && !resolved.startsWith(wsRoot + "/")) {
146
- return null;
147
- }
148
- return resolved;
149
- }
150
-
151
- // Backward compat: old bundle formats with specific archive paths
152
- if (archivePath === "data/db/assistant.db" && this.workspaceDir) {
153
- return join(this.workspaceDir, "data", "db", "assistant.db");
154
- }
155
- if (archivePath === "config/settings.json" && this.workspaceDir) {
156
- return join(this.workspaceDir, "config.json");
157
- }
158
- if (archivePath.startsWith("skills/") && this.workspaceDir) {
159
- const resolved = resolve(
160
- this.workspaceDir,
161
- "skills",
162
- archivePath.slice("skills/".length),
163
- );
164
- const skillsRoot = resolve(this.workspaceDir, "skills");
165
- if (resolved !== skillsRoot && !resolved.startsWith(skillsRoot + "/")) {
166
- return null;
167
- }
168
- return resolved;
169
- }
170
- if (archivePath.startsWith("prompts/") && this.workspaceDir) {
171
- // Old bundles stored prompts as prompts/IDENTITY.md etc — these map
172
- // to the workspace root (e.g. workspace/IDENTITY.md).
173
- // Only accepted prompt filenames resolve — unknown entries are
174
- // skipped so they cannot trigger workspace clearing.
175
- const filename = archivePath.slice("prompts/".length);
176
- if (!ALLOWED_PROMPT_FILENAMES.has(filename)) {
177
- return null;
178
- }
179
-
180
- // Legacy USER.md translation: rewrite the destination to the
181
- // current guardian's per-user persona file `users/<slug>.md`.
182
- // Guardian-less workspaces return null → importer skips with a
183
- // warning (see vbundle-importer.ts). This lookup runs against
184
- // whatever DB state is live at the time of resolve — typically
185
- // the pre-import workspace, which is the common upgrade case.
186
- if (archivePath === LEGACY_USER_MD_ARCHIVE_PATH) {
187
- const guardianPath = this.guardianPersonaPathResolver();
188
- if (!guardianPath) {
189
- log.warn(
190
- { path: archivePath },
191
- "Legacy prompts/USER.md has no guardian target — will be skipped on import",
192
- );
193
- return null;
194
- }
195
- // Containment check: guardian path must live under the workspace.
196
- const wsRoot = resolve(this.workspaceDir);
197
- const guardianResolved = resolve(guardianPath);
198
- if (
199
- guardianResolved !== wsRoot &&
200
- !guardianResolved.startsWith(wsRoot + "/")
201
- ) {
202
- log.warn(
203
- { path: archivePath, guardianPath },
204
- "Guardian persona path falls outside workspace — refusing to write",
205
- );
206
- return null;
207
- }
208
- return guardianResolved;
209
- }
210
-
211
- const resolved = resolve(this.workspaceDir, filename);
212
- const wsRoot = resolve(this.workspaceDir);
213
- if (resolved !== wsRoot && !resolved.startsWith(wsRoot + "/")) {
214
- return null;
215
- }
216
- return resolved;
217
- }
218
- if (archivePath.startsWith("hooks/") && this.hooksDir) {
219
- const resolved = resolve(
220
- this.hooksDir,
221
- archivePath.slice("hooks/".length),
222
- );
223
- const hooksRoot = resolve(this.hooksDir);
224
- if (resolved !== hooksRoot && !resolved.startsWith(hooksRoot + "/")) {
225
- return null;
226
- }
227
- return resolved;
228
- }
229
-
230
- return null;
231
- }
232
- }
233
-
234
- // ---------------------------------------------------------------------------
235
- // Hash helper
236
- // ---------------------------------------------------------------------------
237
-
238
- function sha256Hex(data: Uint8Array): string {
239
- return createHash("sha256").update(data).digest("hex");
240
- }
241
-
242
- // ---------------------------------------------------------------------------
243
- // Core analyzer
244
- // ---------------------------------------------------------------------------
245
-
246
- interface AnalyzeImportOptions {
247
- /** The parsed and validated manifest from the bundle */
248
- manifest: ManifestType;
249
- /** Resolves archive paths to disk paths for comparison */
250
- pathResolver: PathResolver;
251
- }
252
-
253
- /**
254
- * Analyze what importing a .vbundle archive would do without modifying
255
- * any state. Compares bundle contents against current files on disk.
256
- */
257
- export function analyzeImport(
258
- options: AnalyzeImportOptions,
259
- ): ImportDryRunReport {
260
- const { manifest, pathResolver } = options;
261
- const files: ImportFileReport[] = [];
262
- const conflicts: ImportConflict[] = [];
263
-
264
- for (const fileEntry of manifest.contents) {
265
- const diskPath = pathResolver.resolve(fileEntry.path);
266
-
267
- // Credential entries are handled separately by the credential import
268
- // step — skip them without flagging as unknown/conflict.
269
- if (fileEntry.path.startsWith("credentials/")) {
270
- files.push({
271
- path: fileEntry.path,
272
- action: "skip",
273
- bundle_size: fileEntry.size_bytes,
274
- bundle_sha256: fileEntry.sha256,
275
- current_size: null,
276
- current_sha256: null,
277
- });
278
- continue;
279
- }
280
-
281
- if (!diskPath) {
282
- // Legacy `prompts/USER.md` in a guardian-less workspace has no
283
- // destination to translate to. The commit-time path skips this
284
- // entry with a warning rather than failing, so preflight mirrors
285
- // that: emit a non-blocking skip so `can_import` stays true and
286
- // upgrade paths proceed. No conflict is registered.
287
- if (fileEntry.path === LEGACY_USER_MD_ARCHIVE_PATH) {
288
- log.warn(
289
- { path: fileEntry.path },
290
- "Legacy prompts/USER.md has no guardian target — will be skipped on import",
291
- );
292
- files.push({
293
- path: fileEntry.path,
294
- action: "skip",
295
- bundle_size: fileEntry.size_bytes,
296
- bundle_sha256: fileEntry.sha256,
297
- current_size: null,
298
- current_sha256: null,
299
- });
300
- continue;
301
- }
302
-
303
- // Retired-feature file from a legacy bundle: nothing consumes it
304
- // anymore, so drop it silently instead of blocking the restore with
305
- // an UNKNOWN_ARCHIVE_PATH conflict.
306
- if (
307
- isRetiredArchivePath(fileEntry.path) ||
308
- isGatewayArchivePath(fileEntry.path)
309
- ) {
310
- log.info(
311
- { path: fileEntry.path },
312
- "Archive path is not workspace data — will be skipped on import",
313
- );
314
- files.push({
315
- path: fileEntry.path,
316
- action: "skip",
317
- bundle_size: fileEntry.size_bytes,
318
- bundle_sha256: fileEntry.sha256,
319
- current_size: null,
320
- current_sha256: null,
321
- });
322
- continue;
323
- }
324
-
325
- // Unknown archive path — would have nowhere to write
326
- conflicts.push({
327
- code: "UNKNOWN_ARCHIVE_PATH",
328
- message: `Archive path "${fileEntry.path}" has no known disk target — it would be skipped during import`,
329
- path: fileEntry.path,
330
- });
331
- files.push({
332
- path: fileEntry.path,
333
- action: "skip",
334
- bundle_size: fileEntry.size_bytes,
335
- bundle_sha256: fileEntry.sha256,
336
- current_size: null,
337
- current_sha256: null,
338
- });
339
- continue;
340
- }
341
-
342
- let currentSize: number | null = null;
343
- let currentSha256: string | null = null;
344
- let action: ImportAction;
345
-
346
- if (existsSync(diskPath)) {
347
- try {
348
- const stat = statSync(diskPath);
349
- currentSize = stat.size;
350
- const diskData = new Uint8Array(readFileSync(diskPath));
351
- currentSha256 = sha256Hex(diskData);
352
- } catch {
353
- // If we cannot read the file, treat it as a conflict
354
- conflicts.push({
355
- code: "UNREADABLE_EXISTING_FILE",
356
- message: `Cannot read existing file at disk path for "${fileEntry.path}" — import would overwrite it`,
357
- path: fileEntry.path,
358
- });
359
- action = "overwrite";
360
- files.push({
361
- path: fileEntry.path,
362
- action,
363
- bundle_size: fileEntry.size_bytes,
364
- bundle_sha256: fileEntry.sha256,
365
- current_size: currentSize,
366
- current_sha256: currentSha256,
367
- });
368
- continue;
369
- }
370
-
371
- if (currentSha256 === fileEntry.sha256) {
372
- action = "unchanged";
373
- } else {
374
- action = "overwrite";
375
- }
376
- } else {
377
- action = "create";
378
- }
379
-
380
- files.push({
381
- path: fileEntry.path,
382
- action,
383
- bundle_size: fileEntry.size_bytes,
384
- bundle_sha256: fileEntry.sha256,
385
- current_size: currentSize,
386
- current_sha256: currentSha256,
387
- });
388
- }
389
-
390
- const summary = {
391
- total_files: files.length,
392
- files_to_create: files.filter((f) => f.action === "create").length,
393
- files_to_overwrite: files.filter((f) => f.action === "overwrite").length,
394
- files_unchanged: files.filter((f) => f.action === "unchanged").length,
395
- files_to_skip: files.filter((f) => f.action === "skip").length,
396
- };
397
-
398
- return {
399
- can_import: conflicts.length === 0,
400
- summary,
401
- files,
402
- conflicts,
403
- manifest,
404
- };
405
- }
@@ -1,241 +0,0 @@
1
- /**
2
- * Shared invariants and predicate functions consumed by both the buffer-
3
- * based `commitImport` and the streaming `streamCommitImport`. These
4
- * decisions must stay in lockstep across both importers — moving them
5
- * here removes the parallel-implementation skew risk that would otherwise
6
- * grow as either importer evolves.
7
- *
8
- * Pure: no `node:fs`, no I/O, no async. Functions over strings + manifest
9
- * data shapes only.
10
- */
11
-
12
- export const LEGACY_USER_MD_ARCHIVE_PATH = "prompts/USER.md";
13
-
14
- export const CONFIG_ARCHIVE_PATHS: ReadonlySet<string> = new Set([
15
- "workspace/config.json",
16
- "config/settings.json",
17
- ]);
18
-
19
- export const CREDENTIAL_METADATA_ARCHIVE_PATH =
20
- "workspace/data/credentials/metadata.json";
21
-
22
- export const WORKSPACE_PRESERVE_PATHS: readonly string[] = [
23
- "embedding-models",
24
- "deprecated",
25
- "data/db",
26
- "data/qdrant",
27
- ];
28
-
29
- export function isWorkspaceNamespacedArchivePath(archivePath: string): boolean {
30
- return archivePath.startsWith("workspace/");
31
- }
32
-
33
- export function isLegacyPersonaArchivePath(archivePath: string): boolean {
34
- return archivePath === LEGACY_USER_MD_ARCHIVE_PATH;
35
- }
36
-
37
- /**
38
- * Archive paths for retired features. Bundles exported while those features
39
- * existed still contain these entries; preflight and both importers must
40
- * treat them as silent non-blocking skips (no conflict, no warning) so old
41
- * backups stay restorable without noise, and the files are never written
42
- * back to the workspace.
43
- *
44
- * `UPDATES.md` carried the retired update-bulletin release notes. Legacy
45
- * bundles archived it as `prompts/UPDATES.md`; current-format bundles walk
46
- * the workspace dir, so it appears as `workspace/UPDATES.md`. Both spellings
47
- * must be retired — `DefaultPathResolver.resolve()` short-circuits them to
48
- * `null` before any prefix handling so the workspace-format entry cannot
49
- * resolve normally and recreate the file on import.
50
- */
51
- export const RETIRED_ARCHIVE_PATHS: ReadonlySet<string> = new Set([
52
- "prompts/UPDATES.md",
53
- "workspace/UPDATES.md",
54
- ]);
55
-
56
- export function isRetiredArchivePath(archivePath: string): boolean {
57
- return RETIRED_ARCHIVE_PATHS.has(archivePath);
58
- }
59
-
60
- /**
61
- * `gateway/` holds the gateway's own database and logs, added by the debug
62
- * export profile for Cadet staff to open on a debug clone. It is never
63
- * part of a workspace, so preflight and both importers skip it silently,
64
- * the same way they skip retired paths.
65
- */
66
- export function isGatewayArchivePath(archivePath: string): boolean {
67
- return archivePath.startsWith("gateway/");
68
- }
69
-
70
- export function isConfigArchivePath(archivePath: string): boolean {
71
- return CONFIG_ARCHIVE_PATHS.has(archivePath);
72
- }
73
-
74
- export function isCredentialMetadataArchivePath(archivePath: string): boolean {
75
- return archivePath === CREDENTIAL_METADATA_ARCHIVE_PATH;
76
- }
77
-
78
- /**
79
- * Partition `WORKSPACE_PRESERVE_PATHS` into the two skip sets the buffer
80
- * importer's selective-clear loop consumes:
81
- *
82
- * - `topLevelSkipDirs`: single-segment preserve-paths (e.g. "embedding-models").
83
- * - `dataSubdirSkipDirs`: second segment of `data/<x>` preserve-paths
84
- * (e.g. "db" for "data/db").
85
- *
86
- * Stays in sync with WORKSPACE_PRESERVE_PATHS automatically — adding a
87
- * new entry of either shape doesn't require touching the buffer importer.
88
- * Multi-segment paths outside the `data/` subtree are intentionally
89
- * unsupported here; the buffer importer's walk doesn't recurse into
90
- * arbitrary subdirs. If a future preserve-path needs deeper coverage,
91
- * widen this helper and the buffer importer's walk together.
92
- */
93
- export function partitionWorkspacePreserveSkipDirs(): {
94
- topLevelSkipDirs: ReadonlySet<string>;
95
- dataSubdirSkipDirs: ReadonlySet<string>;
96
- } {
97
- const topLevelSkipDirs = new Set<string>();
98
- const dataSubdirSkipDirs = new Set<string>();
99
- for (const rel of WORKSPACE_PRESERVE_PATHS) {
100
- const parts = rel.split("/");
101
- if (parts.length === 1) {
102
- topLevelSkipDirs.add(parts[0]!);
103
- } else if (parts.length === 2 && parts[0] === "data") {
104
- dataSubdirSkipDirs.add(parts[1]!);
105
- }
106
- }
107
- return { topLevelSkipDirs, dataSubdirSkipDirs };
108
- }
109
-
110
- export const LEGACY_RUNTIME_VERSION_SENTINEL = "0.0.0-legacy";
111
-
112
- type SemverTriple = readonly [number, number, number];
113
-
114
- function parseSemverTriple(version: string): SemverTriple | null {
115
- // Strip optional prerelease/build suffix ("-foo", "+sha"). We treat
116
- // "0.7.1-staging.1" as equal-to-release "0.7.1" for gating purposes.
117
- // The cloud-side check uses packaging.version.Version, which sorts
118
- // prereleases BEFORE the corresponding release; matching that exactly
119
- // would require a fuller parser — for runtime-side defense-in-depth
120
- // this conservative read is sufficient (matches release of the same
121
- // base triple).
122
- const base = version.split(/[-+]/)[0] ?? version;
123
- const parts = base.split(".");
124
- if (parts.length !== 3) {
125
- return null;
126
- }
127
- // Reject components with anything other than ASCII digits to avoid
128
- // Number.parseInt's lenient prefix-parse — e.g. "0.8.0foo" would
129
- // otherwise coerce to [0, 8, 0] and silently pass the gate, defeating
130
- // the parse-failure-fail-open contract in evaluateRuntimeCompatibility.
131
- // Leading zeros (e.g. "01.02.03") are intentionally accepted since
132
- // they round-trip to the same numeric triple as the un-padded form.
133
- if (!parts.every((p) => /^\d+$/.test(p))) {
134
- return null;
135
- }
136
- const [maj, min, pat] = parts.map((p) => Number(p));
137
- if (![maj, min, pat].every((n) => Number.isFinite(n) && n >= 0)) {
138
- return null;
139
- }
140
- return [maj!, min!, pat!] as const;
141
- }
142
-
143
- // -1 if a < b, 0 if equal, +1 if a > b. Returns null on parse failure.
144
- export function compareSemver(a: string, b: string): -1 | 0 | 1 | null {
145
- const ta = parseSemverTriple(a);
146
- const tb = parseSemverTriple(b);
147
- if (!ta || !tb) {
148
- return null;
149
- }
150
- for (let i = 0; i < 3; i++) {
151
- if (ta[i]! < tb[i]!) {
152
- return -1;
153
- }
154
- if (ta[i]! > tb[i]!) {
155
- return +1;
156
- }
157
- }
158
- return 0;
159
- }
160
-
161
- export interface RuntimeCompatibility {
162
- min_runtime_version: string;
163
- max_runtime_version: string | null;
164
- }
165
-
166
- export type RuntimeCompatibilityResult =
167
- | { ok: true }
168
- | {
169
- ok: false;
170
- reason: "version_incompatible";
171
- bundle_compat: RuntimeCompatibility;
172
- runtime_version: string;
173
- };
174
-
175
- export function formatRuntimeCompatibilityMessage(
176
- compat: RuntimeCompatibility,
177
- runtimeVersion: string,
178
- ): string {
179
- const range = compat.max_runtime_version
180
- ? `${compat.min_runtime_version}–${compat.max_runtime_version}`
181
- : `${compat.min_runtime_version}+`;
182
- return `Cannot import: bundle requires runtime ${range}, but this runtime is ${runtimeVersion}. Update your runtime before importing.`;
183
- }
184
-
185
- /**
186
- * Whether `version` names a runtime built from a source checkout: the
187
- * `<pkg.version>-local.<timestamp>.<sha>` `APP_VERSION` that the cloud
188
- * repo's `vel up` stamps on the assistant images it builds for its local
189
- * minikube cluster. The triple is the checkout's package.json version,
190
- * which only moves on release cuts, so it says nothing about whether the
191
- * checkout is newer or older than a release. A CLI hatch from a checkout
192
- * without an explicit `APP_VERSION` runs as the plain package version and
193
- * is not covered.
194
- */
195
- export function isLocalDevRuntimeVersion(version: string): boolean {
196
- return /^\d+\.\d+\.\d+-local\./.test(version);
197
- }
198
-
199
- export function evaluateRuntimeCompatibility(
200
- compat: RuntimeCompatibility,
201
- runtimeVersion: string,
202
- ): RuntimeCompatibilityResult {
203
- if (compat.min_runtime_version === LEGACY_RUNTIME_VERSION_SENTINEL) {
204
- return { ok: true };
205
- }
206
- // A local dev build skips the minimum bound, like an unparsable version
207
- // does below: its triple is not comparable to release versions (see
208
- // isLocalDevRuntimeVersion), so "runtime older than the bundle" cannot be
209
- // decided from it. The maximum bound still applies: it is an explicit
210
- // ceiling the bundle's producer recorded, and a local triple above it is
211
- // as good a signal as any release's.
212
- if (!isLocalDevRuntimeVersion(runtimeVersion)) {
213
- const minCmp = compareSemver(runtimeVersion, compat.min_runtime_version);
214
- if (minCmp === null) {
215
- return { ok: true };
216
- }
217
- if (minCmp < 0) {
218
- return {
219
- ok: false,
220
- reason: "version_incompatible",
221
- bundle_compat: compat,
222
- runtime_version: runtimeVersion,
223
- };
224
- }
225
- }
226
- if (compat.max_runtime_version !== null) {
227
- const maxCmp = compareSemver(runtimeVersion, compat.max_runtime_version);
228
- if (maxCmp === null) {
229
- return { ok: true };
230
- }
231
- if (maxCmp > 0) {
232
- return {
233
- ok: false,
234
- reason: "version_incompatible",
235
- bundle_compat: compat,
236
- runtime_version: runtimeVersion,
237
- };
238
- }
239
- }
240
- return { ok: true };
241
- }