@ran-sh/dsh-crew 1.0.2 → 1.1.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 (40) hide show
  1. package/README.md +4 -0
  2. package/README.zh.md +4 -0
  3. package/codex/AGENTS.md +101 -92
  4. package/docs/ui-surfaces.md +48 -21
  5. package/lib/client.js +235 -34
  6. package/official-web-bridge/lib/client.js +368 -4532
  7. package/official-web-bridge/package.json +1 -2
  8. package/package.json +130 -67
  9. package/scripts/build-client.mjs +30 -15
  10. package/scripts/remove-legacy-official-bridge.ps1 +89 -0
  11. package/scripts/setup.mjs +152 -140
  12. package/scripts/verify-npm-install.mjs +311 -310
  13. package/scripts/verify-official-bridge-e2e.mjs +12 -1
  14. package/src/client/host-readiness.mjs +62 -59
  15. package/src/client/index.tsx +117 -24
  16. package/src/client/quick-entry.tsx +10 -0
  17. package/src/client/quick-panel.tsx +295 -0
  18. package/src/client/surface-detection.mjs +43 -30
  19. package/src/credential-reference.mjs +38 -0
  20. package/src/dsh-cli-runtime.mjs +500 -40
  21. package/src/dsh-cohort.mjs +20 -0
  22. package/src/hub/index.mjs +1602 -1016
  23. package/src/install/npx-lifecycle.mjs +1983 -468
  24. package/src/install/official-web.mjs +36 -73
  25. package/src/jobs.mjs +20 -24
  26. package/src/model-catalog.mjs +8 -1
  27. package/src/official-web-bridge.mjs +329 -249
  28. package/src/provider-delete-adapters.mjs +165 -17
  29. package/src/provider-inventory.mjs +17 -3
  30. package/src/provider-layer-migration-adapters.mjs +759 -0
  31. package/src/provider-layer-migration.mjs +198 -0
  32. package/src/provider-lifecycle-state.mjs +4 -1
  33. package/src/provider-profile-store.mjs +193 -4
  34. package/src/provider-settings-store.mjs +82 -6
  35. package/src/provider-store-lock.mjs +67 -0
  36. package/src/runtime-identity.mjs +24 -1
  37. package/src/supervisor/restart-request.mjs +202 -0
  38. package/windows/start-dsh-crew.ps1 +783 -370
  39. package/worker.cordis.yml +32 -46
  40. package/zcode/AGENTS.md +35 -26
@@ -0,0 +1,759 @@
1
+ // Filesystem adapter for the explicit provider-layer migration transaction.
2
+ // It stores only safe provider metadata and source revisions; credential values
3
+ // are never parsed, copied, or persisted in migration snapshots.
4
+
5
+ import { createHash, randomUUID } from 'node:crypto';
6
+ import { existsSync, linkSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, readdirSync, statSync, writeFileSync } from 'node:fs';
7
+ import { basename, dirname, join, relative, resolve as resolvePath } from 'node:path';
8
+ import { addProviderDeclaration, hasInlineProviderCredentials as hasInlineProfileCredentials, readProviderDeclarations, readProviderMaterialization, removeProviderDeclarations } from './provider-profile-store.mjs';
9
+ import { addProviderSettings, hasInlineProviderCredentials as hasInlineSettingsCredentials, readProviderSettingsDeclarations, readProviderSettingsMaterialization, removeProviderSettings } from './provider-settings-store.mjs';
10
+ import { classifyCredentialReference } from './credential-reference.mjs';
11
+ import { acquireProviderStoreLock, recoverProviderStoreLock } from './provider-store-lock.mjs';
12
+
13
+ function text(value) { return typeof value === 'string' && value.trim() ? value.trim() : null; }
14
+ function sha256(value) { return createHash('sha256').update(value, 'utf8').digest('hex'); }
15
+ function safeId(value) { return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value) ? value : null; }
16
+ function safeValue(value, max = 2048) { return typeof value === 'string' && value.trim() && value.length <= max && !/[\r\n]/u.test(value) ? value.trim() : null; }
17
+ function safeTimestamp(value) { return safeValue(value, 128) && Number.isFinite(Date.parse(value)); }
18
+ function safeMaterialization(provider) {
19
+ if (!provider || typeof provider !== 'object' || Array.isArray(provider) || !safeId(provider.id)) return null;
20
+ const allowedProviderKeys = new Set(['id', 'display_name', 'credential_ref', 'api', 'base_url', 'models', 'source_file']);
21
+ if (Object.keys(provider).some((key) => !allowedProviderKeys.has(key))) return null;
22
+ const credential = provider.credential_ref ? classifyCredentialReference(provider.credential_ref, { kind: 'env' }).value : null;
23
+ if (provider.credential_ref && !credential) return null;
24
+ const api = safeValue(provider.api, 128);
25
+ if (api && (!/^[A-Za-z][A-Za-z0-9._-]{0,127}$/u.test(api) || /^(?:sk|pk|rk|token|secret)[_-]/iu.test(api))) return null;
26
+ let baseUrl = safeValue(provider.base_url);
27
+ if (baseUrl) {
28
+ try {
29
+ const parsed = new URL(baseUrl);
30
+ if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash || /[?#]/u.test(baseUrl)) return null;
31
+ } catch { return null; }
32
+ }
33
+ const models = (Array.isArray(provider.models) ? provider.models : []).map((model) => {
34
+ if (!model || typeof model !== 'object' || Array.isArray(model)) return null;
35
+ const allowedModelKeys = new Set(['id', 'name', 'context_window', 'max_tokens', 'input', 'reasoning_efforts', 'compat']);
36
+ if (Object.keys(model).some((key) => !allowedModelKeys.has(key))) return null;
37
+ return {
38
+ id: safeValue(model?.id, 256),
39
+ ...(safeValue(model?.name, 256) ? { name: safeValue(model.name, 256) } : {}),
40
+ ...(Number.isSafeInteger(model?.context_window) && model.context_window > 0 ? { context_window: model.context_window } : {}),
41
+ ...(Number.isSafeInteger(model?.max_tokens) && model.max_tokens > 0 ? { max_tokens: model.max_tokens } : {}),
42
+ ...(Array.isArray(model?.input) && model.input.every((value) => value === 'text' || value === 'image') ? { input: [...new Set(model.input)] } : {}),
43
+ ...(model?.reasoning_efforts && typeof model.reasoning_efforts === 'object' && !Array.isArray(model.reasoning_efforts)
44
+ ? { reasoning_efforts: Object.fromEntries(Object.entries(model.reasoning_efforts).filter(([key, value]) => /^[A-Za-z][A-Za-z0-9_-]*$/u.test(key) && (value === null || safeValue(value, 256)))) } : {}),
45
+ ...(model?.compat && typeof model.compat === 'object' && !Array.isArray(model.compat) && Object.keys(model.compat).length === 0 ? { compat: {} } : {}),
46
+ };
47
+ });
48
+ if (models.some((model) => !model?.id)) return null;
49
+ return {
50
+ id: provider.id,
51
+ display_name: safeValue(provider.display_name, 256) ?? provider.id,
52
+ ...(credential ? { credential_ref: credential } : {}),
53
+ ...(api ? { api } : {}),
54
+ ...(baseUrl ? { base_url: baseUrl } : {}),
55
+ models: models.slice(0, 256),
56
+ };
57
+ }
58
+
59
+ function onlyKeys(value, allowed) {
60
+ return value && typeof value === 'object' && !Array.isArray(value)
61
+ && Object.keys(value).every((key) => allowed.includes(key));
62
+ }
63
+
64
+ function sameProviderSemantics(left, right) {
65
+ return JSON.stringify(safeMaterialization(left)) === JSON.stringify(safeMaterialization(right));
66
+ }
67
+ function canonicalMaterialization(value) {
68
+ const safe = safeMaterialization(value);
69
+ return safe && JSON.stringify(safe) === JSON.stringify(value) ? safe : null;
70
+ }
71
+ function safePlan(plan) {
72
+ if (!plan || !safeId(plan.provider_id) || !safeId(plan.plan_id) || !['promote-existing-user', 'materialize-user'].includes(plan.action)) throw Object.assign(new Error('provider migration plan is invalid'), { code: 'PROVIDER_MIGRATION_PLAN_INVALID' });
73
+ const expected = plan.expected_revisions && typeof plan.expected_revisions === 'object' ? plan.expected_revisions : {};
74
+ if (!/^[a-f0-9]{64}$/u.test(expected.profile ?? '')) throw Object.assign(new Error('provider migration profile revision is required'), { code: 'PROVIDER_MIGRATION_PLAN_INVALID' });
75
+ if (expected.settings !== null && !/^[a-f0-9]{64}$/u.test(expected.settings ?? '')) throw Object.assign(new Error('provider migration settings revision is invalid'), { code: 'PROVIDER_MIGRATION_PLAN_INVALID' });
76
+ if (plan.action === 'promote-existing-user' && !/^[a-f0-9]{64}$/u.test(expected.settings ?? '')) throw Object.assign(new Error('provider migration settings revision is required'), { code: 'PROVIDER_MIGRATION_PLAN_INVALID' });
77
+ const revisions = {
78
+ profile: expected.profile,
79
+ settings: expected.settings ?? null,
80
+ };
81
+ const materialization = safeMaterialization(plan.materialization?.provider);
82
+ if (!materialization) throw Object.assign(new Error('provider materialization is invalid'), { code: 'PROVIDER_MATERIALIZATION_INVALID' });
83
+ const userMaterializationBefore = plan.user_materialization_before === undefined
84
+ ? null
85
+ : safeMaterialization(plan.user_materialization_before);
86
+ if (plan.user_materialization_before !== undefined && !userMaterializationBefore) {
87
+ throw Object.assign(new Error('provider user-layer baseline is invalid'), { code: 'PROVIDER_MIGRATION_PLAN_INVALID' });
88
+ }
89
+ if (plan.action === 'promote-existing-user' && !userMaterializationBefore) {
90
+ throw Object.assign(new Error('provider user-layer baseline is required'), { code: 'PROVIDER_MIGRATION_PLAN_INVALID' });
91
+ }
92
+ const routingReferences = Array.isArray(plan.routing_references)
93
+ ? plan.routing_references.map((entry) => ({ provider: plan.provider_id, model: safeValue(entry?.model, 256) })).filter((entry) => entry.model).slice(0, 256)
94
+ : [];
95
+ if (Array.isArray(plan.routing_references) && routingReferences.length !== plan.routing_references.length) {
96
+ throw Object.assign(new Error('provider migration routing references are invalid'), { code: 'PROVIDER_MIGRATION_PLAN_INVALID' });
97
+ }
98
+ return {
99
+ schema_version: 1,
100
+ kind: 'provider-layer-migration',
101
+ plan_id: plan.plan_id,
102
+ provider_id: plan.provider_id,
103
+ action: plan.action,
104
+ expected_revisions: revisions,
105
+ materialization: { provider: materialization },
106
+ ...(userMaterializationBefore ? { user_materialization_before: userMaterializationBefore } : {}),
107
+ ...(routingReferences.length > 0 ? { routing_references: routingReferences } : {}),
108
+ ...(plan.harness_default_before && typeof plan.harness_default_before === 'object' && safeId(plan.harness_default_before.provider) && safeValue(plan.harness_default_before.model, 256)
109
+ ? { harness_default_before: { provider: plan.harness_default_before.provider, model: safeValue(plan.harness_default_before.model, 256) } } : {}),
110
+ ...(safeValue(plan.runtime_id_before, 128) ? { runtime_id_before: safeValue(plan.runtime_id_before, 128) } : {}),
111
+ };
112
+ }
113
+
114
+ function assertManagedPath(file, root) {
115
+ const resolvedRoot = resolvePath(root);
116
+ const resolved = resolvePath(file);
117
+ try {
118
+ if (lstatSync(resolvedRoot).isSymbolicLink()) throw Object.assign(new Error('provider migration root uses a link'), { code: 'PROVIDER_MIGRATION_UNSAFE_PATH' });
119
+ } catch (error) {
120
+ if (error?.code !== 'ENOENT') throw error;
121
+ }
122
+ const rel = relative(resolvedRoot, resolved);
123
+ if (rel.startsWith('..') || rel === '..' || /^[A-Za-z]:/u.test(rel)) {
124
+ throw Object.assign(new Error('provider migration path escapes managed root'), { code: 'PROVIDER_MIGRATION_UNSAFE_PATH' });
125
+ }
126
+ let cursor = resolvedRoot;
127
+ for (const segment of rel.split(/[\\/]/u).filter(Boolean)) {
128
+ cursor = join(cursor, segment);
129
+ try {
130
+ if (lstatSync(cursor).isSymbolicLink()) throw Object.assign(new Error('provider migration path uses a link'), { code: 'PROVIDER_MIGRATION_UNSAFE_PATH' });
131
+ } catch (error) {
132
+ if (error?.code === 'ENOENT') break;
133
+ throw error;
134
+ }
135
+ }
136
+ }
137
+
138
+ function atomicReplace(file, content, root) {
139
+ assertManagedPath(file, root);
140
+ const temp = `${file}.${process.pid}.${randomUUID()}.tmp`;
141
+ assertManagedPath(temp, root);
142
+ writeFileSync(temp, content, 'utf8');
143
+ try {
144
+ renameSync(temp, file);
145
+ } catch (error) {
146
+ throw Object.assign(new Error('provider migration atomic replace failed'), { code: 'PROVIDER_MIGRATION_REPLACE_FAILED', cause: error });
147
+ } finally { rmSync(temp, { force: true }); }
148
+ }
149
+
150
+ function readText(file) {
151
+ try { return existsSync(file) ? readFileSync(file, 'utf8') : null; } catch (error) {
152
+ throw Object.assign(new Error('provider migration source is unavailable'), { code: 'PROVIDER_MIGRATION_SOURCE_UNAVAILABLE', cause: error });
153
+ }
154
+ }
155
+
156
+ function revision(source) { return source === null ? null : sha256(source); }
157
+ function fileIdentity(file) {
158
+ const stat = lstatSync(file, { bigint: true });
159
+ if (stat.isSymbolicLink() || !stat.isFile()) throw Object.assign(new Error('provider migration file identity is unsafe'), { code: 'PROVIDER_MIGRATION_UNSAFE_PATH' });
160
+ return { dev: String(stat.dev), ino: String(stat.ino), size: Number(stat.size) || 0, mtimeMs: Number(stat.mtimeMs) || 0 };
161
+ }
162
+ function sameFileIdentity(left, right) {
163
+ return left && right && left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs;
164
+ }
165
+ function validFileIdentity(value) {
166
+ return value && typeof value === 'object' && typeof value.dev === 'string' && /^\d+$/u.test(value.dev)
167
+ && typeof value.ino === 'string' && /^\d+$/u.test(value.ino)
168
+ && Number.isFinite(value.size) && Number.isFinite(value.mtimeMs);
169
+ }
170
+ function validWitnessPath(root, value) {
171
+ if (typeof value !== 'string') return false;
172
+ const relativePath = relative(resolvePath(root), resolvePath(value));
173
+ if (relativePath === '..' || relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || /^[A-Za-z]:/u.test(relativePath)) return false;
174
+ if (relativePath.includes('\\') || relativePath.includes('/') || !/^settings\.[0-9a-f-]{36}\.ownership\.witness$/iu.test(basename(value))) return false;
175
+ return true;
176
+ }
177
+ function validWitness(root, value) {
178
+ if (!validWitnessPath(root, value)) return false;
179
+ try { const stat = lstatSync(value); return stat.isFile() && !stat.isSymbolicLink(); } catch { return false; }
180
+ }
181
+
182
+ function publishHardlink(source, target, managedRoot) {
183
+ assertManagedPath(source, managedRoot);
184
+ assertManagedPath(dirname(source), managedRoot);
185
+ assertManagedPath(target, managedRoot);
186
+ assertManagedPath(dirname(target), managedRoot);
187
+ linkSync(source, target);
188
+ assertManagedPath(target, managedRoot);
189
+ }
190
+
191
+ function manifestDigest(manifest) {
192
+ const { checksum: _checksum, ...withoutChecksum } = manifest ?? {};
193
+ return sha256(JSON.stringify(withoutChecksum));
194
+ }
195
+
196
+ function validMigrationManifest(root, manifest) {
197
+ if (!manifest || manifest.schema_version !== 1 || manifest.kind !== 'provider-layer-migration' || !safeId(manifest.plan_id) || !safeId(manifest.provider_id)) return false;
198
+ if (!onlyKeys(manifest, ['schema_version', 'kind', 'plan_id', 'provider_id', 'plan', 'phase', 'files', 'applied_revisions', 'rollback_revisions', 'mutation_journal', 'created_at', 'created_settings_identity', 'created_settings_witness', 'checksum'])) return false;
199
+ if (!['PREPARED', 'USER_MATERIALIZED', 'BASE_REMOVED', 'RESTART_PENDING', 'ROLLBACK_APPLIED', 'ROLLBACK_RESTART_PENDING', 'ROLLBACK_APPLYING', 'VERIFIED', 'ROLLED_BACK'].includes(manifest.phase)) return false;
200
+ try { if (!lstatSync(root).isDirectory()) return false; } catch { return false; }
201
+ if (!manifest.plan || !onlyKeys(manifest.plan, ['schema_version', 'kind', 'plan_id', 'provider_id', 'action', 'expected_revisions', 'materialization', 'user_materialization_before', 'harness_default_before', 'runtime_id_before', 'runtime_id_before_restart', 'rollback_runtime_id_before', 'routing_references'])
202
+ || manifest.plan.schema_version !== 1 || manifest.plan.kind !== 'provider-layer-migration'
203
+ || manifest.plan.plan_id !== manifest.plan_id || manifest.plan.provider_id !== manifest.provider_id || !['promote-existing-user', 'materialize-user'].includes(manifest.plan.action)
204
+ || !onlyKeys(manifest.plan.materialization, ['provider']) || !canonicalMaterialization(manifest.plan.materialization?.provider)
205
+ || (manifest.plan.action === 'promote-existing-user' && !canonicalMaterialization(manifest.plan.user_materialization_before))
206
+ || (manifest.plan.user_materialization_before !== undefined && !canonicalMaterialization(manifest.plan.user_materialization_before))) return false;
207
+ const expected = manifest.plan.expected_revisions;
208
+ if (!onlyKeys(expected, ['profile', 'settings']) || !/^[a-f0-9]{64}$/u.test(expected.profile ?? '') || (expected.settings !== null && !/^[a-f0-9]{64}$/u.test(expected.settings ?? ''))) return false;
209
+ if (manifest.plan.harness_default_before !== undefined && (!onlyKeys(manifest.plan.harness_default_before, ['provider', 'model']) || !safeId(manifest.plan.harness_default_before.provider) || !safeValue(manifest.plan.harness_default_before.model, 256))) return false;
210
+ if (manifest.plan.runtime_id_before !== undefined && !safeValue(manifest.plan.runtime_id_before, 128)) return false;
211
+ if (manifest.plan.runtime_id_before_restart !== undefined && !safeValue(manifest.plan.runtime_id_before_restart, 128)) return false;
212
+ if (manifest.plan.rollback_runtime_id_before !== undefined && !safeValue(manifest.plan.rollback_runtime_id_before, 128)) return false;
213
+ if (manifest.plan.routing_references !== undefined && (!Array.isArray(manifest.plan.routing_references) || manifest.plan.routing_references.length > 256 || manifest.plan.routing_references.some((entry) => !onlyKeys(entry, ['provider', 'model']) || entry.provider !== manifest.provider_id || !safeValue(entry.model, 256)))) return false;
214
+ if (!onlyKeys(manifest.mutation_journal, ['profile', 'settings'])) return false;
215
+ if (['VERIFIED', 'ROLLED_BACK'].includes(manifest.phase) && Object.keys(manifest.mutation_journal).length > 0) return false;
216
+ if (!manifest.applied_revisions || typeof manifest.applied_revisions !== 'object' || Array.isArray(manifest.applied_revisions)
217
+ || Object.keys(manifest.applied_revisions).some((key) => !['profile', 'settings'].includes(key))
218
+ || Object.values(manifest.applied_revisions).some((value) => !/^[a-f0-9]{64}$/u.test(value ?? ''))) return false;
219
+ if (manifest.rollback_revisions !== undefined && (!manifest.rollback_revisions || typeof manifest.rollback_revisions !== 'object' || Array.isArray(manifest.rollback_revisions)
220
+ || Object.keys(manifest.rollback_revisions).some((key) => !['profile', 'settings'].includes(key))
221
+ || Object.values(manifest.rollback_revisions).some((value) => value !== null && !/^[a-f0-9]{64}$/u.test(value ?? '')))) return false;
222
+ if (Object.entries(manifest.mutation_journal).some(([key, entry]) => !['profile', 'settings'].includes(key)
223
+ || !onlyKeys(entry, ['next_revision', 'previous_revision', 'created', 'witness', 'phase', 'direction'])
224
+ || !entry || typeof entry !== 'object' || entry.phase !== 'WRITE_PENDING'
225
+ || (entry.next_revision !== null && !/^[a-f0-9]{64}$/u.test(entry.next_revision ?? ''))
226
+ || (entry.previous_revision !== null && !/^[a-f0-9]{64}$/u.test(entry.previous_revision ?? ''))
227
+ || typeof entry.created !== 'boolean'
228
+ || (entry.direction !== undefined && !['forward', 'rollback'].includes(entry.direction))
229
+ || (entry.witness !== undefined && !validWitnessPath(root, entry.witness))
230
+ || (entry.witness !== undefined && !validWitness(root, entry.witness)
231
+ && !(entry.direction === 'rollback' && entry.next_revision === null && ['ROLLBACK_APPLYING', 'ROLLBACK_APPLIED', 'ROLLBACK_RESTART_PENDING'].includes(manifest.phase))))) return false;
232
+ if (!onlyKeys(manifest.files, ['profile', 'settings']) || manifest.files?.settings?.existed === false && ['USER_MATERIALIZED', 'BASE_REMOVED', 'RESTART_PENDING', 'VERIFIED'].includes(manifest.phase)
233
+ && (!validFileIdentity(manifest.created_settings_identity) || !validWitness(root, manifest.created_settings_witness))) return false;
234
+ if (manifest.created_settings_identity !== undefined && !onlyKeys(manifest.created_settings_identity, ['dev', 'ino', 'size', 'mtimeMs'])) return false;
235
+ if (manifest.created_settings_witness !== undefined && !validWitnessPath(root, manifest.created_settings_witness)) return false;
236
+ if (manifest.created_settings_witness !== undefined && !validWitness(root, manifest.created_settings_witness)
237
+ && !['ROLLBACK_APPLYING', 'ROLLBACK_APPLIED', 'ROLLBACK_RESTART_PENDING'].includes(manifest.phase)) return false;
238
+ if (!safeTimestamp(manifest.created_at)) return false;
239
+ if (typeof manifest.checksum !== 'string' || manifest.checksum !== manifestDigest(manifest)) return false;
240
+ for (const key of ['profile', 'settings']) {
241
+ const entry = manifest.files?.[key];
242
+ if (!onlyKeys(entry, ['existed', 'revision']) || !entry || typeof entry.existed !== 'boolean' || (key === 'profile' && entry.existed !== true) || (!entry.existed && entry.revision !== null) || entry.revision !== expected[key] || (entry.revision !== null && !/^[a-f0-9]{64}$/u.test(entry.revision))) return false;
243
+ if (entry.backup_digest !== undefined || existsSync(join(root, `${key}.backup`))) return false;
244
+ }
245
+ return true;
246
+ }
247
+
248
+ function quarantineUnderRoot(root, actionId, managedRoot) {
249
+ assertManagedPath(root, managedRoot);
250
+ if (typeof actionId !== 'string' || !/^[a-f0-9]{32}$/u.test(actionId)) throw Object.assign(new Error('provider migration action id is invalid'), { code: 'PROVIDER_MIGRATION_PLAN_INVALID' });
251
+ if (!existsSync(root)) throw Object.assign(new Error('provider migration transaction was not found'), { code: 'PROVIDER_MIGRATION_RECOVERY_NOT_FOUND' });
252
+ const entry = readdirSync(root, { withFileTypes: true }).find((candidate) => candidate.name !== '.quarantine' && sha256(candidate.name).slice(0, 32) === actionId);
253
+ if (!entry) throw Object.assign(new Error('provider migration transaction was not found'), { code: 'PROVIDER_MIGRATION_RECOVERY_NOT_FOUND' });
254
+ const source = join(root, entry.name);
255
+ const quarantineRoot = join(root, '.quarantine');
256
+ const sourceStat = lstatSync(source);
257
+ if (!sourceStat.isSymbolicLink()) assertManagedPath(source, managedRoot);
258
+ assertManagedPath(quarantineRoot, managedRoot);
259
+ mkdirSync(quarantineRoot, { recursive: true });
260
+ assertManagedPath(quarantineRoot, managedRoot);
261
+ const target = join(quarantineRoot, `${actionId}.${Date.now()}.${randomUUID().slice(0, 8)}`);
262
+ assertManagedPath(target, managedRoot);
263
+ renameSync(source, target);
264
+ return { ok: true, storage_id: entry.name, action_id: actionId, state: 'QUARANTINED' };
265
+ }
266
+
267
+ export function createProviderLayerMigrationFileHooks({
268
+ profileFile,
269
+ settingsFile,
270
+ backupDir,
271
+ existingMigrationId = null,
272
+ restart = null,
273
+ sharedLockFile = null,
274
+ afterRollbackDeleteJournaled = null,
275
+ afterRollbackFileRemoved = null,
276
+ afterRollbackWitnessRemoved = null,
277
+ } = {}) {
278
+ if (![profileFile, settingsFile, backupDir].every((value) => typeof value === 'string' && value.trim())) throw new TypeError('provider migration paths are required');
279
+ const managedRoot = dirname(resolvePath(backupDir));
280
+ for (const file of [profileFile, settingsFile, backupDir]) assertManagedPath(file, managedRoot);
281
+ mkdirSync(backupDir, { recursive: true });
282
+ const lockFile = join(backupDir, '.migration.lock');
283
+ let lockOwned = false;
284
+ let lockToken = null;
285
+ let sharedStoreLock = null;
286
+ let active = null;
287
+ const ensureLock = () => {
288
+ if (!lockOwned || !lockToken) throw Object.assign(new Error('provider migration lock is unavailable'), { code: 'PROVIDER_MIGRATION_LOCK_UNAVAILABLE' });
289
+ try {
290
+ const owner = JSON.parse(readFileSync(lockFile, 'utf8'));
291
+ if (owner?.token !== lockToken) throw new Error('provider migration lock ownership changed');
292
+ } catch (error) {
293
+ lockOwned = false;
294
+ lockToken = null;
295
+ throw Object.assign(new Error('provider migration lock ownership changed'), { code: 'PROVIDER_MIGRATION_LOCK_UNAVAILABLE', cause: error });
296
+ }
297
+ };
298
+ const writeLock = () => {
299
+ const token = randomUUID();
300
+ writeFileSync(lockFile, JSON.stringify({ pid: process.pid, token, created_at: new Date().toISOString() }) + '\n', { flag: 'wx' });
301
+ lockToken = token;
302
+ lockOwned = true;
303
+ };
304
+
305
+ const acquireLock = async () => {
306
+ if (lockOwned) return { ok: true };
307
+ sharedStoreLock = await acquireProviderStoreLock(sharedLockFile ?? join(managedRoot, 'provider-store.lock'));
308
+ try {
309
+ writeLock();
310
+ return { ok: true };
311
+ } catch (error) {
312
+ if (error?.code === 'EEXIST') {
313
+ const reclaimGuard = `${lockFile}.reclaim`;
314
+ let guardOwned = false;
315
+ try {
316
+ mkdirSync(reclaimGuard, { recursive: false });
317
+ guardOwned = true;
318
+ const owner = JSON.parse(readFileSync(lockFile, 'utf8'));
319
+ if (!owner || typeof owner !== 'object' || Array.isArray(owner) || !Number.isInteger(owner.pid) || owner.pid <= 0 || !safeValue(owner.token, 128)) {
320
+ throw Object.assign(new Error('provider migration lock metadata is invalid'), { code: 'PROVIDER_MIGRATION_BUSY' });
321
+ }
322
+ let alive = false;
323
+ if (Number.isInteger(owner?.pid) && owner.pid > 0) {
324
+ try { process.kill(owner.pid, 0); alive = true; } catch (probeError) { alive = probeError?.code === 'EPERM'; }
325
+ }
326
+ if (!alive) {
327
+ rmSync(lockFile, { force: true });
328
+ writeLock();
329
+ return { ok: true, recovered: true };
330
+ }
331
+ } catch (reclaimError) {
332
+ if (reclaimError?.code === 'PROVIDER_MIGRATION_BUSY') throw reclaimError;
333
+ } finally { if (guardOwned) rmSync(reclaimGuard, { recursive: true, force: true }); }
334
+ throw Object.assign(new Error('provider migration is busy'), { code: 'PROVIDER_MIGRATION_BUSY' });
335
+ }
336
+ throw Object.assign(new Error('provider migration lock is unavailable'), { code: 'PROVIDER_MIGRATION_LOCK_UNAVAILABLE' });
337
+ } finally {
338
+ if (!lockOwned && sharedStoreLock) { await sharedStoreLock.release(); sharedStoreLock = null; }
339
+ }
340
+ };
341
+ const release = async () => {
342
+ if (!lockOwned) return;
343
+ let owned = true;
344
+ try { ensureLock(); } catch { owned = false; /* a replacement owner will clean up its own lock */ }
345
+ if (owned) rmSync(lockFile, { force: true });
346
+ lockOwned = false;
347
+ lockToken = null;
348
+ if (sharedStoreLock) { await sharedStoreLock.release(); sharedStoreLock = null; }
349
+ };
350
+ const recoverLock = async ({ confirm = false } = {}) => {
351
+ const shared = await recoverProviderStoreLock(sharedLockFile ?? join(managedRoot, 'provider-store.lock'), { confirm });
352
+ if (shared.ok !== true || confirm !== true) return shared;
353
+ if (!existsSync(lockFile)) return shared;
354
+ try {
355
+ const owner = JSON.parse(readFileSync(lockFile, 'utf8'));
356
+ if (owner && typeof owner === 'object' && !Array.isArray(owner) && Number.isInteger(owner.pid) && owner.pid > 0 && safeValue(owner.token, 128)) {
357
+ try { process.kill(owner.pid, 0); return { ok: false, code: 'PROVIDER_MIGRATION_BUSY' }; } catch (error) { if (error?.code === 'EPERM') return { ok: false, code: 'PROVIDER_MIGRATION_BUSY' }; }
358
+ }
359
+ } catch {}
360
+ rmSync(lockFile, { force: true });
361
+ return { ...shared, recovered: true };
362
+ };
363
+ const manifestFile = (root) => join(root, 'manifest.json');
364
+ const persist = () => {
365
+ if (!active) throw Object.assign(new Error('provider migration backup is unavailable'), { code: 'PROVIDER_MIGRATION_BACKUP_INVALID' });
366
+ ensureLock();
367
+ active.manifest.checksum = manifestDigest(active.manifest);
368
+ atomicReplace(manifestFile(active.root), JSON.stringify(active.manifest, null, 2) + '\n', managedRoot);
369
+ };
370
+ const load = (migrationId) => {
371
+ const id = safeId(migrationId);
372
+ if (!id) throw Object.assign(new Error('provider migration id is invalid'), { code: 'PROVIDER_MIGRATION_PLAN_INVALID' });
373
+ const root = join(backupDir, id);
374
+ assertManagedPath(root, managedRoot);
375
+ const file = manifestFile(root);
376
+ try { if (lstatSync(file).isSymbolicLink() || !lstatSync(file).isFile()) throw new Error('invalid migration manifest path'); } catch (error) {
377
+ throw Object.assign(new Error('provider migration manifest is invalid'), { code: 'PROVIDER_MIGRATION_BACKUP_INVALID', cause: error });
378
+ }
379
+ const manifest = JSON.parse(readFileSync(file, 'utf8'));
380
+ if (manifest?.plan_id !== id || !validMigrationManifest(root, manifest)) throw Object.assign(new Error('provider migration manifest is invalid'), { code: 'PROVIDER_MIGRATION_BACKUP_INVALID' });
381
+ active = { root, manifest };
382
+ return active;
383
+ };
384
+
385
+ const backup = async (plan) => {
386
+ await acquireLock();
387
+ if (existingMigrationId) { load(existingMigrationId); return { ok: true, migration_id: existingMigrationId }; }
388
+ const safe = safePlan(plan);
389
+ const root = join(backupDir, safe.plan_id);
390
+ assertManagedPath(root, managedRoot);
391
+ if (existsSync(root)) throw Object.assign(new Error('provider migration transaction already exists'), { code: 'PROVIDER_MIGRATION_EXISTS' });
392
+ mkdirSync(root, { recursive: true });
393
+ const profile = readText(profileFile);
394
+ const settings = readText(settingsFile);
395
+ const expected = safe.expected_revisions;
396
+ if (revision(profile) !== expected.profile || revision(settings) !== expected.settings) {
397
+ rmSync(root, { recursive: true, force: true });
398
+ throw Object.assign(new Error('provider migration source changed before backup'), { code: 'PROVIDER_MIGRATION_SOURCE_CHANGED' });
399
+ }
400
+ // Migration snapshots are structured metadata only. Reject known inline
401
+ // credential shapes in either source before touching it, while never
402
+ // persisting a complete source file (including unrelated custom fields).
403
+ const profileCheck = hasInlineProfileCredentials(profile ?? '');
404
+ if (profileCheck.ok !== true || profileCheck.inline === true) {
405
+ rmSync(root, { recursive: true, force: true });
406
+ throw Object.assign(new Error('provider profile credential shape is unsupported'), { code: 'PROVIDER_INLINE_CREDENTIAL_UNSUPPORTED' });
407
+ }
408
+ if (settings !== null) {
409
+ const settingsCheck = hasInlineSettingsCredentials(settings);
410
+ if (settingsCheck.ok !== true || settingsCheck.inline === true) {
411
+ rmSync(root, { recursive: true, force: true });
412
+ throw Object.assign(new Error('provider settings credential shape is unsupported'), { code: 'PROVIDER_INLINE_CREDENTIAL_UNSUPPORTED' });
413
+ }
414
+ }
415
+ const manifest = {
416
+ schema_version: 1,
417
+ kind: 'provider-layer-migration',
418
+ plan_id: safe.plan_id,
419
+ provider_id: safe.provider_id,
420
+ plan: safe,
421
+ phase: 'PREPARED',
422
+ files: {
423
+ profile: { existed: profile !== null, revision: revision(profile) },
424
+ settings: { existed: settings !== null, revision: revision(settings) },
425
+ },
426
+ applied_revisions: {},
427
+ mutation_journal: {},
428
+ created_at: new Date().toISOString(),
429
+ };
430
+ active = { root, manifest };
431
+ persist();
432
+ return { ok: true, migration_id: safe.plan_id };
433
+ };
434
+
435
+ const assertExpected = (file, expected, code) => {
436
+ const current = readText(file);
437
+ if (expected !== undefined && revision(current) !== expected) throw Object.assign(new Error('provider migration source changed'), { code });
438
+ return current;
439
+ };
440
+ const writeApplied = (key, file, content) => {
441
+ ensureLock();
442
+ const nextRevision = revision(content);
443
+ const previousRevision = active.manifest.files[key]?.revision ?? null;
444
+ let witness = null;
445
+ if (key === 'settings' && active.manifest.files.settings.existed === false) {
446
+ witness = join(active.root, `settings.${randomUUID()}.ownership.witness`);
447
+ assertManagedPath(witness, managedRoot);
448
+ writeFileSync(witness, content, { flag: 'wx' });
449
+ }
450
+ active.manifest.mutation_journal[key] = {
451
+ next_revision: nextRevision,
452
+ previous_revision: previousRevision,
453
+ created: active.manifest.files[key]?.existed === false,
454
+ ...(witness ? { witness } : {}),
455
+ phase: 'WRITE_PENDING',
456
+ };
457
+ persist();
458
+ if (witness) {
459
+ try { publishHardlink(witness, file, managedRoot); }
460
+ catch (error) { throw Object.assign(new Error('provider migration exclusive create failed'), { code: 'PROVIDER_MIGRATION_REPLACE_FAILED', cause: error }); }
461
+ // The witness is created inside the managed root, but the destination
462
+ // can still be swapped between planning and publication. Re-check the
463
+ // complete destination path immediately before and after the hardlink.
464
+ assertManagedPath(file, managedRoot);
465
+ fileIdentity(file);
466
+ } else atomicReplace(file, content, managedRoot);
467
+ active.manifest.applied_revisions[key] = nextRevision;
468
+ if (key === 'settings' && active.manifest.files.settings.existed === false) {
469
+ active.manifest.created_settings_identity = fileIdentity(file);
470
+ active.manifest.created_settings_witness = witness;
471
+ }
472
+ active.manifest.phase = key === 'settings' ? 'USER_MATERIALIZED' : 'BASE_REMOVED';
473
+ delete active.manifest.mutation_journal[key];
474
+ persist();
475
+ };
476
+
477
+ const writeRollbackApplied = (key, file, content) => {
478
+ ensureLock();
479
+ const nextRevision = revision(content);
480
+ active.manifest.mutation_journal[key] = {
481
+ next_revision: nextRevision,
482
+ previous_revision: active.manifest.applied_revisions[key] ?? active.manifest.files[key]?.revision ?? null,
483
+ created: false,
484
+ direction: 'rollback',
485
+ phase: 'WRITE_PENDING',
486
+ };
487
+ persist();
488
+ atomicReplace(file, content, managedRoot);
489
+ active.manifest.rollback_revisions ??= {};
490
+ active.manifest.rollback_revisions[key] = nextRevision;
491
+ delete active.manifest.mutation_journal[key];
492
+ active.manifest.phase = 'ROLLBACK_APPLYING';
493
+ persist();
494
+ };
495
+
496
+ const writeRollbackDeleted = (key, file, witness = null) => {
497
+ ensureLock();
498
+ if (witness && !validWitnessPath(active.root, witness)) throw Object.assign(new Error('provider migration witness path is unsafe'), { code: 'PROVIDER_MIGRATION_UNSAFE_PATH' });
499
+ active.manifest.mutation_journal[key] = {
500
+ next_revision: null,
501
+ previous_revision: active.manifest.applied_revisions[key] ?? active.manifest.files[key]?.revision ?? null,
502
+ created: false,
503
+ direction: 'rollback',
504
+ ...(witness ? { witness } : {}),
505
+ phase: 'WRITE_PENDING',
506
+ };
507
+ persist();
508
+ if (typeof afterRollbackDeleteJournaled === 'function') afterRollbackDeleteJournaled({ key, file, witness });
509
+ rmSync(file, { force: true });
510
+ if (typeof afterRollbackFileRemoved === 'function') afterRollbackFileRemoved({ key, file, witness });
511
+ active.manifest.rollback_revisions ??= {};
512
+ active.manifest.rollback_revisions[key] = null;
513
+ active.manifest.phase = 'ROLLBACK_APPLYING';
514
+ // Checkpoint the successful unlink while the witness is still durable.
515
+ // A crash after this point can reopen the rollback journal and finish
516
+ // witness cleanup without losing ownership evidence.
517
+ persist();
518
+ if (witness) rmSync(witness, { force: true });
519
+ if (typeof afterRollbackWitnessRemoved === 'function') afterRollbackWitnessRemoved({ key, file, witness });
520
+ if (witness && active.manifest.created_settings_witness === witness) delete active.manifest.created_settings_witness;
521
+ delete active.manifest.mutation_journal[key];
522
+ persist();
523
+ };
524
+
525
+ const materialize = async (plan) => {
526
+ if (!active) throw Object.assign(new Error('provider migration backup is unavailable'), { code: 'PROVIDER_MIGRATION_BACKUP_INVALID' });
527
+ if (plan.action !== 'materialize-user') return { ok: true, changed: false };
528
+ const expected = active.manifest.files.settings.revision;
529
+ const current = assertExpected(settingsFile, expected, 'PROVIDER_SETTINGS_CHANGED');
530
+ const source = current ?? 'llm-pi-ai:\n providers: {}\n';
531
+ const result = addProviderSettings(source, { provider: plan.materialization?.provider, expectedRevision: revision(source) });
532
+ if (!result.ok) throw Object.assign(new Error('provider settings materialization failed'), { code: result.code });
533
+ writeApplied('settings', settingsFile, result.text);
534
+ return { ok: true, changed: true, revision: result.revision };
535
+ };
536
+
537
+ const removeBase = async (plan) => {
538
+ if (!active) throw Object.assign(new Error('provider migration backup is unavailable'), { code: 'PROVIDER_MIGRATION_BACKUP_INVALID' });
539
+ const settingsExpected = active.manifest.plan.action === 'materialize-user'
540
+ ? active.manifest.applied_revisions.settings
541
+ : active.manifest.files.settings.revision;
542
+ if (settingsExpected !== undefined && revision(readText(settingsFile)) !== settingsExpected) throw Object.assign(new Error('provider settings changed before base removal'), { code: 'PROVIDER_SETTINGS_CHANGED' });
543
+ const expected = active.manifest.files.profile.revision;
544
+ const current = assertExpected(profileFile, expected, 'PROVIDER_PROFILE_CHANGED');
545
+ if (current === null) throw Object.assign(new Error('provider profile source is missing'), { code: 'PROVIDER_PROFILE_CHANGED' });
546
+ const result = removeProviderDeclarations(current, { providerIds: [plan.provider_id], expectedRevision: expected });
547
+ if (!result.ok) throw Object.assign(new Error('provider profile migration failed'), { code: result.code });
548
+ writeApplied('profile', profileFile, result.text);
549
+ return { ok: true, changed: true, revision: result.revision };
550
+ };
551
+
552
+ const rollback = async () => {
553
+ if (!active) throw Object.assign(new Error('provider migration backup is unavailable'), { code: 'PROVIDER_MIGRATION_BACKUP_INVALID' });
554
+ if (['ROLLBACK_APPLIED', 'ROLLBACK_RESTART_PENDING', 'ROLLED_BACK'].includes(active.manifest.phase)) return { ok: true, state: active.manifest.phase };
555
+ const cleanupWitnesses = new Set();
556
+ for (const [key, file] of [['profile', profileFile], ['settings', settingsFile]]) {
557
+ ensureLock();
558
+ const snapshot = active.manifest.files[key];
559
+ const current = readText(file);
560
+ const currentRevision = revision(current);
561
+ const pending = active.manifest.mutation_journal?.[key];
562
+ const pendingApplied = currentRevision !== null && currentRevision === pending?.next_revision;
563
+ const rollbackRevision = active.manifest.rollback_revisions?.[key];
564
+ const rollbackReplay = active.manifest.phase?.startsWith('ROLLBACK') === true || pending?.direction === 'rollback';
565
+ const semanticComplete = key === 'profile'
566
+ ? (() => {
567
+ const parsed = current === null ? { ok: false } : readProviderMaterialization(current, { providerId: active.manifest.provider_id, file: 'harness/profiles/dsh-crew/cordis.patch.yml' });
568
+ return parsed.ok === true && sameProviderSemantics(parsed.provider, active.manifest.plan.materialization?.provider);
569
+ })()
570
+ : active.manifest.plan.action === 'materialize-user'
571
+ ? (() => { const parsed = current === null ? { ok: false } : readProviderSettingsDeclarations(current, { file: 'harness/settings.yaml' }); return parsed.ok === true && !parsed.declarations.some((entry) => entry.id === active.manifest.provider_id); })()
572
+ : (() => { const material = current === null ? { ok: false } : readProviderSettingsMaterialization(current, { providerId: active.manifest.provider_id, file: 'harness/settings.yaml' }); return material.ok === true && sameProviderSemantics(material.provider, active.manifest.plan.user_materialization_before); })();
573
+ const alreadyRolledBack = (rollbackRevision === null && rollbackRevision !== undefined ? current === null : currentRevision === rollbackRevision)
574
+ || (rollbackReplay && semanticComplete);
575
+ // For an originally absent settings file, a matching digest is not
576
+ // ownership evidence: a different actor can recreate the same bytes on
577
+ // a different inode after a crash. Require the durable witness/identity
578
+ // path below instead of allowing the hash-only pending journal fallback.
579
+ const pendingWitnessPath = key === 'settings' && pending?.witness ? pending.witness : null;
580
+ if (pendingWitnessPath) cleanupWitnesses.add(pendingWitnessPath);
581
+ const pendingWitnessIdentity = pendingWitnessPath && validWitness(active.root, pendingWitnessPath)
582
+ ? fileIdentity(pending.witness) : null;
583
+ const ownershipSafePending = key !== 'settings' || snapshot.existed !== false || (pendingApplied
584
+ && (sameFileIdentity(fileIdentity(file), active.manifest.created_settings_identity)
585
+ || sameFileIdentity(fileIdentity(file), pendingWitnessIdentity)));
586
+ const allowed = alreadyRolledBack || currentRevision === snapshot.revision || currentRevision === active.manifest.applied_revisions[key] || (pendingApplied && ownershipSafePending);
587
+ if (!allowed) throw Object.assign(new Error('provider migration state changed during rollback'), { code: 'PROVIDER_MIGRATION_STATE_CHANGED' });
588
+ if (key === 'profile') {
589
+ if (!snapshot.existed) throw Object.assign(new Error('provider profile snapshot is unavailable'), { code: 'PROVIDER_MIGRATION_BACKUP_INVALID' });
590
+ if (!alreadyRolledBack && currentRevision !== snapshot.revision) {
591
+ const restored = addProviderDeclaration(current, { provider: active.manifest.plan.materialization?.provider, expectedRevision: currentRevision });
592
+ if (!restored.ok) throw Object.assign(new Error('provider migration profile restore failed'), { code: restored.code });
593
+ writeRollbackApplied('profile', file, restored.text);
594
+ }
595
+ } else if (active.manifest.plan.action === 'materialize-user') {
596
+ if (snapshot.existed) {
597
+ const restored = readProviderSettingsDeclarations(current ?? '', { file: 'harness/settings.yaml' });
598
+ if (!restored.ok) throw Object.assign(new Error('provider migration settings restore failed'), { code: 'PROVIDER_MIGRATION_BACKUP_INVALID' });
599
+ const target = restored.declarations.some((entry) => entry.id === active.manifest.provider_id) ? current : null;
600
+ if (target === null) throw Object.assign(new Error('provider migration settings target is missing'), { code: 'PROVIDER_MIGRATION_STATE_CHANGED' });
601
+ // Remove only the provider materialized by this transaction; other
602
+ // settings (including user fields) remain byte-for-byte untouched.
603
+ const removed = removeProviderSettings(current, { providerIds: [active.manifest.provider_id], expectedRevision: currentRevision });
604
+ if (!removed.ok) throw Object.assign(new Error('provider migration settings restore failed'), { code: removed.code });
605
+ writeRollbackApplied('settings', file, removed.text);
606
+ } else if (current !== null) {
607
+ const witnessPath = active.manifest.created_settings_witness ?? pending?.witness;
608
+ if (witnessPath) cleanupWitnesses.add(witnessPath);
609
+ const witnessIdentity = witnessPath && validWitness(active.root, witnessPath) ? fileIdentity(witnessPath) : null;
610
+ const ownedByIdentity = sameFileIdentity(fileIdentity(file), active.manifest.created_settings_identity) || sameFileIdentity(fileIdentity(file), witnessIdentity);
611
+ if (!ownedByIdentity) throw Object.assign(new Error('provider migration settings ownership changed'), { code: 'PROVIDER_MIGRATION_STATE_CHANGED' });
612
+ writeRollbackDeleted('settings', file, witnessPath);
613
+ }
614
+ }
615
+ }
616
+ if (cleanupWitnesses.size > 0 && active.manifest.phase !== 'ROLLBACK_APPLYING') {
617
+ active.manifest.phase = 'ROLLBACK_APPLYING';
618
+ persist();
619
+ }
620
+ for (const witness of cleanupWitnesses) {
621
+ if (validWitness(active.root, witness)) rmSync(witness, { force: true });
622
+ if (active.manifest.created_settings_witness === witness) delete active.manifest.created_settings_witness;
623
+ }
624
+ active.manifest.mutation_journal = {};
625
+ active.manifest.phase = 'ROLLBACK_APPLIED';
626
+ if (active.manifest.created_settings_witness) {
627
+ if (!validWitnessPath(active.root, active.manifest.created_settings_witness)) throw Object.assign(new Error('provider migration witness path is unsafe'), { code: 'PROVIDER_MIGRATION_UNSAFE_PATH' });
628
+ rmSync(active.manifest.created_settings_witness, { force: true });
629
+ delete active.manifest.created_settings_witness;
630
+ }
631
+ persist();
632
+ return { ok: true, state: 'ROLLBACK_APPLIED' };
633
+ };
634
+
635
+ const verify = async (plan) => {
636
+ const profile = readText(profileFile);
637
+ const settings = readText(settingsFile);
638
+ const profileParsed = profile === null ? { ok: false } : readProviderDeclarations(profile, { file: 'harness/profiles/dsh-crew/cordis.patch.yml' });
639
+ const settingsParsed = settings === null ? { ok: false } : readProviderSettingsDeclarations(settings, { file: 'harness/settings.yaml' });
640
+ const baseAbsent = profileParsed.ok === true && !profileParsed.declarations.some((entry) => entry.id === plan.provider_id);
641
+ const userPresent = settingsParsed.ok === true && settingsParsed.declarations.some((entry) => entry.id === plan.provider_id);
642
+ const userSemanticsPreserved = plan.action !== 'promote-existing-user'
643
+ || (() => {
644
+ const material = settings === null ? { ok: false } : readProviderSettingsMaterialization(settings, { providerId: plan.provider_id, file: 'harness/settings.yaml' });
645
+ return material.ok === true && sameProviderSemantics(material.provider, active?.manifest?.plan?.user_materialization_before ?? plan.user_materialization_before);
646
+ })();
647
+ const nativeRemovable = baseAbsent && userPresent && userSemanticsPreserved;
648
+ return { ok: nativeRemovable, baseAbsent, userPresent, userSemanticsPreserved, nativeRemovable };
649
+ };
650
+
651
+ const finalizeVerified = async () => {
652
+ if (!active) throw Object.assign(new Error('provider migration backup is unavailable'), { code: 'PROVIDER_MIGRATION_BACKUP_INVALID' });
653
+ if (Object.keys(active.manifest.mutation_journal ?? {}).length > 0) throw Object.assign(new Error('provider migration mutation journal is not empty'), { code: 'PROVIDER_MIGRATION_BACKUP_INVALID' });
654
+ active.manifest.phase = 'VERIFIED';
655
+ persist();
656
+ return { ok: true, state: 'VERIFIED' };
657
+ };
658
+
659
+ const setRollbackRuntimeBaseline = async (runtimeId) => {
660
+ if (!active || !safeValue(runtimeId, 128)) throw Object.assign(new Error('provider migration runtime identity is unavailable'), { code: 'PROVIDER_RUNTIME_ID_MISSING' });
661
+ ensureLock();
662
+ active.manifest.plan.rollback_runtime_id_before = safeValue(runtimeId, 128);
663
+ active.manifest.phase = 'ROLLBACK_RESTART_PENDING';
664
+ persist();
665
+ return { ok: true, runtime_id: active.manifest.plan.rollback_runtime_id_before };
666
+ };
667
+
668
+ const setRestartRuntimeBaseline = async (runtimeId) => {
669
+ if (!active || !safeValue(runtimeId, 128)) throw Object.assign(new Error('provider migration runtime identity is unavailable'), { code: 'PROVIDER_RUNTIME_ID_MISSING' });
670
+ ensureLock();
671
+ active.manifest.plan.runtime_id_before_restart = safeValue(runtimeId, 128);
672
+ active.manifest.phase = 'RESTART_PENDING';
673
+ persist();
674
+ return { ok: true, runtime_id: active.manifest.plan.runtime_id_before_restart };
675
+ };
676
+
677
+ const verifyRollback = async (plan) => {
678
+ const profile = readText(profileFile);
679
+ const settings = readText(settingsFile);
680
+ const profileParsed = profile === null ? { ok: false } : readProviderDeclarations(profile, { file: 'harness/profiles/dsh-crew/cordis.patch.yml' });
681
+ const settingsParsed = settings === null ? { ok: false } : readProviderSettingsDeclarations(settings, { file: 'harness/settings.yaml' });
682
+ const restored = profileParsed.ok === true
683
+ && (() => {
684
+ const material = readProviderMaterialization(profile, { providerId: plan.provider_id, file: 'harness/profiles/dsh-crew/cordis.patch.yml' });
685
+ return material.ok === true && sameProviderSemantics(material.provider, active?.manifest?.plan?.materialization?.provider ?? plan.materialization?.provider);
686
+ })();
687
+ const basePresent = restored;
688
+ const userPresent = settingsParsed.ok === true && settingsParsed.declarations.some((entry) => entry.id === plan.provider_id);
689
+ const expectedUser = plan.action === 'promote-existing-user';
690
+ const userSemantics = expectedUser
691
+ ? (() => { const material = readProviderSettingsMaterialization(settings, { providerId: plan.provider_id, file: 'harness/settings.yaml' }); return material.ok === true && sameProviderSemantics(material.provider, active?.manifest?.plan?.user_materialization_before ?? plan.user_materialization_before); })()
692
+ : !userPresent;
693
+ return { ok: basePresent && userSemantics, basePresent, userPresent, userSemantics, expectedUser };
694
+ };
695
+
696
+ const finalizeRollback = async () => {
697
+ if (!active) throw Object.assign(new Error('provider migration backup is unavailable'), { code: 'PROVIDER_MIGRATION_BACKUP_INVALID' });
698
+ ensureLock();
699
+ for (const entry of Object.values(active.manifest.mutation_journal ?? {})) {
700
+ if (entry?.witness && validWitness(active.root, entry.witness)) rmSync(entry.witness, { force: true });
701
+ }
702
+ active.manifest.mutation_journal = {};
703
+ active.manifest.phase = 'ROLLED_BACK';
704
+ if (active.manifest.created_settings_witness) {
705
+ rmSync(active.manifest.created_settings_witness, { force: true });
706
+ delete active.manifest.created_settings_witness;
707
+ }
708
+ persist();
709
+ return { ok: true, state: 'ROLLED_BACK' };
710
+ };
711
+
712
+ const quarantine = async (actionId) => {
713
+ await acquireLock();
714
+ try { ensureLock(); return quarantineUnderRoot(backupDir, actionId, managedRoot); }
715
+ finally { await release(); }
716
+ };
717
+
718
+ return {
719
+ acquireLock, release, recoverLock, backup, materialize, removeBase, rollback, verify, verifyRollback, finalizeVerified, finalizeRollback, setRollbackRuntimeBaseline, setRestartRuntimeBaseline, quarantine,
720
+ backupPlan: () => active?.manifest?.plan ? JSON.parse(JSON.stringify(active.manifest.plan)) : null,
721
+ backupManifest: () => active?.manifest ? JSON.parse(JSON.stringify(active.manifest)) : null,
722
+ restart,
723
+ };
724
+ }
725
+
726
+ /** Return nonterminal migration transactions without exposing source bytes. */
727
+ export function readProviderLayerMigrationTransactions(root) {
728
+ if (typeof root !== 'string' || !existsSync(root)) return [];
729
+ try { if (lstatSync(root).isSymbolicLink() || !lstatSync(root).isDirectory()) return [{ storage_id: null, action_id: null, transaction_id: null, provider_id: null, phase: 'RECOVERY_UNRESOLVED', updated_at: new Date(0).toISOString(), recoverable: false, unresolved: true, source: 'provider-layer-migration' }]; } catch { return [{ storage_id: null, action_id: null, transaction_id: null, provider_id: null, phase: 'RECOVERY_UNRESOLVED', updated_at: new Date(0).toISOString(), recoverable: false, unresolved: true, source: 'provider-layer-migration' }]; }
730
+ try {
731
+ return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.name !== '.quarantine' && !/^\.migration\.lock(?:\.reclaim)?$/u.test(entry.name)).flatMap((entry) => {
732
+ const file = join(root, entry.name, 'manifest.json');
733
+ const actionId = sha256(entry.name).slice(0, 32);
734
+ if (!entry.isDirectory() || entry.isSymbolicLink()) return [{ storage_id: entry.name, action_id: actionId, transaction_id: null, provider_id: null, phase: 'RECOVERY_UNRESOLVED', updated_at: new Date(0).toISOString(), recoverable: false, unresolved: true, source: 'provider-layer-migration' }];
735
+ try {
736
+ if (lstatSync(file).isSymbolicLink() || !lstatSync(file).isFile()) throw new Error('invalid migration manifest path');
737
+ const manifest = JSON.parse(readFileSync(file, 'utf8'));
738
+ if (!validMigrationManifest(join(root, entry.name), manifest)) return [{ storage_id: entry.name, action_id: actionId, transaction_id: safeId(manifest?.plan_id), provider_id: safeId(manifest?.provider_id), phase: 'RECOVERY_UNRESOLVED', updated_at: new Date(0).toISOString(), recoverable: false, unresolved: true, source: 'provider-layer-migration' }];
739
+ const phase = text(manifest?.phase) ?? 'RECOVERY_UNRESOLVED';
740
+ if (['VERIFIED', 'ROLLED_BACK'].includes(phase)) return [];
741
+ const providerId = safeId(manifest?.provider_id);
742
+ return [{
743
+ storage_id: entry.name, action_id: actionId,
744
+ transaction_id: safeId(manifest?.plan_id),
745
+ provider_id: providerId,
746
+ phase,
747
+ updated_at: text(manifest?.created_at) ?? statSync(file).mtime.toISOString(),
748
+ recoverable: providerId !== null,
749
+ unresolved: providerId === null,
750
+ source: 'provider-layer-migration',
751
+ }];
752
+ } catch {
753
+ return [{ storage_id: entry.name, action_id: actionId, transaction_id: null, provider_id: null, phase: 'RECOVERY_UNRESOLVED', updated_at: new Date(0).toISOString(), recoverable: false, unresolved: true, source: 'provider-layer-migration' }];
754
+ }
755
+ });
756
+ } catch {
757
+ return [{ storage_id: null, action_id: null, transaction_id: null, provider_id: null, phase: 'RECOVERY_UNRESOLVED', updated_at: new Date(0).toISOString(), recoverable: false, unresolved: true, source: 'provider-layer-migration' }];
758
+ }
759
+ }