@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,826 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Commits a validated .vbundle archive to disk.
|
|
3
|
-
*
|
|
4
|
-
* Given a .vbundle archive, this module:
|
|
5
|
-
* 1. Validates the bundle (decompresses and parses once — reuses the entries
|
|
6
|
-
* from validation to avoid a second decompression pass)
|
|
7
|
-
* 2. Backs up existing files before overwriting
|
|
8
|
-
* 3. Writes bundle files to their target disk locations
|
|
9
|
-
* 4. Verifies written files match expected checksums (post-write integrity)
|
|
10
|
-
* 5. Returns a detailed import report
|
|
11
|
-
*
|
|
12
|
-
* Backup files are stored alongside the originals with a timestamped suffix.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { createHash } from "node:crypto";
|
|
16
|
-
import {
|
|
17
|
-
copyFileSync,
|
|
18
|
-
existsSync,
|
|
19
|
-
lstatSync,
|
|
20
|
-
mkdirSync,
|
|
21
|
-
readdirSync,
|
|
22
|
-
readFileSync,
|
|
23
|
-
readlinkSync,
|
|
24
|
-
rmSync,
|
|
25
|
-
symlinkSync,
|
|
26
|
-
writeFileSync,
|
|
27
|
-
} from "node:fs";
|
|
28
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
29
|
-
|
|
30
|
-
import { sanitizeConfigForTransfer } from "../../config/sanitize-for-transfer.js";
|
|
31
|
-
import { isGuardianPersonaCustomized } from "../../prompts/persona-resolver.js";
|
|
32
|
-
import { getLogger } from "../../util/logger.js";
|
|
33
|
-
import { APP_VERSION } from "../../version.js";
|
|
34
|
-
import type { PathResolver } from "./vbundle-import-analyzer.js";
|
|
35
|
-
import type { RuntimeCompatibility } from "./vbundle-import-policy.js";
|
|
36
|
-
import * as policy from "./vbundle-import-policy.js";
|
|
37
|
-
import { mergeMetadataPreservingCadet } from "./vbundle-metadata-merge.js";
|
|
38
|
-
import type { ManifestType, VBundleTarEntry } from "./vbundle-validator.js";
|
|
39
|
-
import { validateVBundle } from "./vbundle-validator.js";
|
|
40
|
-
|
|
41
|
-
const log = getLogger("vbundle-importer");
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Public types
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
export type ImportFileAction = "created" | "overwritten" | "skipped";
|
|
48
|
-
|
|
49
|
-
export interface ImportedFileReport {
|
|
50
|
-
/** Archive path (e.g. "data/db/assistant.db") */
|
|
51
|
-
path: string;
|
|
52
|
-
/** Disk path the file was written to */
|
|
53
|
-
disk_path: string;
|
|
54
|
-
/** What happened to this file */
|
|
55
|
-
action: ImportFileAction;
|
|
56
|
-
/** Size of the written file in bytes */
|
|
57
|
-
size: number;
|
|
58
|
-
/** SHA-256 of the written file */
|
|
59
|
-
sha256: string;
|
|
60
|
-
/** Path to the backup file, if one was created */
|
|
61
|
-
backup_path: string | null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface ImportCommitReport {
|
|
65
|
-
/** Whether the import succeeded */
|
|
66
|
-
success: boolean;
|
|
67
|
-
/** Summary of what was imported */
|
|
68
|
-
summary: {
|
|
69
|
-
total_files: number;
|
|
70
|
-
files_created: number;
|
|
71
|
-
files_overwritten: number;
|
|
72
|
-
files_skipped: number;
|
|
73
|
-
backups_created: number;
|
|
74
|
-
};
|
|
75
|
-
/** Per-file import details */
|
|
76
|
-
files: ImportedFileReport[];
|
|
77
|
-
/** The manifest from the imported bundle */
|
|
78
|
-
manifest: ManifestType;
|
|
79
|
-
/** Any integrity warnings (non-fatal) */
|
|
80
|
-
warnings: string[];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export type ImportCommitResult =
|
|
84
|
-
| { ok: true; report: ImportCommitReport }
|
|
85
|
-
| {
|
|
86
|
-
ok: false;
|
|
87
|
-
reason: "validation_failed";
|
|
88
|
-
errors: Array<{ code: string; message: string; path?: string }>;
|
|
89
|
-
}
|
|
90
|
-
| { ok: false; reason: "extraction_failed"; message: string }
|
|
91
|
-
| {
|
|
92
|
-
ok: false;
|
|
93
|
-
reason: "version_incompatible";
|
|
94
|
-
bundle_compat: RuntimeCompatibility;
|
|
95
|
-
runtime_version: string;
|
|
96
|
-
}
|
|
97
|
-
| {
|
|
98
|
-
ok: false;
|
|
99
|
-
reason: "write_failed";
|
|
100
|
-
message: string;
|
|
101
|
-
partial_report?: ImportCommitReport;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
// Hash helper
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
|
|
108
|
-
function sha256Hex(data: Uint8Array): string {
|
|
109
|
-
return createHash("sha256").update(data).digest("hex");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
// Backup helper
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
function generateBackupPath(diskPath: string): string {
|
|
117
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
118
|
-
return `${diskPath}.backup-${timestamp}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Defense-in-depth: returns true if `linkTarget`, when resolved relative to
|
|
123
|
-
* the symlink's own directory (`dirname(diskPath)`), lands outside the
|
|
124
|
-
* supplied `workspaceDir`. The validator (`validateVBundle`) already enforces
|
|
125
|
-
* archive-relative containment, but we re-check here so the buffer importer
|
|
126
|
-
* is safe even if a caller passes a hand-built `preValidatedManifest`.
|
|
127
|
-
*
|
|
128
|
-
* Returns false when `workspaceDir` is undefined — the importer is permitted
|
|
129
|
-
* to write outside any workspace in that mode (e.g. legacy hooks-only
|
|
130
|
-
* imports).
|
|
131
|
-
*/
|
|
132
|
-
function isOutsideWorkspace(
|
|
133
|
-
diskPath: string,
|
|
134
|
-
linkTarget: string,
|
|
135
|
-
workspaceDir: string | undefined,
|
|
136
|
-
): boolean {
|
|
137
|
-
if (!workspaceDir) {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
const resolved = resolve(dirname(diskPath), linkTarget);
|
|
141
|
-
const ws = resolve(workspaceDir);
|
|
142
|
-
return resolved !== ws && !resolved.startsWith(ws + sep);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
// Core importer
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
|
|
149
|
-
export interface ImportCommitOptions {
|
|
150
|
-
/** Raw .vbundle archive bytes — used only when pre-validated data is not provided. */
|
|
151
|
-
archiveData: Uint8Array;
|
|
152
|
-
/** Resolves archive paths to disk paths */
|
|
153
|
-
pathResolver: PathResolver;
|
|
154
|
-
/** Pre-validated manifest from a prior validateVBundle call. When provided
|
|
155
|
-
* with `preValidatedEntries`, skips internal re-validation to avoid
|
|
156
|
-
* holding two copies of decompressed data in memory. */
|
|
157
|
-
preValidatedManifest?: ManifestType;
|
|
158
|
-
/** Pre-parsed tar entries from a prior validateVBundle call. */
|
|
159
|
-
preValidatedEntries?: Map<string, VBundleTarEntry>;
|
|
160
|
-
/**
|
|
161
|
-
* Absolute path to the workspace directory. When set and the bundle
|
|
162
|
-
* contains workspace/ entries, the workspace is cleared (except
|
|
163
|
-
* skip dirs) before writing to ensure an exact-match restore.
|
|
164
|
-
*/
|
|
165
|
-
workspaceDir?: string;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Validate, extract, and write a .vbundle archive to disk.
|
|
170
|
-
*
|
|
171
|
-
* This is a destructive operation — files on disk will be overwritten.
|
|
172
|
-
* Existing files are backed up before being replaced. The bundle is
|
|
173
|
-
* re-validated before any state mutation to prevent writing corrupt data.
|
|
174
|
-
*/
|
|
175
|
-
export function commitImport(options: ImportCommitOptions): ImportCommitResult {
|
|
176
|
-
const {
|
|
177
|
-
archiveData,
|
|
178
|
-
pathResolver,
|
|
179
|
-
preValidatedManifest,
|
|
180
|
-
preValidatedEntries,
|
|
181
|
-
workspaceDir,
|
|
182
|
-
} = options;
|
|
183
|
-
|
|
184
|
-
let manifest: ManifestType;
|
|
185
|
-
let entryMap: Map<string, VBundleTarEntry>;
|
|
186
|
-
|
|
187
|
-
if (preValidatedManifest && preValidatedEntries) {
|
|
188
|
-
// Caller already validated and decompressed — reuse directly
|
|
189
|
-
manifest = preValidatedManifest;
|
|
190
|
-
entryMap = preValidatedEntries;
|
|
191
|
-
} else {
|
|
192
|
-
// Validate the bundle (validation before mutation).
|
|
193
|
-
// validateVBundle decompresses and parses the tar, returning the entries
|
|
194
|
-
// alongside the validation result so we avoid a second decompression.
|
|
195
|
-
const validation = validateVBundle(archiveData);
|
|
196
|
-
if (!validation.is_valid || !validation.manifest || !validation.entries) {
|
|
197
|
-
return {
|
|
198
|
-
ok: false,
|
|
199
|
-
reason: "validation_failed",
|
|
200
|
-
errors: validation.errors,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
manifest = validation.manifest;
|
|
205
|
-
entryMap = validation.entries;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Defense-in-depth: refuse to import a bundle whose declared compat range
|
|
209
|
-
// excludes this runtime BEFORE any state mutation. The cloud-side gate
|
|
210
|
-
// is the primary check; this catches legacy bundles whose ExportJob row
|
|
211
|
-
// predates PR #5470 (compat columns NULL → platform gate skipped) and
|
|
212
|
-
// any caller that bypasses the cloud-issued signed URL flow.
|
|
213
|
-
const compatResult = policy.evaluateRuntimeCompatibility(
|
|
214
|
-
manifest.compatibility,
|
|
215
|
-
APP_VERSION,
|
|
216
|
-
);
|
|
217
|
-
if (!compatResult.ok) {
|
|
218
|
-
return {
|
|
219
|
-
ok: false,
|
|
220
|
-
reason: "version_incompatible",
|
|
221
|
-
bundle_compat: compatResult.bundle_compat,
|
|
222
|
-
runtime_version: compatResult.runtime_version,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Directories to preserve when clearing the workspace. Derived from the
|
|
227
|
-
// shared WORKSPACE_PRESERVE_PATHS list so the streaming importer's
|
|
228
|
-
// carry-over logic and this in-place clear stay in sync.
|
|
229
|
-
const { topLevelSkipDirs, dataSubdirSkipDirs } =
|
|
230
|
-
policy.partitionWorkspacePreserveSkipDirs();
|
|
231
|
-
|
|
232
|
-
// Step 1b: Clear the workspace directory before restore if the bundle
|
|
233
|
-
// contains new-format workspace/ entries. This ensures an exact-match
|
|
234
|
-
// restore with no stale files left behind. Skips embedding-models/,
|
|
235
|
-
// data/qdrant/ (large, regenerable), and data/db/ (critical — prevents
|
|
236
|
-
// data loss if the import fails partway or the archive omits the DB).
|
|
237
|
-
//
|
|
238
|
-
// Only new-format bundles (workspace/ prefix) trigger clearing. Old-format
|
|
239
|
-
// bundles (skills/, hooks/, data/db/*, config/*) wrote specific files
|
|
240
|
-
// without clearing — preserving that behavior avoids wiping workspace
|
|
241
|
-
// data when importing legacy bundles.
|
|
242
|
-
//
|
|
243
|
-
// Gate on resolution: at least one workspace/ entry must resolve to a
|
|
244
|
-
// valid disk path. This prevents path-traversal entries (e.g.
|
|
245
|
-
// "workspace/../../etc/passwd") from triggering a workspace purge while
|
|
246
|
-
// resolving to nothing.
|
|
247
|
-
const hasWorkspaceEntries = manifest.contents.some(
|
|
248
|
-
(f) =>
|
|
249
|
-
policy.isWorkspaceNamespacedArchivePath(f.path) &&
|
|
250
|
-
!!pathResolver.resolve(f.path),
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
// Capture the target's credential metadata BEFORE the workspace clear
|
|
254
|
-
// runs. Step 1b wipes `data/credentials/`, so reading live metadata
|
|
255
|
-
// later (during the per-file write loop) would always miss. The merge
|
|
256
|
-
// helper needs this content to preserve the target's cloud-identity
|
|
257
|
-
// (`cadet:*`) entries across the overwrite.
|
|
258
|
-
let liveCredentialMetadataJson: string | null = null;
|
|
259
|
-
const credentialMetadataDiskPath = pathResolver.resolve(
|
|
260
|
-
policy.CREDENTIAL_METADATA_ARCHIVE_PATH,
|
|
261
|
-
);
|
|
262
|
-
if (credentialMetadataDiskPath && existsSync(credentialMetadataDiskPath)) {
|
|
263
|
-
try {
|
|
264
|
-
liveCredentialMetadataJson = readFileSync(
|
|
265
|
-
credentialMetadataDiskPath,
|
|
266
|
-
"utf-8",
|
|
267
|
-
);
|
|
268
|
-
} catch (err) {
|
|
269
|
-
log.warn(
|
|
270
|
-
{ err, path: credentialMetadataDiskPath },
|
|
271
|
-
"Failed to read live credential metadata before import; cadet:* entries may not be preserved",
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
let workspaceWasCleared = false;
|
|
277
|
-
if (hasWorkspaceEntries && workspaceDir && existsSync(workspaceDir)) {
|
|
278
|
-
try {
|
|
279
|
-
// Clear workspace contents selectively, preserving skip dirs
|
|
280
|
-
const topEntries = readdirSync(workspaceDir, { withFileTypes: true });
|
|
281
|
-
for (const entry of topEntries) {
|
|
282
|
-
if (topLevelSkipDirs.has(entry.name)) {
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const entryPath = join(workspaceDir, entry.name);
|
|
287
|
-
if (entry.name === "data" && entry.isDirectory()) {
|
|
288
|
-
// Inside data/, preserve qdrant/ (large, regenerable) and db/
|
|
289
|
-
// (critical user data) but clear everything else
|
|
290
|
-
const dataEntries = readdirSync(entryPath, { withFileTypes: true });
|
|
291
|
-
for (const dataEntry of dataEntries) {
|
|
292
|
-
if (dataSubdirSkipDirs.has(dataEntry.name)) {
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
rmSync(join(entryPath, dataEntry.name), {
|
|
296
|
-
recursive: true,
|
|
297
|
-
force: true,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
} else {
|
|
301
|
-
rmSync(entryPath, { recursive: true, force: true });
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
workspaceWasCleared = true;
|
|
305
|
-
} catch (err) {
|
|
306
|
-
return {
|
|
307
|
-
ok: false,
|
|
308
|
-
reason: "write_failed",
|
|
309
|
-
message: `Failed to clear workspace directory "${workspaceDir}": ${
|
|
310
|
-
err instanceof Error ? err.message : String(err)
|
|
311
|
-
}`,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Step 2: Write files to disk with backups
|
|
317
|
-
const importedFiles: ImportedFileReport[] = [];
|
|
318
|
-
const warnings: string[] = [];
|
|
319
|
-
let backupsCreated = 0;
|
|
320
|
-
|
|
321
|
-
for (const fileEntry of manifest.contents) {
|
|
322
|
-
// Credential entries are handled separately by extractCredentialsFromBundle()
|
|
323
|
-
// in migration-routes.ts — skip them silently without warnings or skip counts.
|
|
324
|
-
if (fileEntry.path.startsWith("credentials/")) {
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const diskPath = pathResolver.resolve(fileEntry.path);
|
|
329
|
-
|
|
330
|
-
if (!diskPath) {
|
|
331
|
-
// Unknown archive path — skip it. Retired-feature paths (see
|
|
332
|
-
// `policy.RETIRED_ARCHIVE_PATHS`) are expected in legacy bundles and
|
|
333
|
-
// skip silently so the import report matches preflight; anything else
|
|
334
|
-
// unresolvable earns a warning.
|
|
335
|
-
importedFiles.push({
|
|
336
|
-
path: fileEntry.path,
|
|
337
|
-
disk_path: "",
|
|
338
|
-
action: "skipped",
|
|
339
|
-
size: fileEntry.size_bytes,
|
|
340
|
-
sha256: fileEntry.sha256,
|
|
341
|
-
backup_path: null,
|
|
342
|
-
});
|
|
343
|
-
if (
|
|
344
|
-
!policy.isRetiredArchivePath(fileEntry.path) &&
|
|
345
|
-
!policy.isGatewayArchivePath(fileEntry.path)
|
|
346
|
-
) {
|
|
347
|
-
warnings.push(
|
|
348
|
-
`Skipped "${fileEntry.path}": no known disk target for this archive path`,
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Symlink branch: recreate the entry on disk as a real symlink so the
|
|
355
|
-
// post-import workspace mirrors the source's link topology rather than
|
|
356
|
-
// duplicating bytes. The validator already enforces archive-relative
|
|
357
|
-
// containment, sha256-over-target, and size==0 — we still reapply
|
|
358
|
-
// absolute-target and workspace-escape gates here so a hand-built
|
|
359
|
-
// `preValidatedManifest` cannot bypass them.
|
|
360
|
-
if (fileEntry.link_target !== undefined) {
|
|
361
|
-
const archiveEntry = entryMap.get(fileEntry.path);
|
|
362
|
-
if (!archiveEntry) {
|
|
363
|
-
importedFiles.push({
|
|
364
|
-
path: fileEntry.path,
|
|
365
|
-
disk_path: diskPath,
|
|
366
|
-
action: "skipped",
|
|
367
|
-
size: 0,
|
|
368
|
-
sha256: fileEntry.sha256,
|
|
369
|
-
backup_path: null,
|
|
370
|
-
});
|
|
371
|
-
warnings.push(
|
|
372
|
-
`Skipped "${fileEntry.path}": declared in manifest but not found in archive`,
|
|
373
|
-
);
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Legacy guardian persona (prompts/USER.md) is translated to the
|
|
378
|
-
// current guardian's users/<slug>.md by DefaultPathResolver. If the
|
|
379
|
-
// bundle ships USER.md as a symlink and the target already holds
|
|
380
|
-
// user-authored content, skip rather than clobber — mirrors the
|
|
381
|
-
// protection in the regular-file branch below.
|
|
382
|
-
if (
|
|
383
|
-
policy.isLegacyPersonaArchivePath(fileEntry.path) &&
|
|
384
|
-
isGuardianPersonaCustomized(diskPath)
|
|
385
|
-
) {
|
|
386
|
-
log.warn(
|
|
387
|
-
{ archivePath: fileEntry.path, diskPath },
|
|
388
|
-
"Skipping legacy prompts/USER.md symlink import: guardian persona is already customized",
|
|
389
|
-
);
|
|
390
|
-
warnings.push(
|
|
391
|
-
`Skipped "${fileEntry.path}": guardian persona at "${diskPath}" is already customized`,
|
|
392
|
-
);
|
|
393
|
-
importedFiles.push({
|
|
394
|
-
path: fileEntry.path,
|
|
395
|
-
disk_path: diskPath,
|
|
396
|
-
action: "skipped",
|
|
397
|
-
size: 0,
|
|
398
|
-
sha256: fileEntry.sha256,
|
|
399
|
-
backup_path: null,
|
|
400
|
-
});
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Defense-in-depth path-traversal gate.
|
|
405
|
-
if (
|
|
406
|
-
fileEntry.link_target.startsWith("/") ||
|
|
407
|
-
isOutsideWorkspace(diskPath, fileEntry.link_target, workspaceDir)
|
|
408
|
-
) {
|
|
409
|
-
importedFiles.push({
|
|
410
|
-
path: fileEntry.path,
|
|
411
|
-
disk_path: diskPath,
|
|
412
|
-
action: "skipped",
|
|
413
|
-
size: 0,
|
|
414
|
-
sha256: fileEntry.sha256,
|
|
415
|
-
backup_path: null,
|
|
416
|
-
});
|
|
417
|
-
warnings.push(
|
|
418
|
-
`Skipped "${fileEntry.path}": symlink target "${fileEntry.link_target}" escapes workspace`,
|
|
419
|
-
);
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Back up an existing entry at diskPath, if any. Use `lstatSync` so we
|
|
424
|
-
// detect a pre-existing dangling symlink (which `existsSync` reports
|
|
425
|
-
// as missing) — `symlinkSync` would otherwise fail with EEXIST. For
|
|
426
|
-
// regular files and resolvable symlinks we copy the file contents into
|
|
427
|
-
// the backup, matching the existing contract; for dangling symlinks
|
|
428
|
-
// we preserve the linkname via `readlinkSync`+`symlinkSync` so the
|
|
429
|
-
// original entry can be inspected after the import. The pre-existing
|
|
430
|
-
// entry is removed before `symlinkSync` so the new symlink can land.
|
|
431
|
-
let backupPath: string | null = null;
|
|
432
|
-
let action: ImportFileAction;
|
|
433
|
-
let preExistingEntry = false;
|
|
434
|
-
let preExistingIsSymlink = false;
|
|
435
|
-
try {
|
|
436
|
-
const stats = lstatSync(diskPath);
|
|
437
|
-
preExistingEntry = true;
|
|
438
|
-
preExistingIsSymlink = stats.isSymbolicLink();
|
|
439
|
-
} catch {
|
|
440
|
-
// ENOENT — no pre-existing entry at this path.
|
|
441
|
-
}
|
|
442
|
-
if (preExistingEntry) {
|
|
443
|
-
backupPath = generateBackupPath(diskPath);
|
|
444
|
-
try {
|
|
445
|
-
if (preExistingIsSymlink) {
|
|
446
|
-
const oldTarget = readlinkSync(diskPath);
|
|
447
|
-
symlinkSync(oldTarget, backupPath);
|
|
448
|
-
} else {
|
|
449
|
-
copyFileSync(diskPath, backupPath);
|
|
450
|
-
}
|
|
451
|
-
backupsCreated++;
|
|
452
|
-
} catch (err) {
|
|
453
|
-
return {
|
|
454
|
-
ok: false,
|
|
455
|
-
reason: "write_failed",
|
|
456
|
-
message: `Failed to back up "${diskPath}": ${
|
|
457
|
-
err instanceof Error ? err.message : String(err)
|
|
458
|
-
}`,
|
|
459
|
-
partial_report: buildPartialReport(
|
|
460
|
-
importedFiles,
|
|
461
|
-
manifest,
|
|
462
|
-
warnings,
|
|
463
|
-
backupsCreated,
|
|
464
|
-
),
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
action = "overwritten";
|
|
468
|
-
try {
|
|
469
|
-
rmSync(diskPath, { force: true });
|
|
470
|
-
} catch {
|
|
471
|
-
/* best effort — symlinkSync below will surface the real error */
|
|
472
|
-
}
|
|
473
|
-
} else {
|
|
474
|
-
action = "created";
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Ensure parent directory exists.
|
|
478
|
-
const parentDir = dirname(diskPath);
|
|
479
|
-
if (!existsSync(parentDir)) {
|
|
480
|
-
try {
|
|
481
|
-
mkdirSync(parentDir, { recursive: true });
|
|
482
|
-
} catch (err) {
|
|
483
|
-
return {
|
|
484
|
-
ok: false,
|
|
485
|
-
reason: "write_failed",
|
|
486
|
-
message: `Failed to create directory "${parentDir}": ${
|
|
487
|
-
err instanceof Error ? err.message : String(err)
|
|
488
|
-
}`,
|
|
489
|
-
partial_report: buildPartialReport(
|
|
490
|
-
importedFiles,
|
|
491
|
-
manifest,
|
|
492
|
-
warnings,
|
|
493
|
-
backupsCreated,
|
|
494
|
-
),
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Create the symlink. The target is stored verbatim — OS symlink
|
|
500
|
-
// semantics resolve it relative to the symlink's own directory at
|
|
501
|
-
// use time.
|
|
502
|
-
try {
|
|
503
|
-
symlinkSync(fileEntry.link_target, diskPath);
|
|
504
|
-
} catch (err) {
|
|
505
|
-
return {
|
|
506
|
-
ok: false,
|
|
507
|
-
reason: "write_failed",
|
|
508
|
-
message: `Failed to create symlink "${diskPath}" -> "${fileEntry.link_target}": ${
|
|
509
|
-
err instanceof Error ? err.message : String(err)
|
|
510
|
-
}`,
|
|
511
|
-
partial_report: buildPartialReport(
|
|
512
|
-
importedFiles,
|
|
513
|
-
manifest,
|
|
514
|
-
warnings,
|
|
515
|
-
backupsCreated,
|
|
516
|
-
),
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
importedFiles.push({
|
|
521
|
-
path: fileEntry.path,
|
|
522
|
-
disk_path: diskPath,
|
|
523
|
-
action,
|
|
524
|
-
size: 0,
|
|
525
|
-
sha256: fileEntry.sha256,
|
|
526
|
-
backup_path: backupPath,
|
|
527
|
-
});
|
|
528
|
-
// Skip the regular-file branches (and the post-write integrity check,
|
|
529
|
-
// which would dereference the symlink and read the target's bytes).
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const archiveEntry = entryMap.get(fileEntry.path);
|
|
534
|
-
if (!archiveEntry) {
|
|
535
|
-
// File declared in manifest but not found in archive — should not
|
|
536
|
-
// happen after validation, but guard against it
|
|
537
|
-
importedFiles.push({
|
|
538
|
-
path: fileEntry.path,
|
|
539
|
-
disk_path: diskPath,
|
|
540
|
-
action: "skipped",
|
|
541
|
-
size: fileEntry.size_bytes,
|
|
542
|
-
sha256: fileEntry.sha256,
|
|
543
|
-
backup_path: null,
|
|
544
|
-
});
|
|
545
|
-
warnings.push(
|
|
546
|
-
`Skipped "${fileEntry.path}": declared in manifest but not found in archive`,
|
|
547
|
-
);
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Legacy guardian persona (prompts/USER.md) is translated to the
|
|
552
|
-
// current guardian's users/<slug>.md by DefaultPathResolver. If
|
|
553
|
-
// that target already holds user-authored content, skip rather
|
|
554
|
-
// than clobber — the user has curated their persona since the
|
|
555
|
-
// bundle was exported.
|
|
556
|
-
if (
|
|
557
|
-
policy.isLegacyPersonaArchivePath(fileEntry.path) &&
|
|
558
|
-
isGuardianPersonaCustomized(diskPath)
|
|
559
|
-
) {
|
|
560
|
-
log.warn(
|
|
561
|
-
{ archivePath: fileEntry.path, diskPath },
|
|
562
|
-
"Skipping legacy prompts/USER.md import: guardian persona is already customized",
|
|
563
|
-
);
|
|
564
|
-
warnings.push(
|
|
565
|
-
`Skipped "${fileEntry.path}": guardian persona at "${diskPath}" is already customized`,
|
|
566
|
-
);
|
|
567
|
-
importedFiles.push({
|
|
568
|
-
path: fileEntry.path,
|
|
569
|
-
disk_path: diskPath,
|
|
570
|
-
action: "skipped",
|
|
571
|
-
size: fileEntry.size_bytes,
|
|
572
|
-
sha256: fileEntry.sha256,
|
|
573
|
-
backup_path: null,
|
|
574
|
-
});
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Determine action and create backup if needed
|
|
579
|
-
let backupPath: string | null = null;
|
|
580
|
-
let action: ImportFileAction;
|
|
581
|
-
|
|
582
|
-
if (existsSync(diskPath)) {
|
|
583
|
-
// Back up existing file before overwriting
|
|
584
|
-
backupPath = generateBackupPath(diskPath);
|
|
585
|
-
try {
|
|
586
|
-
copyFileSync(diskPath, backupPath);
|
|
587
|
-
backupsCreated++;
|
|
588
|
-
} catch (err) {
|
|
589
|
-
return {
|
|
590
|
-
ok: false,
|
|
591
|
-
reason: "write_failed",
|
|
592
|
-
message: `Failed to create backup of "${diskPath}": ${
|
|
593
|
-
err instanceof Error ? err.message : String(err)
|
|
594
|
-
}`,
|
|
595
|
-
partial_report: buildPartialReport(
|
|
596
|
-
importedFiles,
|
|
597
|
-
manifest,
|
|
598
|
-
warnings,
|
|
599
|
-
backupsCreated,
|
|
600
|
-
),
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
action = "overwritten";
|
|
604
|
-
} else {
|
|
605
|
-
action = "created";
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Ensure parent directory exists
|
|
609
|
-
const parentDir = dirname(diskPath);
|
|
610
|
-
if (!existsSync(parentDir)) {
|
|
611
|
-
try {
|
|
612
|
-
mkdirSync(parentDir, { recursive: true });
|
|
613
|
-
} catch (err) {
|
|
614
|
-
return {
|
|
615
|
-
ok: false,
|
|
616
|
-
reason: "write_failed",
|
|
617
|
-
message: `Failed to create directory "${parentDir}": ${
|
|
618
|
-
err instanceof Error ? err.message : String(err)
|
|
619
|
-
}`,
|
|
620
|
-
partial_report: buildPartialReport(
|
|
621
|
-
importedFiles,
|
|
622
|
-
manifest,
|
|
623
|
-
warnings,
|
|
624
|
-
backupsCreated,
|
|
625
|
-
),
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Sanitize config files to strip environment-specific fields (defense-in-depth)
|
|
631
|
-
let dataToWrite: Uint8Array = archiveEntry.data;
|
|
632
|
-
if (policy.isConfigArchivePath(fileEntry.path)) {
|
|
633
|
-
const configJson = new TextDecoder().decode(archiveEntry.data);
|
|
634
|
-
const sanitized = sanitizeConfigForTransfer(configJson);
|
|
635
|
-
dataToWrite = new TextEncoder().encode(sanitized);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Preserve target's `cadet:*` metadata entries across the overwrite.
|
|
639
|
-
// Django's post-hatch provisioning writes these on the target via
|
|
640
|
-
// POST /v1/secrets; a naive overwrite of the bundle's metadata.json
|
|
641
|
-
// would wipe them and break the gateway's cadet credential read.
|
|
642
|
-
// We use the snapshot captured BEFORE the workspace clear because
|
|
643
|
-
// Step 1b may have already removed the live file.
|
|
644
|
-
if (policy.isCredentialMetadataArchivePath(fileEntry.path)) {
|
|
645
|
-
const bundleJson = new TextDecoder().decode(archiveEntry.data);
|
|
646
|
-
const merged = mergeMetadataPreservingCadet(
|
|
647
|
-
bundleJson,
|
|
648
|
-
liveCredentialMetadataJson,
|
|
649
|
-
);
|
|
650
|
-
dataToWrite = new TextEncoder().encode(merged);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// If we're about to replace a SQLite main database file, remove any
|
|
654
|
-
// pre-existing `.db-wal`/`.db-shm`/`.db-journal` siblings at the
|
|
655
|
-
// target. Those auxiliary files are only valid as a pair with the
|
|
656
|
-
// exact `.db` that wrote them; leaving them alongside a replacement
|
|
657
|
-
// DB causes SQLite to replay incompatible WAL frames on the first
|
|
658
|
-
// open and report "database disk image is malformed". The exporter
|
|
659
|
-
// already checkpointed the source WAL into the main DB before the
|
|
660
|
-
// bundle was built, so dropping the sibling aux files doesn't lose
|
|
661
|
-
// data from the source workspace.
|
|
662
|
-
if (diskPath.endsWith(".db")) {
|
|
663
|
-
for (const suffix of [".db-wal", ".db-shm", ".db-journal"]) {
|
|
664
|
-
const auxPath = `${diskPath.slice(0, -".db".length)}${suffix}`;
|
|
665
|
-
try {
|
|
666
|
-
rmSync(auxPath, { force: true });
|
|
667
|
-
} catch {
|
|
668
|
-
/* best effort — if the aux file doesn't exist we're fine */
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Write the file
|
|
674
|
-
try {
|
|
675
|
-
writeFileSync(diskPath, dataToWrite);
|
|
676
|
-
} catch (err) {
|
|
677
|
-
return {
|
|
678
|
-
ok: false,
|
|
679
|
-
reason: "write_failed",
|
|
680
|
-
message: `Failed to write "${diskPath}": ${
|
|
681
|
-
err instanceof Error ? err.message : String(err)
|
|
682
|
-
}`,
|
|
683
|
-
partial_report: buildPartialReport(
|
|
684
|
-
importedFiles,
|
|
685
|
-
manifest,
|
|
686
|
-
warnings,
|
|
687
|
-
backupsCreated,
|
|
688
|
-
),
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Step 3: Post-write integrity check — verify the written file
|
|
693
|
-
// Use the SHA of the data we actually wrote (which may differ from the
|
|
694
|
-
// manifest SHA if the config was sanitized during import).
|
|
695
|
-
const expectedSha256 = sha256Hex(dataToWrite);
|
|
696
|
-
try {
|
|
697
|
-
const writtenData = new Uint8Array(readFileSync(diskPath));
|
|
698
|
-
const writtenSha256 = sha256Hex(writtenData);
|
|
699
|
-
|
|
700
|
-
if (writtenSha256 !== expectedSha256) {
|
|
701
|
-
warnings.push(
|
|
702
|
-
`Post-write integrity warning for "${fileEntry.path}": ` +
|
|
703
|
-
`expected SHA-256 ${expectedSha256}, got ${writtenSha256}`,
|
|
704
|
-
);
|
|
705
|
-
}
|
|
706
|
-
} catch {
|
|
707
|
-
warnings.push(
|
|
708
|
-
`Could not verify post-write integrity for "${fileEntry.path}"`,
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
importedFiles.push({
|
|
713
|
-
path: fileEntry.path,
|
|
714
|
-
disk_path: diskPath,
|
|
715
|
-
action,
|
|
716
|
-
size: dataToWrite.length,
|
|
717
|
-
sha256: expectedSha256,
|
|
718
|
-
backup_path: backupPath,
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// If Step 1b actually cleared the workspace AND the bundle did not
|
|
723
|
-
// carry a metadata.json entry, the target's cadet:* entries were
|
|
724
|
-
// wiped along with the `data/credentials/` directory. Restore them by
|
|
725
|
-
// writing a minimal file containing just the preserved entries so the
|
|
726
|
-
// gateway can still locate the cloud API key. When Step 1b did NOT
|
|
727
|
-
// run (e.g. workspaceDir unset) the live metadata.json is still on
|
|
728
|
-
// disk untouched — we must not rewrite it here or we would drop the
|
|
729
|
-
// non-cadet entries the caller chose to keep.
|
|
730
|
-
const bundleHadMetadata = manifest.contents.some((f) =>
|
|
731
|
-
policy.isCredentialMetadataArchivePath(f.path),
|
|
732
|
-
);
|
|
733
|
-
if (
|
|
734
|
-
workspaceWasCleared &&
|
|
735
|
-
!bundleHadMetadata &&
|
|
736
|
-
liveCredentialMetadataJson &&
|
|
737
|
-
credentialMetadataDiskPath
|
|
738
|
-
) {
|
|
739
|
-
const merged = mergeMetadataPreservingCadet(
|
|
740
|
-
JSON.stringify({ version: 5, credentials: [] }),
|
|
741
|
-
liveCredentialMetadataJson,
|
|
742
|
-
);
|
|
743
|
-
try {
|
|
744
|
-
mkdirSync(dirname(credentialMetadataDiskPath), { recursive: true });
|
|
745
|
-
writeFileSync(credentialMetadataDiskPath, merged);
|
|
746
|
-
} catch (err) {
|
|
747
|
-
warnings.push(
|
|
748
|
-
`Failed to restore preserved cadet:* credential metadata: ${
|
|
749
|
-
err instanceof Error ? err.message : String(err)
|
|
750
|
-
}`,
|
|
751
|
-
);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// Build final report
|
|
756
|
-
const report: ImportCommitReport = {
|
|
757
|
-
success: true,
|
|
758
|
-
summary: {
|
|
759
|
-
total_files: importedFiles.length,
|
|
760
|
-
files_created: importedFiles.filter((f) => f.action === "created").length,
|
|
761
|
-
files_overwritten: importedFiles.filter((f) => f.action === "overwritten")
|
|
762
|
-
.length,
|
|
763
|
-
files_skipped: importedFiles.filter((f) => f.action === "skipped").length,
|
|
764
|
-
backups_created: backupsCreated,
|
|
765
|
-
},
|
|
766
|
-
files: importedFiles,
|
|
767
|
-
manifest,
|
|
768
|
-
warnings,
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
return { ok: true, report };
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// ---------------------------------------------------------------------------
|
|
775
|
-
// Credential extraction
|
|
776
|
-
// ---------------------------------------------------------------------------
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Extract credential entries from a validated vbundle tar entries map.
|
|
780
|
-
*
|
|
781
|
-
* Credentials are stored under the `credentials/` prefix in the archive,
|
|
782
|
-
* where the remainder of the path is the account name and the entry data
|
|
783
|
-
* is the credential value.
|
|
784
|
-
*/
|
|
785
|
-
export function extractCredentialsFromBundle(
|
|
786
|
-
entries: Map<string, VBundleTarEntry>,
|
|
787
|
-
manifest: ManifestType,
|
|
788
|
-
): Array<{ account: string; value: string }> {
|
|
789
|
-
const manifestPaths = new Set(manifest.contents.map((f) => f.path));
|
|
790
|
-
const credentials: Array<{ account: string; value: string }> = [];
|
|
791
|
-
for (const [path, entry] of entries) {
|
|
792
|
-
if (path.startsWith("credentials/") && manifestPaths.has(path)) {
|
|
793
|
-
const account = path.slice("credentials/".length);
|
|
794
|
-
if (account) {
|
|
795
|
-
const value = new TextDecoder().decode(entry.data);
|
|
796
|
-
credentials.push({ account, value });
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
return credentials;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// ---------------------------------------------------------------------------
|
|
804
|
-
// Helpers
|
|
805
|
-
// ---------------------------------------------------------------------------
|
|
806
|
-
|
|
807
|
-
function buildPartialReport(
|
|
808
|
-
files: ImportedFileReport[],
|
|
809
|
-
manifest: ManifestType,
|
|
810
|
-
warnings: string[],
|
|
811
|
-
backupsCreated: number,
|
|
812
|
-
): ImportCommitReport {
|
|
813
|
-
return {
|
|
814
|
-
success: false,
|
|
815
|
-
summary: {
|
|
816
|
-
total_files: files.length,
|
|
817
|
-
files_created: files.filter((f) => f.action === "created").length,
|
|
818
|
-
files_overwritten: files.filter((f) => f.action === "overwritten").length,
|
|
819
|
-
files_skipped: files.filter((f) => f.action === "skipped").length,
|
|
820
|
-
backups_created: backupsCreated,
|
|
821
|
-
},
|
|
822
|
-
files,
|
|
823
|
-
manifest,
|
|
824
|
-
warnings,
|
|
825
|
-
};
|
|
826
|
-
}
|