@ran-sh/dsh-crew 1.0.1 → 1.0.2

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.
@@ -1,451 +1,1786 @@
1
- // Filesystem-backed adapters for the provider deletion transaction.
2
- //
3
- // The transaction state machine remains side-effect free. This module owns
4
- // only the three Crew-managed files that can be changed by a provider delete:
5
- // the Harness provider patch, the canonical Crew config, and the lifecycle
6
- // tombstone file. Credentials are deliberately never read or copied.
7
-
8
- import {
9
- copyFileSync,
10
- existsSync,
11
- lstatSync,
12
- mkdirSync,
13
- readFileSync,
14
- renameSync,
15
- rmSync,
16
- writeFileSync,
17
- } from 'node:fs';
18
- import { createHash } from 'node:crypto';
19
- import { basename, dirname, join } from 'node:path';
20
- import {
21
- inspectProviderProfile,
22
- readProviderDeclarations,
23
- removeProviderDeclarations,
24
- } from './provider-profile-store.mjs';
25
- import { scrubProviderReferences } from './provider-config-scrub.mjs';
26
- import {
27
- markProviderTombstone,
28
- normalizeProviderLifecycleState,
29
- recordProviderTransaction,
30
- } from './provider-lifecycle-state.mjs';
31
-
32
- const FILE_KEYS = Object.freeze(['profile', 'config', 'lifecycle']);
33
- const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
34
-
35
- function validProviderId(value) {
36
- return typeof value === 'string' && ID_PATTERN.test(value);
37
- }
38
-
39
- function sha256(value) {
40
- return createHash('sha256').update(String(value), 'utf8').digest('hex');
41
- }
42
-
43
- function safePlanId(value) {
44
- return typeof value === 'string' && /^[0-9a-f-]{36}$/i.test(value) ? value : null;
45
- }
46
-
47
- function atomicWrite(file, content) {
48
- const dir = dirname(file);
49
- mkdirSync(dir, { recursive: true });
50
- const temp = join(dir, `.${basename(file)}.${process.pid}.${Date.now()}.dsh-crew.tmp`);
51
- try {
52
- writeFileSync(temp, content);
53
- renameSync(temp, file);
54
- } catch (error) {
55
- try { rmSync(temp, { force: true }); } catch {}
56
- throw error;
57
- }
58
- }
59
-
60
- function readJson(file, fallback) {
61
- if (!existsSync(file)) return fallback;
62
- try {
63
- return JSON.parse(readFileSync(file, 'utf8'));
64
- } catch {
65
- throw Object.assign(new Error('managed provider file is invalid'), { code: 'PROVIDER_DELETE_FILE_INVALID' });
66
- }
67
- }
68
-
69
- function defaultReadConfig(configFile) {
70
- assertManagedPath(configFile);
71
- return readJson(configFile, {});
72
- }
73
-
74
- function defaultWriteConfig(configFile, config) {
75
- atomicWrite(configFile, JSON.stringify(config, null, 2) + '\n');
76
- }
77
-
78
- function profileContainsProvider(source, providerId) {
79
- const escaped = providerId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
80
- return new RegExp(`^\\s+${escaped}:\\s*$`, 'm').test(source);
81
- }
82
-
83
- function fileMap({ profileFile, configFile, lifecycleFile }) {
84
- return { profile: profileFile, config: configFile, lifecycle: lifecycleFile };
85
- }
86
-
87
- function hasOwn(value, key) {
88
- return !!value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key);
89
- }
90
-
91
- function safeModelRef(value) {
92
- if (!value || typeof value !== 'object' || Array.isArray(value)) return value ?? null;
93
- const result = {};
94
- for (const key of ['provider', 'model', 'reasoningEffort']) {
95
- if (typeof value[key] === 'string' && value[key].trim()) result[key] = value[key].trim();
96
- }
97
- return result;
98
- }
99
-
100
- function safeModelRefList(value) {
101
- return Array.isArray(value) ? value.map((entry) => safeModelRef(entry)) : value;
102
- }
103
-
104
- function configProjection(config) {
105
- const projection = { schema_version: 1, fields: {} };
106
- for (const key of ['flash_model_priority', 'pro_model_priority', 'harness_default', 'agent_default_model', 'agentDefaultModel']) {
107
- if (hasOwn(config, key)) projection.fields[key] = key.endsWith('_priority') ? safeModelRefList(config[key]) : safeModelRef(config[key]);
108
- }
109
- for (const [scope, value] of [['worker', config?.worker?.model_policy], ['review', config?.review?.model_policy]]) {
110
- if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
111
- const fields = {};
112
- for (const key of ['priority', 'escalation_priority']) if (hasOwn(value, key)) fields[key] = safeModelRefList(value[key]);
113
- if (Object.keys(fields).length > 0) projection.fields[scope] = { model_policy: fields };
114
- }
115
- return projection;
116
- }
117
-
118
- function lifecycleProjection(state) {
119
- return { tombstones: { ...(state?.tombstones ?? {}) } };
120
- }
121
-
122
- function restoreConfigProjection(config, projection) {
123
- const next = typeof structuredClone === 'function' ? structuredClone(config) : JSON.parse(JSON.stringify(config));
124
- for (const key of ['flash_model_priority', 'pro_model_priority', 'harness_default', 'agent_default_model', 'agentDefaultModel']) {
125
- if (hasOwn(projection?.fields, key)) next[key] = projection.fields[key];
126
- }
127
- for (const scope of ['worker', 'review']) {
128
- const saved = projection?.fields?.[scope]?.model_policy;
129
- if (!saved) continue;
130
- next[scope] = { ...(next[scope] ?? {}), model_policy: { ...(next[scope]?.model_policy ?? {}) } };
131
- for (const key of ['priority', 'escalation_priority']) if (hasOwn(saved, key)) next[scope].model_policy[key] = saved[key];
132
- }
133
- return next;
134
- }
135
-
136
- function safeRestoreFile(source, target) {
137
- if (lstatSync(source).isSymbolicLink()) throw Object.assign(new Error('backup file is a symlink'), { code: 'PROVIDER_DELETE_ROLLBACK_UNSAFE_PATH' });
138
- try {
139
- if (lstatSync(target).isSymbolicLink()) rmSync(target, { force: true });
140
- } catch (error) {
141
- if (error?.code !== 'ENOENT') throw error;
142
- }
143
- atomicWrite(target, readFileSync(source));
144
- }
145
-
146
- function assertManagedPath(file) {
147
- try {
148
- if (lstatSync(file).isSymbolicLink()) {
149
- throw Object.assign(new Error('managed provider path is a symlink'), { code: 'PROVIDER_DELETE_UNSAFE_PATH' });
150
- }
151
- } catch (error) {
152
- if (error?.code === 'ENOENT') return;
153
- throw error;
154
- }
155
- }
156
-
157
- function validatePaths(paths) {
158
- for (const key of FILE_KEYS) {
159
- if (typeof paths[key] !== 'string' || !paths[key].trim()) {
160
- throw new TypeError(`provider delete ${key} path is required`);
161
- }
162
- }
163
- }
164
-
165
- /**
166
- * Build explicit adapters for executeProviderDelete.
167
- *
168
- * `restart` is intentionally required from the caller. The Hub process must
169
- * not guess how its supervisor works or restart the official 3080 process.
170
- * Without a supplied supervisor the transaction fails closed and compensates
171
- * any writes using the managed-file backup.
172
- */
173
- export function createProviderDeleteFileHooks({
174
- profileFile,
175
- configFile,
176
- lifecycleFile,
177
- backupDir,
178
- readConfig = () => defaultReadConfig(configFile),
179
- writeConfig = (config) => defaultWriteConfig(configFile, config),
180
- restart,
181
- existingBackupId = null,
182
- expectedProviderId = null,
183
- fs = { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync },
184
- } = {}) {
185
- validatePaths({ profile: profileFile, config: configFile, lifecycle: lifecycleFile });
186
- if (typeof backupDir !== 'string' || !backupDir.trim()) throw new TypeError('provider delete backup directory is required');
187
- const paths = fileMap({ profileFile, configFile, lifecycleFile });
188
- let activeBackup = null;
189
- let lockPath = null;
190
- let lockOwned = false;
191
-
192
- const acquireLock = async () => {
193
- if (lockOwned) return;
194
- fs.mkdirSync(backupDir, { recursive: true });
195
- lockPath = join(backupDir, '.delete.lock');
196
- try {
197
- fs.mkdirSync(lockPath);
198
- lockOwned = true;
199
- } catch (error) {
200
- throw Object.assign(new Error('another provider deletion is active'), { code: error?.code === 'EEXIST' ? 'PROVIDER_DELETE_BUSY' : 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
201
- }
202
- };
203
-
204
- if (existingBackupId !== null) {
205
- const backupId = safePlanId(existingBackupId);
206
- if (!backupId) throw Object.assign(new Error('provider backup id is invalid'), { code: 'PROVIDER_DELETE_PLAN_INVALID' });
207
- const root = join(backupDir, backupId);
208
- const manifest = readJson(join(root, 'manifest.json'), null);
209
- if (!manifest || manifest.schema_version !== 1 || !manifest.files || !manifest.plan || (expectedProviderId && manifest.provider_id !== expectedProviderId)) {
210
- throw Object.assign(new Error('provider backup is invalid'), { code: 'PROVIDER_DELETE_BACKUP_INVALID' });
211
- }
212
- activeBackup = { root, manifest };
213
- }
214
-
215
- const persistManifest = () => {
216
- if (!activeBackup) return;
217
- atomicWrite(join(activeBackup.root, 'manifest.json'), JSON.stringify(activeBackup.manifest, null, 2) + '\n');
218
- };
219
-
220
- const backup = async (plan) => {
221
- const planId = safePlanId(plan?.plan_id);
222
- if (!planId) throw Object.assign(new Error('provider delete plan id is invalid'), { code: 'PROVIDER_DELETE_PLAN_INVALID' });
223
- await acquireLock();
224
- const root = join(backupDir, planId);
225
- fs.mkdirSync(root, { recursive: true });
226
- const manifest = {
227
- schema_version: 1,
228
- provider_id: plan.provider_id,
229
- plan: {
230
- plan_id: plan.plan_id,
231
- provider_id: plan.provider_id,
232
- expected_revision: plan.expected_revision ?? null,
233
- replacement_default: plan.replacement_default ?? null,
234
- was_harness_default: plan.was_harness_default === true,
235
- replacement_default_model: plan.replacement_default_model ?? null,
236
- credential_refs: (Array.isArray(plan.credential_refs) ? plan.credential_refs : []).map((ref) => ({
237
- kind: typeof ref?.kind === 'string' ? ref.kind.trim() : 'unknown',
238
- name_or_handle: typeof ref?.name_or_handle === 'string' ? ref.name_or_handle.trim() : 'unknown',
239
- ownership: typeof ref?.ownership === 'string' ? ref.ownership.trim() : 'unknown',
240
- })).filter((ref) => ref.kind && ref.name_or_handle && ref.ownership).slice(0, 32),
241
- },
242
- files: {},
243
- };
244
- for (const key of FILE_KEYS) {
245
- const source = paths[key];
246
- assertManagedPath(source);
247
- const existed = fs.existsSync(source);
248
- manifest.files[key] = { existed };
249
- if (existed && key !== 'config') {
250
- const target = join(root, `${key}.backup`);
251
- fs.copyFileSync(source, target);
252
- }
253
- }
254
- const config = defaultReadConfig(configFile);
255
- const lifecycle = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
256
- manifest.config_projection = configProjection(config);
257
- manifest.config_revision = sha256(JSON.stringify(config));
258
- manifest.lifecycle_revision = sha256(JSON.stringify(lifecycle));
259
- manifest.routing_projection_digest = sha256(JSON.stringify(manifest.config_projection));
260
- manifest.lifecycle_projection_digest = sha256(JSON.stringify(lifecycleProjection(lifecycle)));
261
- manifest.profile_revision = sha256(fs.readFileSync(profileFile, 'utf8'));
262
- activeBackup = { root, manifest };
263
- persistManifest();
264
- return { ok: true, backup_id: planId };
265
- };
266
-
267
- const markTombstone = async (providerId) => {
268
- if (!validProviderId(providerId)) throw Object.assign(new Error('provider id is invalid'), { code: 'PROVIDER_NOT_FOUND' });
269
- assertManagedPath(lifecycleFile);
270
- const state = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
271
- if (activeBackup?.manifest?.lifecycle_revision && sha256(JSON.stringify(state)) !== activeBackup.manifest.lifecycle_revision) {
272
- throw Object.assign(new Error('provider lifecycle changed'), { code: 'PROVIDER_LIFECYCLE_CHANGED' });
273
- }
274
- const marked = markProviderTombstone(state, providerId);
275
- atomicWrite(lifecycleFile, JSON.stringify(marked, null, 2) + '\n');
276
- if (activeBackup) {
277
- activeBackup.manifest.applied_lifecycle_revision = sha256(JSON.stringify(marked));
278
- persistManifest();
279
- }
280
- };
281
-
282
- const scrubReferences = async (plan) => {
283
- const config = await readConfig();
284
- if (sha256(JSON.stringify(config)) !== activeBackup?.manifest?.config_revision) {
285
- throw Object.assign(new Error('provider config changed'), { code: 'PROVIDER_CONFIG_CHANGED' });
286
- }
287
- const scrubbed = scrubProviderReferences(config, [plan.provider_id]);
288
- if (plan.was_harness_default === true) {
289
- if (!validProviderId(plan.replacement_default) || typeof plan.replacement_default_model !== 'string' || !plan.replacement_default_model.trim()) {
290
- throw Object.assign(new Error('replacement Harness Default model is required'), { code: 'PROVIDER_DEFAULT_REPLACEMENT_MODEL_REQUIRED' });
291
- }
292
- scrubbed.config.harness_default = {
293
- provider: plan.replacement_default,
294
- model: plan.replacement_default_model.trim(),
295
- };
296
- }
297
- await writeConfig(scrubbed.config);
298
- if (activeBackup) {
299
- activeBackup.manifest.applied_config_revision = sha256(JSON.stringify(scrubbed.config));
300
- persistManifest();
301
- }
302
- };
303
-
304
- const removeDeclarations = async (plan) => {
305
- assertManagedPath(profileFile);
306
- const source = fs.readFileSync(profileFile, 'utf8');
307
- const result = removeProviderDeclarations(source, {
308
- providerIds: [plan.provider_id],
309
- expectedRevision: plan.expected_revision,
310
- });
311
- if (!result.ok) throw Object.assign(new Error('provider profile changed'), { code: result.code });
312
- atomicWrite(profileFile, result.text);
313
- if (activeBackup) {
314
- activeBackup.manifest.applied_profile_revision = result.revision;
315
- persistManifest();
316
- }
317
- };
318
-
319
- const checkpointApplied = async () => {
320
- if (!activeBackup) throw Object.assign(new Error('provider delete backup is unavailable'), { code: 'PROVIDER_DELETE_BACKUP_INVALID' });
321
- const config = await readConfig();
322
- const lifecycle = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
323
- activeBackup.manifest.applied_config_revision = sha256(JSON.stringify(config));
324
- activeBackup.manifest.applied_lifecycle_revision = sha256(JSON.stringify(lifecycle));
325
- activeBackup.manifest.applied_profile_revision = sha256(fs.readFileSync(profileFile, 'utf8'));
326
- persistManifest();
327
- };
328
-
329
- const backupPlan = () => (activeBackup?.manifest?.plan ? { ...activeBackup.manifest.plan } : null);
330
-
331
- const verify = async (plan) => {
332
- assertManagedPath(profileFile);
333
- assertManagedPath(lifecycleFile);
334
- const source = fs.readFileSync(profileFile, 'utf8');
335
- const parsed = readProviderDeclarations(source, { file: 'harness/profiles/dsh-crew/cordis.patch.yml' });
336
- const providerAbsent = parsed.ok
337
- ? !parsed.declarations.some((declaration) => declaration.id === plan.provider_id)
338
- : !profileContainsProvider(source, plan.provider_id);
339
- const config = await readConfig();
340
- const routingClear = scrubProviderReferences(config, [plan.provider_id]).removed.length === 0;
341
- const replacementApplied = plan.was_harness_default !== true || (
342
- config?.harness_default?.provider === plan.replacement_default
343
- && config?.harness_default?.model === plan.replacement_default_model
344
- );
345
- const state = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
346
- return {
347
- providerAbsent,
348
- routingClear: routingClear && replacementApplied,
349
- tombstonePresent: state.tombstones[plan.provider_id] === 'absent',
350
- };
351
- };
352
-
353
- const rollback = async () => {
354
- if (!activeBackup) throw Object.assign(new Error('provider delete backup is unavailable'), { code: 'PROVIDER_DELETE_ROLLBACK_UNAVAILABLE' });
355
- const allowedRevision = (current, original, applied) => current === original || (typeof applied === 'string' && current === applied);
356
- const currentConfig = await readConfig();
357
- const currentLifecycle = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
358
- const currentProfile = fs.readFileSync(profileFile, 'utf8');
359
- if (!allowedRevision(sha256(JSON.stringify(currentConfig)), activeBackup.manifest.config_revision, activeBackup.manifest.applied_config_revision)
360
- || !allowedRevision(sha256(JSON.stringify(currentLifecycle)), activeBackup.manifest.lifecycle_revision, activeBackup.manifest.applied_lifecycle_revision)
361
- || !allowedRevision(sha256(currentProfile), activeBackup.manifest.profile_revision, activeBackup.manifest.applied_profile_revision)) {
362
- throw Object.assign(new Error('managed provider state changed after the transaction'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
363
- }
364
- for (const key of FILE_KEYS) {
365
- const target = paths[key];
366
- const entry = activeBackup.manifest.files[key];
367
- const backupFile = join(activeBackup.root, `${key}.backup`);
368
- assertManagedPath(target);
369
- if (key === 'config') {
370
- if (entry?.existed === true) {
371
- const restored = restoreConfigProjection(await readConfig(), activeBackup.manifest.config_projection);
372
- await writeConfig(restored);
373
- } else {
374
- fs.rmSync(target, { force: true });
375
- }
376
- } else if (entry?.existed === true) {
377
- safeRestoreFile(backupFile, target);
378
- } else {
379
- fs.rmSync(target, { force: true });
380
- }
381
- }
382
- };
383
-
384
- const release = async () => {
385
- if (!lockOwned || !lockPath) return;
386
- try { fs.rmSync(lockPath, { recursive: true, force: true }); } finally { lockOwned = false; lockPath = null; }
387
- };
388
-
389
- const verifyRollback = async (plan) => {
390
- assertManagedPath(profileFile);
391
- assertManagedPath(lifecycleFile);
392
- const source = fs.readFileSync(profileFile, 'utf8');
393
- const parsed = readProviderDeclarations(source, { file: 'harness/profiles/dsh-crew/cordis.patch.yml' });
394
- const providerPresent = parsed.ok && parsed.declarations.some((declaration) => declaration.id === plan.provider_id);
395
- const config = await readConfig();
396
- const state = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
397
- const routingRestored = typeof activeBackup?.manifest?.routing_projection_digest === 'string'
398
- && sha256(JSON.stringify(configProjection(config))) === activeBackup.manifest.routing_projection_digest;
399
- const lifecycleRestored = typeof activeBackup?.manifest?.lifecycle_projection_digest === 'string'
400
- && sha256(JSON.stringify(lifecycleProjection(state))) === activeBackup.manifest.lifecycle_projection_digest;
401
- const declarationRestored = typeof activeBackup?.manifest?.profile_revision === 'string'
402
- && sha256(source) === activeBackup.manifest.profile_revision;
403
- return {
404
- ok: providerPresent && state.tombstones[plan.provider_id] !== 'absent'
405
- && routingRestored && lifecycleRestored && declarationRestored,
406
- providerPresent,
407
- tombstoneCleared: state.tombstones[plan.provider_id] !== 'absent',
408
- routingRestored,
409
- lifecycleRestored,
410
- declarationRestored,
411
- };
412
- };
413
-
414
- const recordTransaction = async (result, plan) => {
415
- assertManagedPath(lifecycleFile);
416
- const state = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
417
- const next = recordProviderTransaction(state, {
418
- transaction_id: result?.transaction_id ?? plan?.plan_id,
419
- provider_id: result?.provider_id ?? plan?.provider_id,
420
- state: result?.state,
421
- expected_revision: plan?.expected_revision,
422
- credential_refs: plan?.credential_refs,
423
- });
424
- if (result?.state === 'VERIFIED') {
425
- assertManagedPath(profileFile);
426
- next.last_verified_revision[plan.provider_id] = sha256(fs.readFileSync(profileFile, 'utf8'));
427
- }
428
- atomicWrite(lifecycleFile, JSON.stringify(next, null, 2) + '\n');
429
- };
430
-
431
- return {
432
- backup,
433
- acquireLock,
434
- checkpointApplied,
435
- backupPlan,
436
- markTombstone,
437
- scrubReferences,
438
- removeDeclarations,
439
- restart: typeof restart === 'function'
440
- ? async (plan) => restart(plan)
441
- : async () => ({ ok: false, code: 'PROVIDER_DELETE_RESTART_SUPERVISOR_UNAVAILABLE' }),
442
- restartRollback: typeof restart === 'function'
443
- ? async (plan) => restart(plan)
444
- : async () => ({ ok: false, code: 'PROVIDER_DELETE_RESTART_SUPERVISOR_UNAVAILABLE' }),
445
- verify,
446
- verifyRollback,
447
- recordTransaction,
448
- rollback,
449
- release,
450
- };
451
- }
1
+ // Filesystem-backed adapters for the provider deletion transaction.
2
+ //
3
+ // The transaction state machine remains side-effect free. This module owns
4
+ // only the Crew-managed files that can be changed by a provider delete: the
5
+ // Harness provider patch, Harness settings provider map, canonical Crew
6
+ // config, and lifecycle tombstone file. Credentials are deliberately never
7
+ // read or copied.
8
+
9
+ import {
10
+ copyFileSync,
11
+ closeSync,
12
+ existsSync,
13
+ fstatSync,
14
+ lstatSync,
15
+ linkSync,
16
+ mkdirSync,
17
+ openSync,
18
+ realpathSync,
19
+ readFileSync,
20
+ renameSync,
21
+ rmSync,
22
+ writeFileSync,
23
+ readdirSync,
24
+ } from 'node:fs';
25
+ import { createHash, randomUUID } from 'node:crypto';
26
+ import { basename, dirname, isAbsolute, join, relative, resolve as resolvePath } from 'node:path';
27
+ import { hasInlineProviderCredentials as hasInlineProfileCredentials, readProviderDeclarations, removeProviderDeclarations } from './provider-profile-store.mjs';
28
+ import {
29
+ readProviderSettingsDeclarations,
30
+ removeProviderSettings,
31
+ readHarnessDefault,
32
+ mutateProviderSettings,
33
+ hasInlineProviderCredentials as hasInlineSettingsCredentials,
34
+ } from './provider-settings-store.mjs';
35
+ import { scrubProviderReferences } from './provider-config-scrub.mjs';
36
+ import {
37
+ markProviderTombstone,
38
+ normalizeProviderLifecycleState,
39
+ recordProviderTransaction,
40
+ } from './provider-lifecycle-state.mjs';
41
+
42
+ const FILE_KEYS = Object.freeze(['profile', 'config', 'lifecycle']);
43
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
44
+
45
+ function validProviderId(value) {
46
+ return typeof value === 'string' && ID_PATTERN.test(value);
47
+ }
48
+
49
+ function sha256(value) {
50
+ const input = Buffer.isBuffer(value) || value instanceof Uint8Array ? value : String(value);
51
+ return createHash('sha256').update(input).digest('hex');
52
+ }
53
+
54
+ function decodeUtf8(value) {
55
+ if (typeof value === 'string') return value;
56
+ try { return new TextDecoder('utf-8', { fatal: true }).decode(value); }
57
+ catch { throw Object.assign(new Error('managed provider file is not valid UTF-8'), { code: 'PROVIDER_DELETE_FILE_INVALID' }); }
58
+ }
59
+
60
+ function parseManagedJson(value) {
61
+ try { return JSON.parse(decodeUtf8(value)); }
62
+ catch { throw Object.assign(new Error('managed provider file is invalid'), { code: 'PROVIDER_DELETE_FILE_INVALID' }); }
63
+ }
64
+
65
+ function safePlanId(value) {
66
+ return typeof value === 'string' && /^[0-9a-f-]{36}$/i.test(value) ? value : null;
67
+ }
68
+
69
+ function safeRecoveryEntryName(value) {
70
+ return typeof value === 'string' && value.length > 0 && value.length <= 255
71
+ && value !== '.' && value !== '..' && !/[\\/\u0000]/u.test(value) ? value : null;
72
+ }
73
+
74
+ function atomicWrite(file, content, managedRoot = null, { replace = true } = {}) {
75
+ if (managedRoot) assertManagedPath(file, managedRoot);
76
+ const dir = dirname(file);
77
+ if (managedRoot) {
78
+ assertManagedPath(dir, managedRoot);
79
+ if (!existsSync(dir)) throw Object.assign(new Error('managed provider directory is missing'), { code: 'PROVIDER_DELETE_UNSAFE_PATH' });
80
+ } else {
81
+ mkdirSync(dir, { recursive: true });
82
+ }
83
+ const tempRoot = managedRoot ? resolvePath(managedRoot) : dir;
84
+ const temp = join(tempRoot, `.${basename(file)}.${process.pid}.${Date.now()}.dsh-crew.tmp`);
85
+ if (managedRoot) assertManagedPath(temp, managedRoot);
86
+ try {
87
+ writeFileSync(temp, content);
88
+ if (managedRoot) assertManagedPath(file, managedRoot);
89
+ if (replace) renameSync(temp, file);
90
+ else {
91
+ linkSync(temp, file);
92
+ rmSync(temp, { force: true });
93
+ }
94
+ } catch (error) {
95
+ try { rmSync(temp, { force: true }); } catch {}
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ function readJson(file, fallback) {
101
+ if (!existsSync(file)) return fallback;
102
+ try {
103
+ return JSON.parse(readFileSync(file, 'utf8'));
104
+ } catch {
105
+ throw Object.assign(new Error('managed provider file is invalid'), { code: 'PROVIDER_DELETE_FILE_INVALID' });
106
+ }
107
+ }
108
+
109
+ function defaultReadConfig(configFile, managedRoot = null) {
110
+ assertManagedPath(configFile, managedRoot);
111
+ return readJson(configFile, {});
112
+ }
113
+
114
+ function atomicCreateWithWitness(file, content, managedRoot, witness) {
115
+ if (!managedRoot || typeof witness !== 'string' || !witness.trim()) {
116
+ throw Object.assign(new Error('exclusive provider create witness is unavailable'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
117
+ }
118
+ assertManagedPath(file, managedRoot);
119
+ assertManagedPath(witness, managedRoot);
120
+ const parent = dirname(file);
121
+ assertManagedPath(parent, managedRoot);
122
+ if (!existsSync(parent)) throw Object.assign(new Error('managed provider directory is missing'), { code: 'PROVIDER_DELETE_UNSAFE_PATH' });
123
+ const witnessExisted = existsSync(witness);
124
+ try {
125
+ if (witnessExisted) {
126
+ if (sha256(readFileSync(witness)) !== sha256(content)) throw Object.assign(new Error('provider create witness content changed'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
127
+ } else writeFileSync(witness, content, { flag: 'wx' });
128
+ assertManagedPath(witness, managedRoot);
129
+ linkSync(witness, file);
130
+ assertManagedPath(file, managedRoot);
131
+ return witness;
132
+ } catch (error) {
133
+ if (!witnessExisted) { try { rmSync(witness, { force: true }); } catch {} }
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ function atomicReplaceWithWitness(file, content, managedRoot, witness) {
139
+ if (!managedRoot || typeof witness !== 'string' || !witness.trim()) {
140
+ throw Object.assign(new Error('provider replace witness is unavailable'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
141
+ }
142
+ assertManagedPath(file, managedRoot);
143
+ assertManagedPath(witness, managedRoot);
144
+ const stage = join(dirname(witness), `.${basename(file)}.${randomUUID()}.replace-stage`);
145
+ assertManagedPath(stage, managedRoot);
146
+ let published = false;
147
+ const witnessExisted = existsSync(witness);
148
+ try {
149
+ if (witnessExisted) {
150
+ if (sha256(readFileSync(witness)) !== sha256(content)) throw Object.assign(new Error('provider replace witness content changed'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
151
+ } else writeFileSync(witness, content, { flag: 'wx' });
152
+ linkSync(witness, stage);
153
+ assertManagedPath(file, managedRoot);
154
+ renameSync(stage, file);
155
+ published = true;
156
+ assertManagedPath(file, managedRoot);
157
+ return witness;
158
+ } catch (error) {
159
+ try { rmSync(stage, { force: true }); } catch {}
160
+ if (!published && !witnessExisted) { try { rmSync(witness, { force: true }); } catch {} }
161
+ throw error;
162
+ }
163
+ }
164
+
165
+ function defaultWriteConfig(configFile, config, managedRoot = null, replace = true) {
166
+ atomicWrite(configFile, JSON.stringify(config, null, 2) + '\n', managedRoot, { replace });
167
+ }
168
+
169
+ function fileMap({ profileFile, settingsFile, configFile, lifecycleFile }) {
170
+ return { profile: profileFile, settings: settingsFile, config: configFile, lifecycle: lifecycleFile };
171
+ }
172
+
173
+ function hasOwn(value, key) {
174
+ return !!value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key);
175
+ }
176
+
177
+ function safeModelRef(value) {
178
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value ?? null;
179
+ const result = {};
180
+ for (const key of ['provider', 'model', 'reasoningEffort']) {
181
+ if (typeof value[key] === 'string' && value[key].trim()) result[key] = value[key].trim();
182
+ }
183
+ return result;
184
+ }
185
+
186
+ function safeModelRefList(value) {
187
+ return Array.isArray(value) ? value.map((entry) => safeModelRef(entry)) : value;
188
+ }
189
+
190
+ function configProjection(config) {
191
+ const projection = { schema_version: 1, fields: {}, present_fields: [] };
192
+ for (const key of ['flash_model_priority', 'pro_model_priority', 'harness_default', 'agent_default_model', 'agentDefaultModel']) {
193
+ if (hasOwn(config, key)) {
194
+ projection.present_fields.push(key);
195
+ projection.fields[key] = key.endsWith('_priority') ? safeModelRefList(config[key]) : safeModelRef(config[key]);
196
+ }
197
+ }
198
+ for (const [scope, value] of [['worker', config?.worker?.model_policy], ['review', config?.review?.model_policy]]) {
199
+ if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
200
+ const fields = {};
201
+ for (const key of ['priority', 'escalation_priority']) if (hasOwn(value, key)) fields[key] = safeModelRefList(value[key]);
202
+ if (Object.keys(fields).length > 0) projection.fields[scope] = { model_policy: fields };
203
+ }
204
+ return projection;
205
+ }
206
+
207
+ function lifecycleProjection(state) {
208
+ return { tombstones: { ...(state?.tombstones ?? {}) } };
209
+ }
210
+
211
+ function restoreConfigProjection(config, projection) {
212
+ const next = typeof structuredClone === 'function' ? structuredClone(config) : JSON.parse(JSON.stringify(config));
213
+ for (const key of ['flash_model_priority', 'pro_model_priority', 'harness_default', 'agent_default_model', 'agentDefaultModel']) {
214
+ if (hasOwn(projection?.fields, key)) next[key] = projection.fields[key];
215
+ else if (Array.isArray(projection?.present_fields) && !projection.present_fields.includes(key)) delete next[key];
216
+ }
217
+ for (const scope of ['worker', 'review']) {
218
+ const saved = projection?.fields?.[scope]?.model_policy;
219
+ if (!saved) continue;
220
+ next[scope] = { ...(next[scope] ?? {}), model_policy: { ...(next[scope]?.model_policy ?? {}) } };
221
+ for (const key of ['priority', 'escalation_priority']) if (hasOwn(saved, key)) next[scope].model_policy[key] = saved[key];
222
+ }
223
+ return next;
224
+ }
225
+
226
+ function safeRestoreFile(source, target, managedRoot = null, verifiedContent = null) {
227
+ if (managedRoot) {
228
+ assertManagedPath(source, managedRoot);
229
+ assertManagedPath(target, managedRoot);
230
+ }
231
+ if (lstatSync(source).isSymbolicLink()) throw Object.assign(new Error('backup file is a symlink'), { code: 'PROVIDER_DELETE_ROLLBACK_UNSAFE_PATH' });
232
+ try {
233
+ if (lstatSync(target).isSymbolicLink()) rmSync(target, { force: true });
234
+ } catch (error) {
235
+ if (error?.code !== 'ENOENT') throw error;
236
+ }
237
+ atomicWrite(target, verifiedContent ?? readFileSync(source), managedRoot);
238
+ }
239
+
240
+ function assertManagedPath(file, managedRoot = null) {
241
+ if (managedRoot) {
242
+ const root = resolvePath(managedRoot);
243
+ const target = resolvePath(file);
244
+ const resolvedRelative = relative(root, target);
245
+ const outside = /^(?:\.\.(?:[\\/]|$))/.test(resolvedRelative) || isAbsolute(resolvedRelative);
246
+ if (outside) throw Object.assign(new Error('managed provider path escapes Crew root'), { code: 'PROVIDER_DELETE_UNSAFE_PATH' });
247
+ let current = target;
248
+ while (current && current !== dirname(current)) {
249
+ try {
250
+ if (lstatSync(current).isSymbolicLink()) throw Object.assign(new Error('managed provider path is a symlink'), { code: 'PROVIDER_DELETE_UNSAFE_PATH' });
251
+ current = dirname(current);
252
+ } catch (error) {
253
+ if (error?.code === 'ENOENT') { current = dirname(current); continue; }
254
+ throw error;
255
+ }
256
+ }
257
+ try {
258
+ const existing = realpathSync(target);
259
+ const resolvedRoot = realpathSync(root);
260
+ const resolvedRelative = relative(resolvedRoot, existing);
261
+ if (/^(?:\.\.(?:[\\/]|$))/.test(resolvedRelative) || isAbsolute(resolvedRelative)) throw Object.assign(new Error('managed provider path resolves outside Crew root'), { code: 'PROVIDER_DELETE_UNSAFE_PATH' });
262
+ } catch (error) {
263
+ if (error?.code !== 'ENOENT') throw error;
264
+ }
265
+ }
266
+ try {
267
+ if (lstatSync(file).isSymbolicLink()) {
268
+ throw Object.assign(new Error('managed provider path is a symlink'), { code: 'PROVIDER_DELETE_UNSAFE_PATH' });
269
+ }
270
+ } catch (error) {
271
+ if (error?.code === 'ENOENT') return;
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ function readManagedJsonSnapshot(file, managedRoot, fsLike = { readFileSync }) {
277
+ assertManagedPath(file, managedRoot);
278
+ let stat;
279
+ try { stat = lstatSync(file, { bigint: true }); }
280
+ catch { throw Object.assign(new Error('managed provider manifest is unavailable'), { code: 'PROVIDER_DELETE_BACKUP_INVALID' }); }
281
+ if (!stat.isFile() || stat.isSymbolicLink()) {
282
+ throw Object.assign(new Error('managed provider manifest path is unsafe'), { code: 'PROVIDER_DELETE_UNSAFE_PATH' });
283
+ }
284
+ let fd;
285
+ try {
286
+ fd = openSync(file, 'r');
287
+ const pinned = fstatSync(fd, { bigint: true });
288
+ if (!sameFileIdentity({ dev: String(stat.dev), ino: String(stat.ino) }, { dev: String(pinned.dev), ino: String(pinned.ino) })) {
289
+ throw Object.assign(new Error('managed provider manifest changed during open'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
290
+ }
291
+ return parseManagedJson(readFileSync(fd));
292
+ } finally {
293
+ if (fd !== undefined) { try { closeSync(fd); } catch {} }
294
+ }
295
+ }
296
+
297
+ function assertManagedEntryPath(file, managedRoot) {
298
+ const root = resolvePath(managedRoot);
299
+ const target = resolvePath(file);
300
+ const resolvedRelative = relative(root, target);
301
+ if (/^(?:\.\.(?:[\\/]|$))/.test(resolvedRelative) || isAbsolute(resolvedRelative)) {
302
+ throw Object.assign(new Error('managed provider entry escapes Crew root'), { code: 'PROVIDER_DELETE_UNSAFE_PATH' });
303
+ }
304
+ // Validate every ancestor but intentionally do not follow or reject the
305
+ // leaf: quarantine must be able to move a symlink/junction entry itself.
306
+ assertManagedPath(dirname(target), managedRoot);
307
+ }
308
+
309
+ export function readProviderDeleteManifestFile(file, managedRoot) {
310
+ return readManagedJsonSnapshot(file, managedRoot);
311
+ }
312
+
313
+ function validatePaths(paths) {
314
+ for (const key of FILE_KEYS) {
315
+ if (typeof paths[key] !== 'string' || !paths[key].trim()) {
316
+ throw new TypeError(`provider delete ${key} path is required`);
317
+ }
318
+ }
319
+ if (paths.settings != null && (typeof paths.settings !== 'string' || !paths.settings.trim())) {
320
+ throw new TypeError('provider delete settings path is required');
321
+ }
322
+ }
323
+
324
+ function validatePlanAuthorities(plan) {
325
+ const providerId = typeof plan?.provider_id === 'string' ? plan.provider_id : '';
326
+ const authorities = Array.isArray(plan?.declaration_authorities) ? plan.declaration_authorities : [];
327
+ if (!providerId || authorities.length === 0) throw Object.assign(new Error('provider declaration authority is unavailable'), { code: 'PROVIDER_DELETE_SOURCE_UNRESOLVED' });
328
+ const expected = {
329
+ 'crew-profile': `llm-pi-ai.config.providers.${providerId}`,
330
+ 'harness-settings': `llm-pi-ai.providers.${providerId}`,
331
+ };
332
+ for (const authority of authorities) {
333
+ if (typeof expected[authority?.kind] !== 'string' || authority.locator !== expected[authority.kind]) {
334
+ throw Object.assign(new Error('provider declaration authority locator is invalid'), { code: 'PROVIDER_DELETE_SOURCE_UNRESOLVED' });
335
+ }
336
+ }
337
+ if (plan.was_harness_default === true && (plan.harness_default_authority?.kind !== 'harness-settings' || plan.harness_default_authority.locator !== 'agent-default-model')) {
338
+ throw Object.assign(new Error('Harness Default authority is unavailable'), { code: 'PROVIDER_DEFAULT_AUTHORITY_UNAVAILABLE' });
339
+ }
340
+ }
341
+
342
+ function backupInvalid(message = 'provider backup is invalid') {
343
+ return Object.assign(new Error(message), { code: 'PROVIDER_DELETE_BACKUP_INVALID' });
344
+ }
345
+
346
+ function validRevision(value) {
347
+ return value === null || (typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value));
348
+ }
349
+
350
+ function fileIdentity(file) {
351
+ const stat = lstatSync(file, { bigint: true });
352
+ return { dev: String(stat.dev), ino: String(stat.ino) };
353
+ }
354
+
355
+ function sameFileIdentity(left, right) {
356
+ return !!left && !!right && left.dev === right.dev && left.ino === right.ino;
357
+ }
358
+
359
+ function recoveryDescriptor(manifest) {
360
+ return {
361
+ plan: {
362
+ plan_id: manifest?.plan?.plan_id ?? null,
363
+ provider_id: manifest?.plan?.provider_id ?? null,
364
+ declaration_authorities: manifest?.plan?.declaration_authorities ?? null,
365
+ was_harness_default: manifest?.plan?.was_harness_default ?? null,
366
+ harness_default_authority: manifest?.plan?.harness_default_authority ?? null,
367
+ replacement_default: manifest?.plan?.replacement_default ?? null,
368
+ replacement_default_model: manifest?.plan?.replacement_default_model ?? null,
369
+ harness_default_before: manifest?.plan?.harness_default_before ?? null,
370
+ runtime_id_before: manifest?.plan?.runtime_id_before ?? null,
371
+ delete_runtime_id_before_restart: manifest?.plan?.delete_runtime_id_before_restart ?? null,
372
+ rollback_runtime_id_before_restart: manifest?.plan?.rollback_runtime_id_before_restart ?? null,
373
+ expected_revision: manifest?.plan?.expected_revision ?? null,
374
+ expected_revisions: manifest?.plan?.expected_revisions ?? null,
375
+ credential_refs: manifest?.plan?.credential_refs ?? null,
376
+ },
377
+ files: manifest?.files ?? null,
378
+ backup_digests: manifest?.backup_digests ?? null,
379
+ profile_revision: manifest?.profile_revision ?? null,
380
+ settings_revision: manifest?.settings_revision ?? null,
381
+ config_revision: manifest?.config_revision ?? null,
382
+ lifecycle_revision: manifest?.lifecycle_revision ?? null,
383
+ routing_projection_digest: manifest?.routing_projection_digest ?? null,
384
+ lifecycle_projection_digest: manifest?.lifecycle_projection_digest ?? null,
385
+ applied_revisions: {
386
+ profile: manifest?.applied_profile_revision ?? null,
387
+ settings: manifest?.applied_settings_revision ?? null,
388
+ config: manifest?.applied_config_revision ?? null,
389
+ lifecycle: manifest?.applied_lifecycle_revision ?? null,
390
+ },
391
+ created: {
392
+ profile: manifest?.created_profile ?? false,
393
+ settings: manifest?.created_settings ?? false,
394
+ config: manifest?.created_config ?? false,
395
+ lifecycle: manifest?.created_lifecycle ?? false,
396
+ },
397
+ created_identities: {
398
+ profile: manifest?.created_profile_identity ?? null,
399
+ settings: manifest?.created_settings_identity ?? null,
400
+ config: manifest?.created_config_identity ?? null,
401
+ lifecycle: manifest?.created_lifecycle_identity ?? null,
402
+ },
403
+ ownership_witnesses: manifest?.ownership_witnesses ?? null,
404
+ mutation_journal: manifest?.mutation_journal ?? null,
405
+ phase_journal: manifest?.phase_journal ?? null,
406
+ };
407
+ }
408
+
409
+ function validateBackupManifest(manifest, { root, fs, paths, managedRoot, expectedProviderId }) {
410
+ const backupId = basename(root);
411
+ if (!manifest || manifest.schema_version !== 1 || !validProviderId(manifest.provider_id)
412
+ || (expectedProviderId && manifest.provider_id !== expectedProviderId)
413
+ || !manifest.plan || manifest.plan.plan_id !== backupId
414
+ || !safePlanId(manifest.plan.plan_id) || manifest.plan.provider_id !== manifest.provider_id
415
+ || !Array.isArray(manifest.plan.declaration_authorities)
416
+ || !manifest.files || typeof manifest.files !== 'object' || Array.isArray(manifest.files)
417
+ || !manifest.backup_digests || typeof manifest.backup_digests !== 'object' || Array.isArray(manifest.backup_digests)
418
+ || typeof manifest.recovery_descriptor_digest !== 'string' || !/^[a-f0-9]{64}$/i.test(manifest.recovery_descriptor_digest)
419
+ || sha256(JSON.stringify(recoveryDescriptor(manifest))) !== manifest.recovery_descriptor_digest) {
420
+ throw backupInvalid();
421
+ }
422
+ try {
423
+ validatePlanAuthorities(manifest.plan);
424
+ } catch {
425
+ throw backupInvalid();
426
+ }
427
+ if (manifest.plan.expected_revision !== null && !/^[a-f0-9]{64}$/i.test(manifest.plan.expected_revision)) throw backupInvalid();
428
+ if (manifest.plan.expected_revisions !== undefined) {
429
+ if (!manifest.plan.expected_revisions || typeof manifest.plan.expected_revisions !== 'object' || Array.isArray(manifest.plan.expected_revisions)
430
+ || !validRevision(manifest.plan.expected_revisions.profile) || !validRevision(manifest.plan.expected_revisions.settings)) throw backupInvalid();
431
+ }
432
+ if (!Array.isArray(manifest.plan.credential_refs) || manifest.plan.credential_refs.some((ref) => !ref || typeof ref !== 'object'
433
+ || typeof ref.kind !== 'string' || typeof ref.name_or_handle !== 'string' || typeof ref.ownership !== 'string')) throw backupInvalid();
434
+ const authorityKinds = new Set(manifest.plan.declaration_authorities.map((authority) => authority?.kind));
435
+ for (const key of FILE_KEYS) {
436
+ const entry = manifest.files[key];
437
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)
438
+ || typeof entry.existed !== 'boolean' || typeof entry.managed !== 'boolean') {
439
+ throw backupInvalid();
440
+ }
441
+ const expectedManaged = key === 'profile' ? authorityKinds.has('crew-profile') : true;
442
+ if (entry.managed !== expectedManaged || (!entry.existed && manifest.backup_digests[key] !== undefined)) throw backupInvalid();
443
+ if (key === 'config' && (!manifest.config_projection || typeof manifest.config_projection !== 'object'
444
+ || Array.isArray(manifest.config_projection) || typeof manifest.config_revision !== 'string'
445
+ || !Array.isArray(manifest.config_projection.present_fields)
446
+ || !/^[a-f0-9]{64}$/i.test(manifest.config_revision)
447
+ || typeof manifest.routing_projection_digest !== 'string'
448
+ || !/^[a-f0-9]{64}$/i.test(manifest.routing_projection_digest)
449
+ || sha256(JSON.stringify(manifest.config_projection)) !== manifest.routing_projection_digest)) {
450
+ throw backupInvalid();
451
+ }
452
+ if (key === 'lifecycle' && (!validRevision(manifest.lifecycle_revision) || typeof manifest.lifecycle_revision !== 'string')) {
453
+ throw backupInvalid();
454
+ }
455
+ if (entry.existed && entry.managed && key !== 'config') {
456
+ const backupFile = join(root, `${key}.backup`);
457
+ assertManagedPath(backupFile, managedRoot);
458
+ const digest = manifest.backup_digests[key];
459
+ if (!fs.existsSync(backupFile) || lstatSync(backupFile).isSymbolicLink() || !/^[a-f0-9]{64}$/i.test(digest)) throw backupInvalid();
460
+ const backupBytes = fs.readFileSync(backupFile);
461
+ if (sha256(backupBytes) !== digest) throw backupInvalid();
462
+ if (key === 'profile' && manifest.profile_revision !== digest) throw backupInvalid();
463
+ if (key === 'lifecycle') {
464
+ const lifecycle = normalizeProviderLifecycleState(parseManagedJson(backupBytes));
465
+ if (sha256(JSON.stringify(lifecycle)) !== manifest.lifecycle_revision
466
+ || typeof manifest.lifecycle_projection_digest !== 'string'
467
+ || !/^[a-f0-9]{64}$/i.test(manifest.lifecycle_projection_digest)
468
+ || sha256(JSON.stringify(lifecycleProjection(lifecycle))) !== manifest.lifecycle_projection_digest) throw backupInvalid();
469
+ }
470
+ if (key === 'settings' && manifest.settings_revision !== digest) throw backupInvalid();
471
+ }
472
+ }
473
+ if (paths.settings) {
474
+ const entry = manifest.files.settings;
475
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)
476
+ || typeof entry.existed !== 'boolean' || typeof entry.managed !== 'boolean') throw backupInvalid();
477
+ const expectedManaged = authorityKinds.has('harness-settings') || manifest.plan.was_harness_default === true;
478
+ if (entry.managed !== expectedManaged || (!entry.existed && manifest.backup_digests.settings !== undefined)) throw backupInvalid();
479
+ if (entry.existed && entry.managed) {
480
+ const backupFile = join(root, 'settings.backup');
481
+ assertManagedPath(backupFile, managedRoot);
482
+ const digest = manifest.backup_digests.settings;
483
+ if (!fs.existsSync(backupFile) || lstatSync(backupFile).isSymbolicLink() || !/^[a-f0-9]{64}$/i.test(digest)) throw backupInvalid();
484
+ const settingsBytes = fs.readFileSync(backupFile);
485
+ if (sha256(settingsBytes) !== digest || manifest.settings_revision !== digest) throw backupInvalid();
486
+ }
487
+ }
488
+ if (manifest.profile_revision !== undefined && !/^[a-f0-9]{64}$/i.test(manifest.profile_revision)) throw backupInvalid();
489
+ if (manifest.settings_revision !== undefined && !/^[a-f0-9]{64}$/i.test(manifest.settings_revision)) throw backupInvalid();
490
+ for (const key of ['profile', 'settings', 'config', 'lifecycle']) {
491
+ const applied = manifest[`applied_${key}_revision`];
492
+ const created = manifest[`created_${key}`];
493
+ if (applied !== undefined && !/^[a-f0-9]{64}$/i.test(applied)) throw backupInvalid();
494
+ if (created !== undefined && typeof created !== 'boolean') throw backupInvalid();
495
+ const identity = manifest[`created_${key}_identity`];
496
+ if (identity !== undefined && (!identity || typeof identity !== 'object' || typeof identity.dev !== 'string' || typeof identity.ino !== 'string')) throw backupInvalid();
497
+ if (created === true && !identity) throw backupInvalid();
498
+ }
499
+ if (manifest.mutation_journal !== undefined && (typeof manifest.mutation_journal !== 'object' || Array.isArray(manifest.mutation_journal))) throw backupInvalid();
500
+ if (manifest.phase_journal !== undefined && (typeof manifest.phase_journal !== 'object' || Array.isArray(manifest.phase_journal))) throw backupInvalid();
501
+ if (manifest.ownership_witnesses !== undefined && (typeof manifest.ownership_witnesses !== 'object' || Array.isArray(manifest.ownership_witnesses))) throw backupInvalid();
502
+ for (const key of ['profile', 'settings', 'config', 'lifecycle']) {
503
+ const witnessName = manifest.ownership_witnesses?.[key];
504
+ if (witnessName !== undefined) {
505
+ if (!new RegExp(`^${key}\\.[0-9a-f-]{36}\\.ownership\\.witness$`, 'iu').test(witnessName)) throw backupInvalid();
506
+ const witnessFile = join(root, witnessName);
507
+ assertManagedPath(witnessFile, managedRoot);
508
+ if (!fs.existsSync(witnessFile) || lstatSync(witnessFile).isSymbolicLink()) throw backupInvalid();
509
+ const identity = manifest[`created_${key}_identity`];
510
+ if (manifest[`created_${key}`] !== true || !identity) throw backupInvalid();
511
+ try {
512
+ const witnessIdentity = fileIdentity(witnessFile);
513
+ if (!sameFileIdentity(witnessIdentity, identity)) throw backupInvalid();
514
+ } catch (error) {
515
+ if (error?.code === 'PROVIDER_DELETE_BACKUP_INVALID') throw error;
516
+ throw backupInvalid();
517
+ }
518
+ } else if (manifest[`created_${key}`] === true) {
519
+ throw backupInvalid();
520
+ }
521
+ }
522
+ for (const [key, mutation] of Object.entries(manifest.mutation_journal ?? {})) {
523
+ if (!['profile', 'settings', 'config', 'lifecycle'].includes(key) || !mutation || typeof mutation !== 'object'
524
+ || !/^[a-f0-9]{64}$/i.test(mutation.next_revision) || typeof mutation.created !== 'boolean') throw backupInvalid();
525
+ if (mutation.witness !== undefined) {
526
+ if (typeof mutation.witness !== 'string') throw backupInvalid();
527
+ assertManagedPath(mutation.witness, managedRoot);
528
+ if (mutation.created === true && (!fs.existsSync(mutation.witness) || lstatSync(mutation.witness).isSymbolicLink())) throw backupInvalid();
529
+ } else if (mutation.created === true) {
530
+ throw backupInvalid();
531
+ }
532
+ }
533
+ return true;
534
+ }
535
+
536
+ // Recovery discovery uses the same strict validator as an explicit reopen.
537
+ // A manifest that cannot be reopened safely is reported for manual repair and
538
+ // is never treated as an executable rollback transaction.
539
+ export function isRecoverableProviderDeleteBackup(manifest, options = {}) {
540
+ try {
541
+ validateBackupManifest(manifest, {
542
+ root: options.root,
543
+ fs: { existsSync, readFileSync, lstatSync },
544
+ paths: options.paths ?? {},
545
+ managedRoot: options.managedRoot,
546
+ expectedProviderId: options.expectedProviderId ?? null,
547
+ });
548
+ return true;
549
+ } catch {
550
+ return false;
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Build explicit adapters for executeProviderDelete.
556
+ *
557
+ * `restart` is intentionally required from the caller. The Hub process must
558
+ * not guess how its supervisor works or restart the official 3080 process.
559
+ * Without a supplied supervisor the transaction fails closed and compensates
560
+ * any writes using the managed-file backup.
561
+ */
562
+ export function createProviderDeleteFileHooks({
563
+ profileFile,
564
+ settingsFile = null,
565
+ configFile,
566
+ lifecycleFile,
567
+ backupDir,
568
+ readConfig = null,
569
+ writeConfig = (config, { managedRoot: writeRoot, replace = true } = {}) => defaultWriteConfig(configFile, config, writeRoot, replace),
570
+ writeSettings = null,
571
+ restart,
572
+ runtimeIdProvider = null,
573
+ existingBackupId = null,
574
+ expectedProviderId = null,
575
+ afterLockAcquired = null,
576
+ afterOwnedReplacePublished = null,
577
+ afterMutationJournaled = null,
578
+ fs = { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, lstatSync },
579
+ } = {}) {
580
+ if (typeof backupDir !== 'string' || !backupDir.trim()) throw new TypeError('provider delete backup directory is required');
581
+ const paths = fileMap({ profileFile, settingsFile, configFile, lifecycleFile });
582
+ validatePaths(paths);
583
+ const managedRoot = dirname(resolvePath(backupDir));
584
+ for (const key of [...FILE_KEYS, ...(settingsFile ? ['settings'] : [])]) {
585
+ if (paths[key]) assertManagedPath(paths[key], managedRoot);
586
+ }
587
+ assertManagedPath(backupDir, managedRoot);
588
+ let activeBackup = null;
589
+ let lockPath = null;
590
+ let lockOwned = false;
591
+ let lockOwnerToken = null;
592
+
593
+ const readConfigFn = typeof readConfig === 'function'
594
+ ? readConfig
595
+ : () => defaultReadConfig(configFile, managedRoot);
596
+ const createWitnessPath = (key) => {
597
+ if (!activeBackup) throw backupInvalid();
598
+ const witness = join(activeBackup.root, `.${key}.${randomUUID()}.pending.witness`);
599
+ assertManagedPath(witness, managedRoot);
600
+ return witness;
601
+ };
602
+ const publishExclusiveFile = (file, content, token = randomUUID()) => {
603
+ const stage = join(managedRoot, `.${basename(file)}.${token}.stage`);
604
+ assertManagedPath(file, managedRoot);
605
+ assertManagedPath(stage, managedRoot);
606
+ try {
607
+ fs.writeFileSync(stage, content, { flag: 'wx' });
608
+ assertManagedPath(stage, managedRoot);
609
+ linkSync(stage, file);
610
+ assertManagedPath(file, managedRoot);
611
+ } finally {
612
+ try { fs.rmSync(stage, { force: true }); } catch {}
613
+ }
614
+ };
615
+
616
+ const writeSettingsFn = typeof writeSettings === 'function' ? writeSettings : (content, { expectedRevision } = {}) => {
617
+ if (typeof settingsFile !== 'string' || !settingsFile.trim()) throw Object.assign(new Error('provider settings path is unavailable'), { code: 'PROVIDER_DELETE_SOURCE_UNRESOLVED' });
618
+ const current = readFileSync(settingsFile, 'utf8');
619
+ if (expectedRevision !== undefined && sha256(current) !== expectedRevision) throw Object.assign(new Error('provider settings changed during commit'), { code: 'PROVIDER_SETTINGS_CHANGED' });
620
+ atomicWrite(settingsFile, content, managedRoot);
621
+ };
622
+
623
+ const ensureMutationLock = () => {
624
+ if (!lockOwned || !lockPath || !lockOwnerToken) {
625
+ throw Object.assign(new Error('provider deletion write lock is unavailable'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
626
+ }
627
+ try {
628
+ assertManagedPath(lockPath, managedRoot);
629
+ const ownerPath = lstatSync(lockPath).isDirectory() ? join(lockPath, 'owner.json') : lockPath;
630
+ const owner = readJson(ownerPath, null);
631
+ if (owner?.token !== lockOwnerToken) throw new Error('provider deletion lock ownership changed');
632
+ } catch {
633
+ lockOwned = false;
634
+ lockPath = null;
635
+ lockOwnerToken = null;
636
+ throw Object.assign(new Error('provider deletion write lock is unavailable'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
637
+ }
638
+ };
639
+
640
+ const recoverLock = async ({ force = false } = {}) => {
641
+ const guardPath = join(backupDir, '.delete.reclaim.lock');
642
+ const canonicalPath = join(backupDir, '.delete.lock');
643
+ const recoveryPath = join(backupDir, '.delete.recovery.lock');
644
+ const recoveryClaimPath = `${recoveryPath}.claim`;
645
+ const alive = (owner) => {
646
+ if (!Number.isInteger(owner?.pid) || owner.pid <= 0) return null;
647
+ try { process.kill(owner.pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
648
+ };
649
+ const lockUnavailable = () => Object.assign(new Error('provider deletion lock metadata is unavailable'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
650
+ const fsLstatSync = typeof fs.lstatSync === 'function' ? fs.lstatSync : lstatSync;
651
+ const fsReadFileSync = typeof fs.readFileSync === 'function' ? fs.readFileSync : readFileSync;
652
+ const fsReaddirSync = typeof fs.readdirSync === 'function' ? fs.readdirSync : readdirSync;
653
+ const cleanup = (path) => { try { fs.rmSync(path, { recursive: true, force: true }); } catch {} };
654
+ const inspectPath = (path) => {
655
+ try { return { exists: true, stat: fsLstatSync(path) }; }
656
+ catch (error) {
657
+ if (error?.code === 'ENOENT') return { exists: false, stat: null };
658
+ throw lockUnavailable();
659
+ }
660
+ };
661
+ const readOwner = (path, knownStat = null) => {
662
+ let stat = knownStat;
663
+ if (!stat) {
664
+ const inspected = inspectPath(path);
665
+ if (!inspected.exists) return null;
666
+ stat = inspected.stat;
667
+ }
668
+ if (stat.isSymbolicLink?.()) throw lockUnavailable();
669
+ const ownerPath = stat.isDirectory() ? join(path, 'owner.json') : path;
670
+ let raw;
671
+ try { raw = fsReadFileSync(ownerPath, 'utf8'); }
672
+ catch (error) {
673
+ if (error?.code === 'ENOENT') return null;
674
+ throw lockUnavailable();
675
+ }
676
+ try {
677
+ const parsed = JSON.parse(raw);
678
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
679
+ ? parsed
680
+ : { invalid: true };
681
+ } catch {
682
+ // Explicit offline recovery may reclaim recognized malformed owner
683
+ // metadata, but it must never guess through an unreadable file.
684
+ return { invalid: true };
685
+ }
686
+ };
687
+ assertManagedPath(backupDir, managedRoot);
688
+ for (const path of [guardPath, canonicalPath, recoveryPath, recoveryClaimPath]) assertManagedPath(path, managedRoot);
689
+ const recoveryToken = randomUUID();
690
+ let recoveryOwned = false;
691
+ const recoveryOwner = () => ({ pid: process.pid, token: recoveryToken, created_at: new Date().toISOString() });
692
+ const recoveryAcquire = () => {
693
+ // A crashed reclaimer leaves a claim marker. Do not automatically steal
694
+ // it: only an explicit operator force can clear a dead claim, which
695
+ // keeps concurrent reclaimers from ABA-renaming a fresh recovery lock.
696
+ if (inspectPath(recoveryClaimPath).exists) {
697
+ const claim = readOwner(recoveryClaimPath);
698
+ if (!force || alive(claim) === true) return false;
699
+ const claimToken = claim?.token ?? null;
700
+ const rereadClaim = readOwner(recoveryClaimPath);
701
+ if (claimToken !== (rereadClaim?.token ?? null)) return false;
702
+ cleanup(recoveryClaimPath);
703
+ if (inspectPath(recoveryClaimPath).exists) return false;
704
+ }
705
+ try {
706
+ publishExclusiveFile(recoveryPath, JSON.stringify(recoveryOwner()) + '\n', recoveryToken);
707
+ recoveryOwned = true;
708
+ return true;
709
+ } catch (error) {
710
+ if (error?.code !== 'EEXIST') return false;
711
+ const existing = readOwner(recoveryPath);
712
+ if (existing && alive(existing) === true) return false;
713
+ if (inspectPath(recoveryClaimPath).exists) return false;
714
+ const claim = { pid: process.pid, token: recoveryToken, observed_token: existing?.token ?? null, created_at: new Date().toISOString() };
715
+ try {
716
+ publishExclusiveFile(recoveryClaimPath, JSON.stringify(claim) + '\n', recoveryToken);
717
+ } catch (claimError) {
718
+ if (claimError?.code === 'EEXIST') return false;
719
+ throw claimError;
720
+ }
721
+ const reread = readOwner(recoveryPath);
722
+ if ((existing?.token ?? null) !== (reread?.token ?? null)) {
723
+ cleanup(recoveryClaimPath);
724
+ return false;
725
+ }
726
+ const stalePath = `${recoveryPath}.${recoveryToken}.stale`;
727
+ try { renameSync(recoveryPath, stalePath); }
728
+ catch (renameError) {
729
+ cleanup(recoveryClaimPath);
730
+ if (renameError?.code === 'ENOENT' || renameError?.code === 'EEXIST') return false;
731
+ throw lockUnavailable();
732
+ }
733
+ try {
734
+ publishExclusiveFile(recoveryPath, JSON.stringify(recoveryOwner()) + '\n', recoveryToken);
735
+ recoveryOwned = true;
736
+ return true;
737
+ } finally {
738
+ cleanup(stalePath);
739
+ cleanup(recoveryClaimPath);
740
+ }
741
+ }
742
+ };
743
+ if (!recoveryAcquire()) return { ok: false, code: 'PROVIDER_DELETE_BUSY' };
744
+ try {
745
+ const guardExists = inspectPath(guardPath).exists;
746
+ let guardOwner = null;
747
+ if (guardExists) {
748
+ guardOwner = readOwner(guardPath);
749
+ if (alive(guardOwner) === true) return { ok: false, code: 'PROVIDER_DELETE_BUSY' };
750
+ }
751
+ const candidates = [canonicalPath];
752
+ try {
753
+ for (const entry of fsReaddirSync(backupDir, { withFileTypes: true })) {
754
+ if (/^\.delete\.lock\.[0-9a-f-]+\.(?:active|staging)$/iu.test(entry.name)) candidates.push(join(backupDir, entry.name));
755
+ }
756
+ } catch { return { ok: false, code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' }; }
757
+ const uniqueCandidates = [...new Set(candidates)];
758
+ const presentCandidates = [];
759
+ for (const candidate of uniqueCandidates) {
760
+ const inspected = inspectPath(candidate);
761
+ if (!inspected.exists) continue;
762
+ presentCandidates.push({ candidate, stat: inspected.stat, owner: readOwner(candidate, inspected.stat) });
763
+ }
764
+ if (presentCandidates.length === 0) {
765
+ if (guardExists) cleanup(guardPath);
766
+ return { ok: true, recovered: guardExists };
767
+ }
768
+ for (const { owner } of presentCandidates) if (alive(owner) === true) return { ok: false, code: 'PROVIDER_DELETE_BUSY' };
769
+ for (const { candidate } of presentCandidates) cleanup(candidate);
770
+ cleanup(guardPath);
771
+ return { ok: true, recovered: true };
772
+ } finally {
773
+ if (recoveryOwned) {
774
+ let owner = null;
775
+ try { owner = readJson(recoveryPath, null); } catch {}
776
+ if (owner?.token === recoveryToken) cleanup(recoveryPath);
777
+ }
778
+ }
779
+ };
780
+
781
+ const acquireLock = async () => {
782
+ if (lockOwned) return;
783
+ assertManagedPath(backupDir, managedRoot);
784
+ const backupParent = dirname(backupDir);
785
+ assertManagedPath(backupParent, managedRoot);
786
+ if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
787
+ assertManagedPath(backupDir, managedRoot);
788
+ const ownerToken = randomUUID();
789
+ const canonicalPath = join(backupDir, '.delete.lock');
790
+ const activePath = join(backupDir, `.delete.lock.${ownerToken}.active`);
791
+ const stagingPath = join(backupDir, `.delete.lock.${ownerToken}.staging`);
792
+ const retiredPath = join(backupDir, `.delete.lock.${ownerToken}.retired`);
793
+ const reclaimGuardPath = join(backupDir, '.delete.reclaim.lock');
794
+ const recoveryPath = join(backupDir, '.delete.recovery.lock');
795
+ const recoveryClaimPath = `${recoveryPath}.claim`;
796
+ let guardOwned = false;
797
+ let coordinationOwned = false;
798
+ for (const path of [canonicalPath, activePath, stagingPath, retiredPath, reclaimGuardPath, recoveryPath, recoveryClaimPath]) assertManagedPath(path, managedRoot);
799
+ try {
800
+ if (existsSync(recoveryClaimPath)) throw Object.assign(new Error('provider delete recovery reclaim is active'), { code: 'PROVIDER_DELETE_BUSY' });
801
+ publishExclusiveFile(recoveryPath, JSON.stringify({ pid: process.pid, token: ownerToken, created_at: new Date().toISOString() }) + '\n', ownerToken);
802
+ coordinationOwned = true;
803
+ } catch (error) {
804
+ if (error?.code === 'EEXIST') throw Object.assign(new Error('provider delete recovery is active'), { code: 'PROVIDER_DELETE_BUSY' });
805
+ throw Object.assign(new Error('provider delete recovery lock unavailable'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
806
+ }
807
+ try {
808
+ publishExclusiveFile(reclaimGuardPath, JSON.stringify({ pid: process.pid, token: ownerToken, created_at: new Date().toISOString() }) + '\n', ownerToken);
809
+ guardOwned = true;
810
+ } catch (error) {
811
+ if (coordinationOwned) {
812
+ try { fs.rmSync(recoveryPath, { force: true }); } catch {}
813
+ coordinationOwned = false;
814
+ }
815
+ if (error?.code === 'EEXIST') throw Object.assign(new Error('another provider deletion is reclaiming the lock'), { code: 'PROVIDER_DELETE_BUSY' });
816
+ throw Object.assign(new Error('provider delete lock reclaim unavailable'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
817
+ }
818
+ const ownerIsAlive = (owner) => {
819
+ if (!Number.isInteger(owner?.pid) || owner.pid <= 0) return null;
820
+ try { process.kill(owner.pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
821
+ };
822
+ const lockUnavailable = () => Object.assign(new Error('provider deletion lock metadata is unavailable'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
823
+ const fsReaddirSync = typeof fs.readdirSync === 'function' ? fs.readdirSync : readdirSync;
824
+ const fsLstatSync = typeof fs.lstatSync === 'function' ? fs.lstatSync : lstatSync;
825
+ const fsReadFileSync = typeof fs.readFileSync === 'function' ? fs.readFileSync : readFileSync;
826
+ const lockCandidates = () => {
827
+ try {
828
+ return fsReaddirSync(backupDir, { withFileTypes: true })
829
+ .filter((entry) => entry.name === '.delete.lock' || /^\.delete\.lock\.[0-9a-f-]+\.(?:active|staging)$/iu.test(entry.name))
830
+ .map((entry) => join(backupDir, entry.name));
831
+ } catch { throw lockUnavailable(); }
832
+ };
833
+ const ownerRecord = () => JSON.stringify({
834
+ pid: process.pid,
835
+ token: ownerToken,
836
+ ...(typeof runtimeIdProvider === 'function' ? { runtime_id: runtimeIdProvider() } : {}),
837
+ created_at: new Date().toISOString(),
838
+ }) + '\n';
839
+ const readOwner = (path, knownStat = null) => {
840
+ let stat = knownStat;
841
+ if (!stat) {
842
+ try { stat = fsLstatSync(path); }
843
+ catch (error) {
844
+ if (error?.code === 'ENOENT') return null;
845
+ throw lockUnavailable();
846
+ }
847
+ }
848
+ if (stat.isSymbolicLink?.()) throw lockUnavailable();
849
+ const ownerPath = stat.isDirectory() ? join(path, 'owner.json') : path;
850
+ let raw;
851
+ try { raw = fsReadFileSync(ownerPath, 'utf8'); }
852
+ catch (error) {
853
+ if (error?.code === 'ENOENT') return null;
854
+ throw lockUnavailable();
855
+ }
856
+ try {
857
+ const parsed = JSON.parse(raw);
858
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
859
+ ? parsed
860
+ : { invalid: true };
861
+ }
862
+ catch {
863
+ // A present but malformed owner record is not an orphan. Normal
864
+ // acquisition must fail closed; only explicit offline recovery may
865
+ // reclaim malformed metadata.
866
+ return { invalid: true };
867
+ }
868
+ };
869
+ const inspectCandidate = (path) => {
870
+ let stat;
871
+ try { stat = fsLstatSync(path); }
872
+ catch (error) {
873
+ if (error?.code === 'ENOENT') return { exists: false, owner: null };
874
+ throw lockUnavailable();
875
+ }
876
+ return { exists: true, owner: readOwner(path, stat) };
877
+ };
878
+ const writeStagingOwner = () => {
879
+ fs.writeFileSync(stagingPath, ownerRecord(), { flag: 'wx' });
880
+ const owner = readOwner(stagingPath);
881
+ if (owner?.token !== ownerToken) throw Object.assign(new Error('provider delete owner metadata was not persisted'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
882
+ };
883
+ const adoptPrepared = (path) => {
884
+ assertManagedPath(path, managedRoot);
885
+ const owner = readOwner(path);
886
+ if (owner?.token !== ownerToken) throw Object.assign(new Error('provider delete owner metadata was not persisted'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
887
+ lockPath = path;
888
+ lockOwnerToken = ownerToken;
889
+ lockOwned = true;
890
+ try {
891
+ // Reopen transactions are inspected before lock acquisition so
892
+ // malformed input can fail fast. Refresh once ownership is acquired
893
+ // so a stale in-memory manifest cannot overwrite newer recovery state.
894
+ refreshActiveBackup();
895
+ if (typeof afterLockAcquired === 'function') afterLockAcquired(activeBackup?.manifest ?? null);
896
+ } catch (error) {
897
+ cleanup(path);
898
+ lockOwned = false;
899
+ lockPath = null;
900
+ lockOwnerToken = null;
901
+ throw error;
902
+ }
903
+ };
904
+ const cleanup = (path) => { try { fs.rmSync(path, { recursive: true, force: true }); } catch {} };
905
+ try {
906
+ while (true) {
907
+ const candidates = lockCandidates();
908
+ if (candidates.length === 0) {
909
+ try {
910
+ writeStagingOwner();
911
+ renameSync(stagingPath, canonicalPath);
912
+ adoptPrepared(canonicalPath);
913
+ return;
914
+ } catch (error) {
915
+ cleanup(stagingPath);
916
+ if (error?.code === 'EEXIST' || error?.code === 'ENOENT') continue;
917
+ if (error?.code === 'PROVIDER_DELETE_RECOVERY_PENDING') throw error;
918
+ throw Object.assign(new Error('provider deletion lock unavailable'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
919
+ }
920
+ }
921
+ const uniqueCandidates = [...new Set(candidates)];
922
+ // Inspect every residue before reclaiming any one of them. A stale
923
+ // candidate may appear before a live token-specific owner in
924
+ // directory order; reclaiming it first would otherwise admit a second
925
+ // owner while the live candidate remains untouched.
926
+ for (const candidate of uniqueCandidates) {
927
+ const inspected = inspectCandidate(candidate);
928
+ if (!inspected.exists) continue;
929
+ const owner = inspected.owner;
930
+ if (owner && ownerIsAlive(owner) !== false) {
931
+ throw Object.assign(new Error('another provider deletion is active'), { code: 'PROVIDER_DELETE_BUSY' });
932
+ }
933
+ }
934
+ let raced = false;
935
+ for (const candidate of uniqueCandidates) {
936
+ const inspected = inspectCandidate(candidate);
937
+ if (!inspected.exists) continue;
938
+ const owner = inspected.owner;
939
+ if (owner && ownerIsAlive(owner) !== false) throw Object.assign(new Error('another provider deletion is active'), { code: 'PROVIDER_DELETE_BUSY' });
940
+ try {
941
+ writeStagingOwner();
942
+ renameSync(candidate, retiredPath);
943
+ renameSync(stagingPath, activePath);
944
+ cleanup(retiredPath);
945
+ adoptPrepared(activePath);
946
+ return;
947
+ } catch (error) {
948
+ cleanup(stagingPath);
949
+ if (error?.code === 'ENOENT' || error?.code === 'EEXIST') { raced = true; break; }
950
+ if (error?.code === 'PROVIDER_DELETE_RECOVERY_PENDING') throw error;
951
+ throw Object.assign(new Error('provider deletion lock reclaim unavailable'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
952
+ }
953
+ }
954
+ if (!raced) continue;
955
+ }
956
+ } catch (error) {
957
+ cleanup(stagingPath);
958
+ throw error;
959
+ } finally {
960
+ if (guardOwned) {
961
+ let owner = null;
962
+ try { owner = readJson(reclaimGuardPath, null); } catch {}
963
+ if (owner?.token === ownerToken) cleanup(reclaimGuardPath);
964
+ }
965
+ if (coordinationOwned) {
966
+ let owner = null;
967
+ try { owner = readJson(recoveryPath, null); } catch {}
968
+ if (owner?.token === ownerToken) cleanup(recoveryPath);
969
+ }
970
+ }
971
+ };
972
+
973
+ if (existingBackupId !== null) {
974
+ const backupId = safePlanId(existingBackupId);
975
+ if (!backupId) throw Object.assign(new Error('provider backup id is invalid'), { code: 'PROVIDER_DELETE_PLAN_INVALID' });
976
+ const root = join(backupDir, backupId);
977
+ assertManagedPath(root, managedRoot);
978
+ const manifest = readManagedJsonSnapshot(join(root, 'manifest.json'), managedRoot, fs);
979
+ try {
980
+ validateBackupManifest(manifest, { root, fs, paths, managedRoot, expectedProviderId });
981
+ } catch (error) {
982
+ if (error?.code === 'PROVIDER_DELETE_BACKUP_INVALID') throw error;
983
+ throw backupInvalid();
984
+ }
985
+ activeBackup = { root, manifest };
986
+ }
987
+
988
+ const refreshActiveBackup = () => {
989
+ if (!activeBackup) return;
990
+ const manifestFile = join(activeBackup.root, 'manifest.json');
991
+ const latest = readManagedJsonSnapshot(manifestFile, managedRoot, fs);
992
+ validateBackupManifest(latest, { root: activeBackup.root, fs, paths, managedRoot, expectedProviderId });
993
+ activeBackup = { root: activeBackup.root, manifest: latest };
994
+ };
995
+
996
+ const persistManifest = () => {
997
+ if (!activeBackup) return;
998
+ ensureMutationLock();
999
+ activeBackup.manifest.recovery_descriptor_digest = sha256(JSON.stringify(recoveryDescriptor(activeBackup.manifest)));
1000
+ const manifestFile = join(activeBackup.root, 'manifest.json');
1001
+ assertManagedPath(manifestFile, managedRoot);
1002
+ atomicWrite(manifestFile, JSON.stringify(activeBackup.manifest, null, 2) + '\n', managedRoot);
1003
+ };
1004
+
1005
+ const readVerifiedBackup = (key) => {
1006
+ const entry = activeBackup?.manifest?.files?.[key];
1007
+ if (!entry?.existed || !entry?.managed || key === 'config') return null;
1008
+ const backupFile = join(activeBackup.root, key === 'settings' ? 'settings.backup' : `${key}.backup`);
1009
+ const digest = activeBackup.manifest.backup_digests?.[key];
1010
+ assertManagedPath(backupFile, managedRoot);
1011
+ if (!/^[a-f0-9]{64}$/i.test(digest) || !fs.existsSync(backupFile) || lstatSync(backupFile).isSymbolicLink()) throw backupInvalid();
1012
+ const content = fs.readFileSync(backupFile);
1013
+ if (sha256(content) !== digest) throw backupInvalid();
1014
+ return content;
1015
+ };
1016
+
1017
+ const assertRecoveryDescriptor = () => {
1018
+ const digest = activeBackup?.manifest?.recovery_descriptor_digest;
1019
+ if (!/^[a-f0-9]{64}$/i.test(digest) || sha256(JSON.stringify(recoveryDescriptor(activeBackup.manifest))) !== digest) throw backupInvalid();
1020
+ };
1021
+
1022
+ const prepareMutation = (key, nextRevision, details = {}) => {
1023
+ if (!activeBackup || typeof nextRevision !== 'string') return;
1024
+ const created = typeof details.created === 'boolean'
1025
+ ? details.created
1026
+ : activeBackup.manifest.files?.[key]?.existed === false;
1027
+ let prepublished = false;
1028
+ if (created && typeof details.witness === 'string' && details.witness_content !== undefined && !existsSync(details.witness)) {
1029
+ writeFileSync(details.witness, details.witness_content, { flag: 'wx' });
1030
+ prepublished = true;
1031
+ }
1032
+ activeBackup.manifest.mutation_journal ??= {};
1033
+ activeBackup.manifest.mutation_journal[key] = {
1034
+ next_revision: nextRevision,
1035
+ created,
1036
+ ...(typeof details.witness === 'string' ? { witness: details.witness } : {}),
1037
+ prepared_at: new Date().toISOString(),
1038
+ };
1039
+ try { persistManifest(); }
1040
+ catch (error) {
1041
+ if (prepublished) { try { rmSync(details.witness, { force: true }); } catch {} }
1042
+ throw error;
1043
+ }
1044
+ const journalCallback = typeof details.after_journaled === 'function' ? details.after_journaled : afterMutationJournaled;
1045
+ if (typeof journalCallback === 'function') journalCallback(key);
1046
+ };
1047
+
1048
+ const durableWitnessPath = (key) => {
1049
+ if (!activeBackup) throw backupInvalid();
1050
+ const witness = join(activeBackup.root, `${key}.${randomUUID()}.ownership.witness`);
1051
+ assertManagedPath(witness, managedRoot);
1052
+ return witness;
1053
+ };
1054
+
1055
+ const retainOwnershipWitness = (key, source) => {
1056
+ const witness = durableWitnessPath(key);
1057
+ const previous = activeBackup.manifest.ownership_witnesses?.[key];
1058
+ try {
1059
+ linkSync(source, witness);
1060
+ const sourceIdentity = fileIdentity(source);
1061
+ const witnessIdentity = fileIdentity(witness);
1062
+ if (!sameFileIdentity(sourceIdentity, witnessIdentity)) {
1063
+ throw Object.assign(new Error('transaction-created file ownership witness changed'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
1064
+ }
1065
+ activeBackup.manifest.ownership_witnesses ??= {};
1066
+ activeBackup.manifest.ownership_witnesses[key] = basename(witness);
1067
+ return { identity: witnessIdentity, previous };
1068
+ } catch (error) {
1069
+ try { rmSync(witness, { force: true }); } catch {}
1070
+ throw error;
1071
+ }
1072
+ };
1073
+
1074
+ const commitMutation = (key, nextRevision) => {
1075
+ if (!activeBackup || typeof nextRevision !== 'string') return;
1076
+ const pending = activeBackup.manifest.mutation_journal?.[key];
1077
+ let previousWitness = null;
1078
+ if (pending?.created === true) {
1079
+ const target = key === 'settings' ? settingsFile : paths[key];
1080
+ const witness = pending.witness;
1081
+ if (typeof target !== 'string' || typeof witness !== 'string' || !existsSync(target) || !existsSync(witness)) {
1082
+ throw Object.assign(new Error('transaction-created file ownership witness is missing'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
1083
+ }
1084
+ let targetIdentity;
1085
+ let witnessIdentity;
1086
+ try {
1087
+ targetIdentity = fileIdentity(target);
1088
+ witnessIdentity = fileIdentity(witness);
1089
+ } catch {
1090
+ throw Object.assign(new Error('transaction-created file identity is unavailable'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
1091
+ }
1092
+ if (!sameFileIdentity(targetIdentity, witnessIdentity)) {
1093
+ throw Object.assign(new Error('transaction-created file ownership changed'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
1094
+ }
1095
+ activeBackup.manifest[`created_${key}`] = true;
1096
+ const retained = retainOwnershipWitness(key, witness);
1097
+ activeBackup.manifest[`created_${key}_identity`] = retained.identity;
1098
+ previousWitness = retained.previous;
1099
+ } else if (activeBackup.manifest[`created_${key}`] === true) {
1100
+ throw Object.assign(new Error('transaction-created file replacement lacks a durable witness'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
1101
+ }
1102
+ activeBackup.manifest[`applied_${key}_revision`] = nextRevision;
1103
+ const witness = pending?.witness;
1104
+ if (activeBackup.manifest.mutation_journal) delete activeBackup.manifest.mutation_journal[key];
1105
+ persistManifest();
1106
+ if (typeof previousWitness === 'string') {
1107
+ try { rmSync(join(activeBackup.root, previousWitness), { force: true }); } catch {}
1108
+ }
1109
+ if (typeof witness === 'string') {
1110
+ try { rmSync(witness, { force: true }); } catch {}
1111
+ }
1112
+ };
1113
+
1114
+ const setPhase = (phase, details = {}) => {
1115
+ if (!activeBackup) return;
1116
+ activeBackup.manifest.phase_journal = { phase, ...details, updated_at: new Date().toISOString() };
1117
+ persistManifest();
1118
+ };
1119
+
1120
+ // Every managed writer runs while the transaction lock is held and verifies
1121
+ // both the target boundary and the resulting revision. Custom writers are
1122
+ // accepted for the Hub's in-memory adapters, but cannot bypass these checks.
1123
+ const writeManagedConfig = async (config, expectedRevision, witness = null) => {
1124
+ ensureMutationLock();
1125
+ assertManagedPath(configFile, managedRoot);
1126
+ if (activeBackup?.manifest?.files?.config?.existed === false && fs.existsSync(configFile)) {
1127
+ throw Object.assign(new Error('provider config was created before the transaction write'), { code: 'PROVIDER_CONFIG_CHANGED' });
1128
+ }
1129
+ if (typeof expectedRevision === 'string') {
1130
+ const current = await readConfigFn();
1131
+ if (sha256(JSON.stringify(current)) !== expectedRevision) {
1132
+ throw Object.assign(new Error('provider config changed during commit'), { code: 'PROVIDER_CONFIG_CHANGED' });
1133
+ }
1134
+ }
1135
+ const createOnly = activeBackup?.manifest?.files?.config?.existed === false;
1136
+ try {
1137
+ if (createOnly) atomicCreateWithWitness(configFile, JSON.stringify(config, null, 2) + '\n', managedRoot, witness ?? createWitnessPath('config'));
1138
+ else await writeConfig(config, { expectedRevision, managedRoot, replace: true });
1139
+ } catch (error) {
1140
+ if (createOnly && error?.code === 'EEXIST') throw Object.assign(new Error('provider config was created before the transaction write'), { code: 'PROVIDER_CONFIG_CHANGED' });
1141
+ throw error;
1142
+ }
1143
+ assertManagedPath(configFile, managedRoot);
1144
+ const written = await readConfigFn();
1145
+ if (sha256(JSON.stringify(written)) !== sha256(JSON.stringify(config))) {
1146
+ throw Object.assign(new Error('provider config write could not be verified'), { code: 'PROVIDER_CONFIG_WRITE_UNVERIFIED' });
1147
+ }
1148
+ };
1149
+
1150
+ const writeManagedSettings = async (content, options = {}) => {
1151
+ ensureMutationLock();
1152
+ if (typeof settingsFile !== 'string' || !settingsFile.trim()) {
1153
+ throw Object.assign(new Error('provider settings path is unavailable'), { code: 'PROVIDER_DELETE_SOURCE_UNRESOLVED' });
1154
+ }
1155
+ assertManagedPath(settingsFile, managedRoot);
1156
+ const current = fs.readFileSync(settingsFile, 'utf8');
1157
+ if (typeof options.expectedRevision === 'string' && sha256(current) !== options.expectedRevision) {
1158
+ throw Object.assign(new Error('provider settings changed during commit'), { code: 'PROVIDER_SETTINGS_CHANGED' });
1159
+ }
1160
+ await writeSettingsFn(content, { ...options, managedRoot });
1161
+ assertManagedPath(settingsFile, managedRoot);
1162
+ if (!fs.existsSync(settingsFile) || sha256(fs.readFileSync(settingsFile, 'utf8')) !== sha256(content)) {
1163
+ throw Object.assign(new Error('provider settings write could not be verified'), { code: 'PROVIDER_SETTINGS_WRITE_UNVERIFIED' });
1164
+ }
1165
+ };
1166
+
1167
+ const backup = async (plan) => {
1168
+ const planId = safePlanId(plan?.plan_id);
1169
+ if (!planId) throw Object.assign(new Error('provider delete plan id is invalid'), { code: 'PROVIDER_DELETE_PLAN_INVALID' });
1170
+ validatePlanAuthorities(plan);
1171
+ await acquireLock();
1172
+ const root = join(backupDir, planId);
1173
+ assertManagedPath(root, managedRoot);
1174
+ if (fs.existsSync(root)) throw backupInvalid('provider backup id already exists');
1175
+ fs.mkdirSync(root);
1176
+ assertManagedPath(root, managedRoot);
1177
+ const manifest = {
1178
+ schema_version: 1,
1179
+ provider_id: plan.provider_id,
1180
+ plan: {
1181
+ plan_id: plan.plan_id,
1182
+ provider_id: plan.provider_id,
1183
+ expected_revision: plan.expected_revision ?? null,
1184
+ replacement_default: plan.replacement_default ?? null,
1185
+ was_harness_default: plan.was_harness_default === true,
1186
+ replacement_default_model: plan.replacement_default_model ?? null,
1187
+ ...(plan.harness_default_before && typeof plan.harness_default_before === 'object' ? { harness_default_before: {
1188
+ provider: typeof plan.harness_default_before.provider === 'string' ? plan.harness_default_before.provider.trim() : null,
1189
+ model: typeof plan.harness_default_before.model === 'string' ? plan.harness_default_before.model.trim() : null,
1190
+ } } : {}),
1191
+ ...(plan.harness_default_authority && typeof plan.harness_default_authority === 'object' ? { harness_default_authority: {
1192
+ kind: typeof plan.harness_default_authority.kind === 'string' ? plan.harness_default_authority.kind.trim() : 'unknown',
1193
+ locator: typeof plan.harness_default_authority.locator === 'string' ? plan.harness_default_authority.locator.trim() : 'unknown',
1194
+ } } : {}),
1195
+ ...(typeof plan.runtime_id_before === 'string' && plan.runtime_id_before.trim() ? { runtime_id_before: plan.runtime_id_before.trim().slice(0, 128) } : {}),
1196
+ ...(typeof plan.delete_runtime_id_before_restart === 'string' && plan.delete_runtime_id_before_restart.trim() ? { delete_runtime_id_before_restart: plan.delete_runtime_id_before_restart.trim().slice(0, 128) } : {}),
1197
+ declaration_authorities: (Array.isArray(plan.declaration_authorities) ? plan.declaration_authorities : []).map((authority) => ({
1198
+ kind: typeof authority?.kind === 'string' ? authority.kind.trim() : 'unknown',
1199
+ locator: typeof authority?.locator === 'string' ? authority.locator.trim() : 'unknown',
1200
+ })).filter((authority) => authority.kind && authority.locator).slice(0, 16),
1201
+ ...(plan.expected_revisions && typeof plan.expected_revisions === 'object' ? { expected_revisions: {
1202
+ profile: typeof plan.expected_revisions.profile === 'string' ? plan.expected_revisions.profile : null,
1203
+ settings: typeof plan.expected_revisions.settings === 'string' ? plan.expected_revisions.settings : null,
1204
+ } } : {}),
1205
+ credential_refs: (Array.isArray(plan.credential_refs) ? plan.credential_refs : []).map((ref) => ({
1206
+ kind: typeof ref?.kind === 'string' ? ref.kind.trim() : 'unknown',
1207
+ name_or_handle: typeof ref?.name_or_handle === 'string' ? ref.name_or_handle.trim() : 'unknown',
1208
+ ownership: typeof ref?.ownership === 'string' ? ref.ownership.trim() : 'unknown',
1209
+ })).filter((ref) => ref.kind && ref.name_or_handle && ref.ownership).slice(0, 32),
1210
+ },
1211
+ files: {},
1212
+ backup_digests: {},
1213
+ ownership_witnesses: {},
1214
+ mutation_journal: {},
1215
+ phase_journal: { phase: 'PLANNED', updated_at: new Date().toISOString() },
1216
+ };
1217
+ const authorityKinds = new Set((Array.isArray(plan?.declaration_authorities) ? plan.declaration_authorities : []).map((authority) => authority?.kind));
1218
+ const managesHarnessDefault = plan?.was_harness_default === true && plan?.harness_default_authority?.kind === 'harness-settings';
1219
+ const sourceRevisions = {};
1220
+ let configBytes = null;
1221
+ try { configBytes = fs.readFileSync(configFile); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
1222
+ const configExisted = configBytes !== null;
1223
+ const configSnapshot = configExisted ? parseManagedJson(configBytes) : {};
1224
+ let lifecycleBytes = null;
1225
+ try { lifecycleBytes = fs.readFileSync(lifecycleFile); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
1226
+ const lifecycleExisted = lifecycleBytes !== null;
1227
+ const lifecycleSnapshot = lifecycleExisted
1228
+ ? normalizeProviderLifecycleState(parseManagedJson(lifecycleBytes))
1229
+ : normalizeProviderLifecycleState({});
1230
+ assertManagedPath(lifecycleFile, managedRoot);
1231
+ for (const key of FILE_KEYS) {
1232
+ const source = paths[key];
1233
+ assertManagedPath(source, managedRoot);
1234
+ const existed = key === 'config' ? configExisted : key === 'lifecycle' ? lifecycleExisted : fs.existsSync(source);
1235
+ const managed = key !== 'profile' || authorityKinds.has('crew-profile');
1236
+ manifest.files[key] = { existed, managed };
1237
+ if (key === 'profile' && authorityKinds.has('crew-profile') && !existed) throw Object.assign(new Error('provider profile authority is missing'), { code: 'PROVIDER_DELETE_SOURCE_UNRESOLVED' });
1238
+ if (existed && key !== 'config' && managed) {
1239
+ const sourceBytes = key === 'lifecycle' ? lifecycleBytes : fs.readFileSync(source);
1240
+ const sourceText = decodeUtf8(sourceBytes);
1241
+ // The backup contains this exact snapshot, never a second pathname
1242
+ // read after a possible ancestor/junction swap.
1243
+ assertManagedPath(source, managedRoot);
1244
+ const expectedSourceRevision = key === 'profile'
1245
+ ? (plan.expected_revisions?.profile ?? plan.expected_revision)
1246
+ : plan.expected_revisions?.lifecycle;
1247
+ if (typeof expectedSourceRevision === 'string' && sha256(sourceText) !== expectedSourceRevision) {
1248
+ throw Object.assign(new Error(`${key} changed before backup`), { code: key === 'profile' ? 'PROVIDER_PROFILE_CHANGED' : 'PROVIDER_DELETE_STATE_CHANGED' });
1249
+ }
1250
+ if (key === 'profile') {
1251
+ const credentials = hasInlineProfileCredentials(sourceText);
1252
+ if (credentials.ok !== true) throw Object.assign(new Error('provider profile credential shape is unsupported'), { code: 'PROVIDER_DELETE_FILE_INVALID' });
1253
+ if (credentials.inline === true) throw Object.assign(new Error('inline provider credential is not supported'), { code: 'PROVIDER_INLINE_CREDENTIAL_UNSUPPORTED' });
1254
+ }
1255
+ const target = join(root, `${key}.backup`);
1256
+ assertManagedPath(target, managedRoot);
1257
+ atomicWrite(target, sourceBytes, managedRoot);
1258
+ manifest.backup_digests[key] = sha256(sourceBytes);
1259
+ sourceRevisions[key] = sha256(sourceBytes);
1260
+ }
1261
+ }
1262
+ if (typeof settingsFile === 'string' && settingsFile.trim()) {
1263
+ assertManagedPath(settingsFile, managedRoot);
1264
+ const existed = fs.existsSync(settingsFile);
1265
+ const managed = authorityKinds.has('harness-settings') || managesHarnessDefault;
1266
+ manifest.files.settings = { existed, managed };
1267
+ if ((authorityKinds.has('harness-settings') || managesHarnessDefault) && !existed) throw Object.assign(new Error('provider settings authority is missing'), { code: 'PROVIDER_DELETE_SOURCE_UNRESOLVED' });
1268
+ if (existed && managed) {
1269
+ const sourceBytes = fs.readFileSync(settingsFile);
1270
+ const sourceText = decodeUtf8(sourceBytes);
1271
+ assertManagedPath(settingsFile, managedRoot);
1272
+ const expectedSettingsRevision = plan.expected_revisions?.settings ?? (!authorityKinds.has('crew-profile') ? plan.expected_revision : null);
1273
+ if (typeof expectedSettingsRevision === 'string' && sha256(sourceText) !== expectedSettingsRevision) {
1274
+ throw Object.assign(new Error('provider settings changed before backup'), { code: 'PROVIDER_SETTINGS_CHANGED' });
1275
+ }
1276
+ const credentials = hasInlineSettingsCredentials(sourceText);
1277
+ if (credentials.ok !== true) throw Object.assign(new Error('provider settings credential shape is unsupported'), { code: 'PROVIDER_DELETE_FILE_INVALID' });
1278
+ if (credentials.inline === true) throw Object.assign(new Error('inline provider credential is not supported'), { code: 'PROVIDER_INLINE_CREDENTIAL_UNSUPPORTED' });
1279
+ const target = join(root, 'settings.backup');
1280
+ assertManagedPath(target, managedRoot);
1281
+ atomicWrite(target, sourceBytes, managedRoot);
1282
+ manifest.backup_digests.settings = sha256(sourceBytes);
1283
+ sourceRevisions.settings = sha256(sourceBytes);
1284
+ }
1285
+ }
1286
+ manifest.config_projection = configProjection(configSnapshot);
1287
+ manifest.config_revision = sha256(JSON.stringify(configSnapshot));
1288
+ manifest.lifecycle_revision = sha256(JSON.stringify(lifecycleSnapshot));
1289
+ manifest.routing_projection_digest = sha256(JSON.stringify(manifest.config_projection));
1290
+ manifest.lifecycle_projection_digest = sha256(JSON.stringify(lifecycleProjection(lifecycleSnapshot)));
1291
+ if (authorityKinds.has('crew-profile') && sourceRevisions.profile) manifest.profile_revision = sourceRevisions.profile;
1292
+ if ((authorityKinds.has('harness-settings') || managesHarnessDefault) && sourceRevisions.settings) manifest.settings_revision = sourceRevisions.settings;
1293
+ manifest.recovery_descriptor_digest = sha256(JSON.stringify(recoveryDescriptor(manifest)));
1294
+ activeBackup = { root, manifest };
1295
+ persistManifest();
1296
+ return { ok: true, backup_id: planId };
1297
+ };
1298
+
1299
+ const markTombstone = async (providerId) => {
1300
+ if (!validProviderId(providerId)) throw Object.assign(new Error('provider id is invalid'), { code: 'PROVIDER_NOT_FOUND' });
1301
+ assertManagedPath(lifecycleFile, managedRoot);
1302
+ if (activeBackup?.manifest?.files?.lifecycle?.existed === false && fs.existsSync(lifecycleFile)) {
1303
+ throw Object.assign(new Error('provider lifecycle was created before the transaction write'), { code: 'PROVIDER_LIFECYCLE_CHANGED' });
1304
+ }
1305
+ const state = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
1306
+ if (activeBackup?.manifest?.lifecycle_revision && sha256(JSON.stringify(state)) !== activeBackup.manifest.lifecycle_revision) {
1307
+ throw Object.assign(new Error('provider lifecycle changed'), { code: 'PROVIDER_LIFECYCLE_CHANGED' });
1308
+ }
1309
+ const marked = markProviderTombstone(state, providerId);
1310
+ ensureMutationLock();
1311
+ const latestState = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
1312
+ if (sha256(JSON.stringify(latestState)) !== sha256(JSON.stringify(state))) {
1313
+ throw Object.assign(new Error('provider lifecycle changed during commit'), { code: 'PROVIDER_LIFECYCLE_CHANGED' });
1314
+ }
1315
+ setPhase('TOMBSTONE_APPLYING');
1316
+ const nextRevision = sha256(JSON.stringify(marked));
1317
+ const createOnly = activeBackup?.manifest?.files?.lifecycle?.existed === false;
1318
+ const lifecycleWitness = createOnly ? createWitnessPath('lifecycle') : null;
1319
+ const lifecycleContent = JSON.stringify(marked, null, 2) + '\n';
1320
+ prepareMutation('lifecycle', nextRevision, lifecycleWitness ? {
1321
+ witness: lifecycleWitness,
1322
+ witness_content: lifecycleContent,
1323
+ } : {});
1324
+ try {
1325
+ if (createOnly) atomicCreateWithWitness(lifecycleFile, lifecycleContent, managedRoot, lifecycleWitness);
1326
+ else atomicWrite(lifecycleFile, JSON.stringify(marked, null, 2) + '\n', managedRoot, { replace: true });
1327
+ } catch (error) {
1328
+ if (createOnly && error?.code === 'EEXIST') throw Object.assign(new Error('provider lifecycle was created before the transaction write'), { code: 'PROVIDER_LIFECYCLE_CHANGED' });
1329
+ throw error;
1330
+ }
1331
+ commitMutation('lifecycle', nextRevision);
1332
+ setPhase('TOMBSTONE_APPLIED');
1333
+ };
1334
+
1335
+ const scrubReferences = async (plan) => {
1336
+ assertManagedPath(configFile, managedRoot);
1337
+ const config = await readConfigFn();
1338
+ if (sha256(JSON.stringify(config)) !== activeBackup?.manifest?.config_revision) {
1339
+ throw Object.assign(new Error('provider config changed'), { code: 'PROVIDER_CONFIG_CHANGED' });
1340
+ }
1341
+ const scrubbed = scrubProviderReferences(config, [plan.provider_id]);
1342
+ setPhase('REFERENCES_APPLYING');
1343
+ let settingsMutation = null;
1344
+ const authorityKinds = authorityKindsFor(plan);
1345
+ if (plan.was_harness_default === true) {
1346
+ if (!validProviderId(plan.replacement_default) || typeof plan.replacement_default_model !== 'string' || !plan.replacement_default_model.trim()) {
1347
+ throw Object.assign(new Error('replacement Harness Default model is required'), { code: 'PROVIDER_DEFAULT_REPLACEMENT_MODEL_REQUIRED' });
1348
+ }
1349
+ if (plan.harness_default_authority?.kind !== 'harness-settings' || plan.harness_default_authority.locator !== 'agent-default-model' || typeof settingsFile !== 'string' || !settingsFile.trim() || !fs.existsSync(settingsFile)) {
1350
+ throw Object.assign(new Error('Harness Default authority is unavailable'), { code: 'PROVIDER_DEFAULT_AUTHORITY_UNAVAILABLE' });
1351
+ }
1352
+ const source = fs.readFileSync(settingsFile, 'utf8');
1353
+ settingsMutation = mutateProviderSettings(source, {
1354
+ providerId: authorityKinds.has('harness-settings') ? plan.provider_id : null,
1355
+ removeProvider: authorityKinds.has('harness-settings'),
1356
+ replacementDefault: plan.replacement_default,
1357
+ replacementModel: plan.replacement_default_model.trim(),
1358
+ expectedRevision: plan.expected_revisions?.settings ?? activeBackup?.manifest?.settings_revision,
1359
+ });
1360
+ if (!settingsMutation.ok) throw Object.assign(new Error('Harness Default settings changed'), { code: settingsMutation.code });
1361
+ scrubbed.config.harness_default = {
1362
+ provider: plan.replacement_default,
1363
+ model: plan.replacement_default_model.trim(),
1364
+ };
1365
+ }
1366
+ const configRevision = sha256(JSON.stringify(scrubbed.config));
1367
+ const configCreateOnly = activeBackup?.manifest?.files?.config?.existed === false;
1368
+ const configWitness = configCreateOnly ? createWitnessPath('config') : null;
1369
+ const configContent = JSON.stringify(scrubbed.config, null, 2) + '\n';
1370
+ prepareMutation('config', configRevision, configWitness ? {
1371
+ witness: configWitness,
1372
+ witness_content: configContent,
1373
+ } : {});
1374
+ await writeManagedConfig(scrubbed.config, activeBackup?.manifest?.config_revision, configWitness);
1375
+ commitMutation('config', configRevision);
1376
+ if (settingsMutation) {
1377
+ prepareMutation('settings', settingsMutation.revision);
1378
+ await writeManagedSettings(settingsMutation.text, { expectedRevision: plan.expected_revisions?.settings ?? activeBackup?.manifest?.settings_revision });
1379
+ commitMutation('settings', settingsMutation.revision);
1380
+ }
1381
+ setPhase('REFERENCES_APPLIED');
1382
+ };
1383
+
1384
+ const authorityKindsFor = (plan) => new Set((Array.isArray(plan?.declaration_authorities) ? plan.declaration_authorities : []).map((authority) => authority?.kind));
1385
+
1386
+ const removeDeclarations = async (plan) => {
1387
+ const authorityKinds = authorityKindsFor(plan);
1388
+ setPhase('DECLARATIONS_APPLYING');
1389
+ let removed = plan.was_harness_default === true && authorityKinds.has('harness-settings') ? 1 : 0;
1390
+ if (authorityKinds.has('crew-profile')) {
1391
+ assertManagedPath(profileFile, managedRoot);
1392
+ const source = fs.readFileSync(profileFile, 'utf8');
1393
+ const result = removeProviderDeclarations(source, {
1394
+ providerIds: [plan.provider_id],
1395
+ expectedRevision: plan.expected_revisions?.profile ?? plan.expected_revision,
1396
+ });
1397
+ if (!result.ok) throw Object.assign(new Error('provider profile changed'), { code: result.code });
1398
+ ensureMutationLock();
1399
+ const latestProfile = fs.readFileSync(profileFile, 'utf8');
1400
+ if (sha256(latestProfile) !== (plan.expected_revisions?.profile ?? plan.expected_revision)) {
1401
+ throw Object.assign(new Error('provider profile changed during commit'), { code: 'PROVIDER_PROFILE_CHANGED' });
1402
+ }
1403
+ prepareMutation('profile', result.revision);
1404
+ atomicWrite(profileFile, result.text, managedRoot);
1405
+ removed += result.removed.length;
1406
+ commitMutation('profile', result.revision);
1407
+ }
1408
+ if (authorityKinds.has('harness-settings') && !(plan.was_harness_default === true)) {
1409
+ if (typeof settingsFile !== 'string' || !settingsFile.trim()) throw Object.assign(new Error('provider settings path is unavailable'), { code: 'PROVIDER_DELETE_SOURCE_UNRESOLVED' });
1410
+ assertManagedPath(settingsFile, managedRoot);
1411
+ const source = fs.readFileSync(settingsFile, 'utf8');
1412
+ const result = removeProviderSettings(source, {
1413
+ providerIds: [plan.provider_id],
1414
+ expectedRevision: plan.expected_revisions?.settings,
1415
+ });
1416
+ if (!result.ok) throw Object.assign(new Error('provider settings changed'), { code: result.code });
1417
+ prepareMutation('settings', result.revision);
1418
+ await writeManagedSettings(result.text, { expectedRevision: plan.expected_revisions?.settings });
1419
+ removed += result.removed.length;
1420
+ commitMutation('settings', result.revision);
1421
+ }
1422
+ if (removed === 0) throw Object.assign(new Error('provider declaration source is unavailable'), { code: 'PROVIDER_DELETE_SOURCE_UNRESOLVED' });
1423
+ setPhase('DECLARATIONS_APPLIED');
1424
+ };
1425
+
1426
+ const checkpointApplied = async () => {
1427
+ if (!activeBackup) throw Object.assign(new Error('provider delete backup is unavailable'), { code: 'PROVIDER_DELETE_BACKUP_INVALID' });
1428
+ assertManagedPath(configFile, managedRoot);
1429
+ assertManagedPath(lifecycleFile, managedRoot);
1430
+ assertManagedPath(profileFile, managedRoot);
1431
+ if (settingsFile) assertManagedPath(settingsFile, managedRoot);
1432
+ const config = await readConfigFn();
1433
+ const lifecycle = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
1434
+ activeBackup.manifest.applied_config_revision = sha256(JSON.stringify(config));
1435
+ activeBackup.manifest.applied_lifecycle_revision = sha256(JSON.stringify(lifecycle));
1436
+ if (activeBackup.manifest.files.profile?.managed !== false && fs.existsSync(profileFile)) activeBackup.manifest.applied_profile_revision = sha256(fs.readFileSync(profileFile, 'utf8'));
1437
+ if (activeBackup.manifest.files.settings?.managed === true && typeof settingsFile === 'string' && settingsFile.trim() && fs.existsSync(settingsFile)) activeBackup.manifest.applied_settings_revision = sha256(fs.readFileSync(settingsFile, 'utf8'));
1438
+ activeBackup.manifest.mutation_journal = {};
1439
+ persistManifest();
1440
+ };
1441
+
1442
+ const backupPlan = () => (activeBackup?.manifest?.plan ? { ...activeBackup.manifest.plan } : null);
1443
+
1444
+ const setRuntimeBaseline = async (runtimeId, phase) => {
1445
+ if (!activeBackup) throw Object.assign(new Error('provider delete backup is unavailable'), { code: 'PROVIDER_DELETE_BACKUP_INVALID' });
1446
+ if (typeof runtimeId !== 'string' || runtimeId.trim() === '') throw Object.assign(new Error('runtime identity is required'), { code: 'PROVIDER_RUNTIME_ID_MISSING' });
1447
+ const key = phase === 'rollback' ? 'rollback_runtime_id_before_restart' : phase === 'delete' ? 'delete_runtime_id_before_restart' : null;
1448
+ if (!key) throw Object.assign(new Error('runtime baseline phase is invalid'), { code: 'PROVIDER_DELETE_PLAN_INVALID' });
1449
+ activeBackup.manifest.plan[key] = runtimeId.trim().slice(0, 128);
1450
+ setPhase(phase === 'rollback' ? 'ROLLBACK_RESTART_PENDING' : 'DELETE_RESTART_PENDING');
1451
+ persistManifest();
1452
+ return { ok: true, phase, runtime_id: activeBackup.manifest.plan[key] };
1453
+ };
1454
+
1455
+ const captureRuntimeBaseline = async (_plan, phase) => {
1456
+ if (typeof runtimeIdProvider !== 'function') return { ok: false, code: 'PROVIDER_RUNTIME_ID_PROVIDER_UNAVAILABLE' };
1457
+ return setRuntimeBaseline(runtimeIdProvider(), phase);
1458
+ };
1459
+
1460
+ const verify = async (plan) => {
1461
+ const authorityKinds = authorityKindsFor(plan);
1462
+ assertManagedPath(configFile, managedRoot);
1463
+ assertManagedPath(profileFile, managedRoot);
1464
+ assertManagedPath(lifecycleFile, managedRoot);
1465
+ if (settingsFile) assertManagedPath(settingsFile, managedRoot);
1466
+ let providerAbsent = true;
1467
+ if (authorityKinds.has('crew-profile')) {
1468
+ const source = fs.readFileSync(profileFile, 'utf8');
1469
+ const parsed = readProviderDeclarations(source, { file: 'harness/profiles/dsh-crew/cordis.patch.yml' });
1470
+ providerAbsent = parsed.ok && !parsed.declarations.some((declaration) => declaration.id === plan.provider_id);
1471
+ }
1472
+ if (authorityKinds.has('harness-settings')) {
1473
+ if (typeof settingsFile !== 'string' || !settingsFile.trim() || !fs.existsSync(settingsFile)) providerAbsent = false;
1474
+ else {
1475
+ const parsed = readProviderSettingsDeclarations(fs.readFileSync(settingsFile, 'utf8'), { file: 'harness/settings.yaml' });
1476
+ providerAbsent = providerAbsent && parsed.ok && !parsed.declarations.some((declaration) => declaration.id === plan.provider_id);
1477
+ }
1478
+ }
1479
+ const config = await readConfigFn();
1480
+ const routingClear = scrubProviderReferences(config, [plan.provider_id]).removed.length === 0;
1481
+ const replacementApplied = plan.was_harness_default !== true || (
1482
+ config?.harness_default?.provider === plan.replacement_default
1483
+ && config?.harness_default?.model === plan.replacement_default_model
1484
+ );
1485
+ const persistedDefault = typeof settingsFile === 'string' && settingsFile.trim() && fs.existsSync(settingsFile)
1486
+ ? readHarnessDefault(fs.readFileSync(settingsFile, 'utf8')) : { ok: false };
1487
+ const harnessDefaultApplied = plan.was_harness_default === true
1488
+ ? persistedDefault.ok && persistedDefault.provider === plan.replacement_default && persistedDefault.model === plan.replacement_default_model
1489
+ : !persistedDefault.ok || persistedDefault.provider !== plan.provider_id;
1490
+ const state = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
1491
+ return {
1492
+ providerAbsent,
1493
+ routingClear: routingClear && replacementApplied && harnessDefaultApplied,
1494
+ tombstonePresent: state.tombstones[plan.provider_id] === 'absent',
1495
+ };
1496
+ };
1497
+
1498
+ const rollback = async () => {
1499
+ if (!activeBackup) throw Object.assign(new Error('provider delete backup is unavailable'), { code: 'PROVIDER_DELETE_ROLLBACK_UNAVAILABLE' });
1500
+ ensureMutationLock();
1501
+ assertRecoveryDescriptor();
1502
+ assertManagedPath(configFile, managedRoot);
1503
+ assertManagedPath(profileFile, managedRoot);
1504
+ assertManagedPath(lifecycleFile, managedRoot);
1505
+ if (settingsFile) assertManagedPath(settingsFile, managedRoot);
1506
+ const allowedRevision = (current, original, applied, key) => current === original
1507
+ || (typeof applied === 'string' && current === applied)
1508
+ || (typeof activeBackup.manifest.mutation_journal?.[key]?.next_revision === 'string' && current === activeBackup.manifest.mutation_journal[key].next_revision);
1509
+ const currentConfig = await readConfigFn();
1510
+ const currentLifecycle = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
1511
+ const currentProfile = fs.existsSync(profileFile) ? fs.readFileSync(profileFile, 'utf8') : null;
1512
+ const currentSettings = typeof settingsFile === 'string' && settingsFile.trim() && fs.existsSync(settingsFile) ? fs.readFileSync(settingsFile, 'utf8') : null;
1513
+ if (!allowedRevision(sha256(JSON.stringify(currentConfig)), activeBackup.manifest.config_revision, activeBackup.manifest.applied_config_revision, 'config')
1514
+ || !allowedRevision(sha256(JSON.stringify(currentLifecycle)), activeBackup.manifest.lifecycle_revision, activeBackup.manifest.applied_lifecycle_revision, 'lifecycle')
1515
+ || (activeBackup.manifest.profile_revision && (!currentProfile || !allowedRevision(sha256(currentProfile), activeBackup.manifest.profile_revision, activeBackup.manifest.applied_profile_revision, 'profile')))
1516
+ || (activeBackup.manifest.settings_revision && (!currentSettings || !allowedRevision(sha256(currentSettings), activeBackup.manifest.settings_revision, activeBackup.manifest.applied_settings_revision, 'settings')))) {
1517
+ throw Object.assign(new Error('managed provider state changed after the transaction'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
1518
+ }
1519
+ const removeIfTransactionCreated = async (key, target) => {
1520
+ ensureMutationLock();
1521
+ assertManagedPath(target, managedRoot);
1522
+ if (!fs.existsSync(target)) {
1523
+ const orphanWitness = activeBackup.manifest.mutation_journal?.[key]?.witness;
1524
+ if (typeof orphanWitness === 'string') { try { rmSync(orphanWitness, { force: true }); } catch {} }
1525
+ return;
1526
+ }
1527
+ // A pending journal proves intent, not ownership: a crash can happen
1528
+ // before the exclusive-create rename. Only the durable commit marker
1529
+ // authorizes deleting an originally absent file during compensation.
1530
+ let created = activeBackup.manifest[`created_${key}`] === true;
1531
+ if (created) {
1532
+ const identity = activeBackup.manifest[`created_${key}_identity`];
1533
+ const witnessName = activeBackup.manifest.ownership_witnesses?.[key];
1534
+ const witnessFile = typeof witnessName === 'string' ? join(activeBackup.root, witnessName) : null;
1535
+ if (!identity || !fs.existsSync(target) || !witnessFile || !fs.existsSync(witnessFile)) created = false;
1536
+ else {
1537
+ try {
1538
+ const currentIdentity = fileIdentity(target);
1539
+ const witnessIdentity = fileIdentity(witnessFile);
1540
+ created = sameFileIdentity(currentIdentity, witnessIdentity) && sameFileIdentity(witnessIdentity, identity);
1541
+ } catch { created = false; }
1542
+ }
1543
+ }
1544
+ const pending = activeBackup.manifest.mutation_journal?.[key];
1545
+ let pendingOwned = false;
1546
+ if (!created && pending?.created === true && typeof pending.witness === 'string' && fs.existsSync(pending.witness) && fs.existsSync(target)) {
1547
+ try {
1548
+ created = sameFileIdentity(fileIdentity(pending.witness), fileIdentity(target));
1549
+ pendingOwned = created;
1550
+ } catch { created = false; }
1551
+ }
1552
+ const expected = pendingOwned
1553
+ ? pending.next_revision
1554
+ : activeBackup.manifest[`applied_${key}_revision`] ?? pending?.next_revision;
1555
+ if (typeof expected !== 'string' || !created) throw Object.assign(new Error('managed provider state changed during rollback'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
1556
+ let current;
1557
+ if (key === 'config') current = sha256(JSON.stringify(await readConfigFn()));
1558
+ else if (key === 'lifecycle') current = sha256(JSON.stringify(normalizeProviderLifecycleState(readJson(lifecycleFile, {}))));
1559
+ else current = sha256(fs.readFileSync(target));
1560
+ if (current !== expected) throw Object.assign(new Error('managed provider state changed during rollback'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
1561
+ assertManagedPath(target, managedRoot);
1562
+ fs.rmSync(target, { force: true });
1563
+ if (pending?.witness) { try { fs.rmSync(pending.witness, { force: true }); } catch {} }
1564
+ assertManagedPath(target, managedRoot);
1565
+ };
1566
+ setPhase('ROLLBACK_APPLYING');
1567
+ for (const key of FILE_KEYS) {
1568
+ const target = paths[key];
1569
+ const entry = activeBackup.manifest.files[key];
1570
+ if (key === 'profile' && entry?.managed === false) continue;
1571
+ const backupFile = join(activeBackup.root, `${key}.backup`);
1572
+ assertManagedPath(backupFile, managedRoot);
1573
+ assertManagedPath(target, managedRoot);
1574
+ if (key === 'config') {
1575
+ if (entry?.existed === true) {
1576
+ const restored = restoreConfigProjection(await readConfigFn(), activeBackup.manifest.config_projection);
1577
+ await writeManagedConfig(restored, sha256(JSON.stringify(currentConfig)));
1578
+ } else {
1579
+ await removeIfTransactionCreated('config', target);
1580
+ }
1581
+ } else if (key === 'lifecycle' && entry?.existed === true) {
1582
+ ensureMutationLock();
1583
+ const verifiedLifecycle = readVerifiedBackup('lifecycle');
1584
+ const latestLifecycle = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
1585
+ if (!allowedRevision(sha256(JSON.stringify(latestLifecycle)), activeBackup.manifest.lifecycle_revision, activeBackup.manifest.applied_lifecycle_revision, 'lifecycle')) {
1586
+ throw Object.assign(new Error('managed provider state changed during rollback'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
1587
+ }
1588
+ const savedLifecycle = normalizeProviderLifecycleState(parseManagedJson(verifiedLifecycle));
1589
+ const restoredLifecycle = { ...currentLifecycle, tombstones: { ...savedLifecycle.tombstones } };
1590
+ const restoredRevision = sha256(JSON.stringify(restoredLifecycle));
1591
+ prepareMutation('lifecycle', restoredRevision);
1592
+ atomicWrite(target, JSON.stringify(restoredLifecycle, null, 2) + '\n', managedRoot);
1593
+ commitMutation('lifecycle', restoredRevision);
1594
+ } else if (entry?.existed === true) {
1595
+ ensureMutationLock();
1596
+ const verifiedContent = readVerifiedBackup(key);
1597
+ const latest = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
1598
+ if (latest === null || !allowedRevision(sha256(latest), activeBackup.manifest[`${key}_revision`], activeBackup.manifest[`applied_${key}_revision`], key)) {
1599
+ throw Object.assign(new Error('managed provider state changed during rollback'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
1600
+ }
1601
+ safeRestoreFile(backupFile, target, managedRoot, verifiedContent);
1602
+ } else {
1603
+ await removeIfTransactionCreated(key, target);
1604
+ }
1605
+ }
1606
+ const settingsEntry = activeBackup.manifest.files.settings;
1607
+ if (settingsEntry?.managed === true && typeof settingsFile === 'string' && settingsFile.trim()) {
1608
+ ensureMutationLock();
1609
+ assertManagedPath(settingsFile, managedRoot);
1610
+ const backupFile = join(activeBackup.root, 'settings.backup');
1611
+ assertManagedPath(backupFile, managedRoot);
1612
+ if (settingsEntry?.existed === true) {
1613
+ const verifiedSettings = readVerifiedBackup('settings');
1614
+ const latestSettings = fs.existsSync(settingsFile) ? fs.readFileSync(settingsFile, 'utf8') : null;
1615
+ if (latestSettings === null || !allowedRevision(sha256(latestSettings), activeBackup.manifest.settings_revision, activeBackup.manifest.applied_settings_revision, 'settings')) {
1616
+ throw Object.assign(new Error('managed provider state changed during rollback'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
1617
+ }
1618
+ safeRestoreFile(backupFile, settingsFile, managedRoot, verifiedSettings);
1619
+ }
1620
+ else await removeIfTransactionCreated('settings', settingsFile);
1621
+ }
1622
+ activeBackup.manifest.mutation_journal = {};
1623
+ setPhase('ROLLBACK_RESTORED');
1624
+ };
1625
+
1626
+ const release = async () => {
1627
+ if (!lockOwned || !lockPath) return;
1628
+ try {
1629
+ let owner = null;
1630
+ let ownerPath = lockPath;
1631
+ try { if (lstatSync(lockPath).isDirectory()) ownerPath = join(lockPath, 'owner.json'); } catch {}
1632
+ try { owner = readJson(ownerPath, null); } catch {}
1633
+ if (owner?.token === lockOwnerToken) fs.rmSync(lockPath, { recursive: true, force: true });
1634
+ } finally { lockOwned = false; lockPath = null; lockOwnerToken = null; }
1635
+ };
1636
+
1637
+ const quarantine = async (entryName) => {
1638
+ const safeName = safeRecoveryEntryName(entryName);
1639
+ if (!safeName) throw Object.assign(new Error('provider recovery entry name is invalid'), { code: 'PROVIDER_DELETE_PLAN_INVALID' });
1640
+ await acquireLock();
1641
+ try {
1642
+ const source = join(backupDir, safeName);
1643
+ const quarantineRoot = join(backupDir, '.quarantine');
1644
+ assertManagedEntryPath(source, managedRoot);
1645
+ assertManagedPath(quarantineRoot, managedRoot);
1646
+ try { lstatSync(source); }
1647
+ catch {
1648
+ throw Object.assign(new Error('provider recovery transaction was not found'), { code: 'PROVIDER_DELETE_RECOVERY_NOT_FOUND' });
1649
+ }
1650
+ if (existsSync(quarantineRoot) && !lstatSync(quarantineRoot).isDirectory()) {
1651
+ throw Object.assign(new Error('provider recovery quarantine path is unsafe'), { code: 'PROVIDER_DELETE_UNSAFE_PATH' });
1652
+ }
1653
+ mkdirSync(quarantineRoot, { recursive: true });
1654
+ assertManagedPath(quarantineRoot, managedRoot);
1655
+ const target = join(quarantineRoot, `${sha256(safeName).slice(0, 32)}.${Date.now()}.${randomUUID().slice(0, 8)}`);
1656
+ assertManagedPath(target, managedRoot);
1657
+ renameSync(source, target);
1658
+ return { ok: true, storage_id: safeName, state: 'QUARANTINED' };
1659
+ } finally {
1660
+ await release();
1661
+ }
1662
+ };
1663
+
1664
+ const verifyRollback = async (plan) => {
1665
+ const authorityKinds = authorityKindsFor(plan);
1666
+ assertManagedPath(configFile, managedRoot);
1667
+ assertManagedPath(profileFile, managedRoot);
1668
+ assertManagedPath(lifecycleFile, managedRoot);
1669
+ if (settingsFile) assertManagedPath(settingsFile, managedRoot);
1670
+ const source = fs.existsSync(profileFile) ? fs.readFileSync(profileFile, 'utf8') : null;
1671
+ const parsed = source === null ? { ok: false } : readProviderDeclarations(source, { file: 'harness/profiles/dsh-crew/cordis.patch.yml' });
1672
+ const legacyPlan = authorityKinds.size === 0;
1673
+ let providerPresent = (authorityKinds.has('crew-profile') || legacyPlan) && parsed.ok && parsed.declarations.some((declaration) => declaration.id === plan.provider_id);
1674
+ if ((authorityKinds.has('harness-settings') || legacyPlan) && activeBackup?.manifest?.files?.settings?.existed === true && typeof settingsFile === 'string' && settingsFile.trim()) {
1675
+ const settings = readProviderSettingsDeclarations(fs.readFileSync(settingsFile, 'utf8'), { file: 'harness/settings.yaml' });
1676
+ providerPresent = providerPresent || (settings.ok && settings.declarations.some((declaration) => declaration.id === plan.provider_id));
1677
+ }
1678
+ const config = await readConfigFn();
1679
+ const state = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
1680
+ const routingRestored = typeof activeBackup?.manifest?.routing_projection_digest === 'string'
1681
+ && sha256(JSON.stringify(configProjection(config))) === activeBackup.manifest.routing_projection_digest;
1682
+ const lifecycleRestored = typeof activeBackup?.manifest?.lifecycle_projection_digest === 'string'
1683
+ && sha256(JSON.stringify(lifecycleProjection(state))) === activeBackup.manifest.lifecycle_projection_digest;
1684
+ const declarationRestored = !activeBackup?.manifest?.profile_revision
1685
+ || (source !== null && sha256(source) === activeBackup.manifest.profile_revision);
1686
+ const settingsRestored = !activeBackup?.manifest?.settings_revision || (typeof settingsFile === 'string' && settingsFile.trim() && fs.existsSync(settingsFile) && sha256(fs.readFileSync(settingsFile, 'utf8')) === activeBackup.manifest.settings_revision);
1687
+ return {
1688
+ ok: providerPresent && state.tombstones[plan.provider_id] !== 'absent'
1689
+ && routingRestored && lifecycleRestored && declarationRestored && settingsRestored,
1690
+ providerPresent,
1691
+ tombstoneCleared: state.tombstones[plan.provider_id] !== 'absent',
1692
+ routingRestored,
1693
+ lifecycleRestored,
1694
+ declarationRestored,
1695
+ settingsRestored,
1696
+ };
1697
+ };
1698
+
1699
+ const recordTransaction = async (result, plan) => {
1700
+ ensureMutationLock();
1701
+ assertManagedPath(configFile, managedRoot);
1702
+ assertManagedPath(profileFile, managedRoot);
1703
+ assertManagedPath(lifecycleFile, managedRoot);
1704
+ if (settingsFile) assertManagedPath(settingsFile, managedRoot);
1705
+ const state = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
1706
+ const initialRevision = sha256(JSON.stringify(state));
1707
+ const next = recordProviderTransaction(state, {
1708
+ transaction_id: result?.transaction_id ?? plan?.plan_id,
1709
+ provider_id: result?.provider_id ?? plan?.provider_id,
1710
+ state: result?.state,
1711
+ updated_at: new Date().toISOString(),
1712
+ expected_revision: plan?.expected_revision,
1713
+ credential_refs: plan?.credential_refs,
1714
+ });
1715
+ if (result?.state === 'VERIFIED') {
1716
+ const profileRevision = fs.existsSync(profileFile) ? sha256(fs.readFileSync(profileFile, 'utf8')) : '';
1717
+ const settingsRevision = activeBackup?.manifest?.files?.settings?.managed === true
1718
+ && typeof settingsFile === 'string' && settingsFile.trim() && fs.existsSync(settingsFile)
1719
+ ? sha256(fs.readFileSync(settingsFile, 'utf8')) : '';
1720
+ next.last_verified_revision[plan.provider_id] = sha256(`${profileRevision}:${settingsRevision}`);
1721
+ }
1722
+ const nextRevision = sha256(JSON.stringify(next));
1723
+ const latestState = normalizeProviderLifecycleState(readJson(lifecycleFile, {}));
1724
+ if (sha256(JSON.stringify(latestState)) !== initialRevision) {
1725
+ throw Object.assign(new Error('provider lifecycle changed during audit'), { code: 'PROVIDER_LIFECYCLE_CHANGED' });
1726
+ }
1727
+ const lifecycleOriginallyAbsent = activeBackup?.manifest?.files?.lifecycle?.existed === false;
1728
+ let lifecycleOwned = false;
1729
+ if (activeBackup?.manifest?.created_lifecycle === true && fs.existsSync(lifecycleFile)) {
1730
+ const recordedIdentity = activeBackup.manifest.created_lifecycle_identity;
1731
+ try {
1732
+ const currentIdentity = fileIdentity(lifecycleFile);
1733
+ lifecycleOwned = sameFileIdentity(currentIdentity, recordedIdentity);
1734
+ } catch { lifecycleOwned = false; }
1735
+ if (!lifecycleOwned) {
1736
+ throw Object.assign(new Error('provider lifecycle ownership changed during audit'), { code: 'PROVIDER_LIFECYCLE_CHANGED' });
1737
+ }
1738
+ }
1739
+ if (lifecycleOriginallyAbsent && fs.existsSync(lifecycleFile) && !lifecycleOwned) {
1740
+ throw Object.assign(new Error('provider lifecycle was created outside the transaction'), { code: 'PROVIDER_LIFECYCLE_CHANGED' });
1741
+ }
1742
+ const lifecycleCreateOnly = lifecycleOriginallyAbsent && !lifecycleOwned;
1743
+ const lifecycleOwnershipHandoff = lifecycleOriginallyAbsent && lifecycleOwned;
1744
+ const lifecycleWitness = lifecycleCreateOnly || lifecycleOwnershipHandoff ? createWitnessPath('lifecycle') : null;
1745
+ const lifecycleContent = JSON.stringify(next, null, 2) + '\n';
1746
+ prepareMutation('lifecycle', nextRevision, {
1747
+ created: lifecycleCreateOnly || lifecycleOwnershipHandoff,
1748
+ ...(lifecycleWitness ? { witness: lifecycleWitness } : {}),
1749
+ ...(lifecycleWitness ? { witness_content: lifecycleContent } : {}),
1750
+ ...(typeof afterMutationJournaled === 'function' ? { after_journaled: afterMutationJournaled } : {}),
1751
+ });
1752
+ if (lifecycleCreateOnly) atomicCreateWithWitness(lifecycleFile, lifecycleContent, managedRoot, lifecycleWitness);
1753
+ else if (lifecycleOwnershipHandoff) {
1754
+ atomicReplaceWithWitness(lifecycleFile, lifecycleContent, managedRoot, lifecycleWitness);
1755
+ if (typeof afterOwnedReplacePublished === 'function') afterOwnedReplacePublished('lifecycle', lifecycleFile, lifecycleWitness);
1756
+ }
1757
+ else atomicWrite(lifecycleFile, JSON.stringify(next, null, 2) + '\n', managedRoot);
1758
+ commitMutation('lifecycle', nextRevision);
1759
+ setPhase(result?.state === 'RESTART_PENDING' ? 'RESTART_PENDING' : result?.state === 'ROLLBACK_PENDING' ? 'ROLLBACK_PENDING' : result?.state ?? 'AUDIT_APPLYING');
1760
+ };
1761
+
1762
+ return {
1763
+ backup,
1764
+ acquireLock,
1765
+ recoverLock,
1766
+ checkpointApplied,
1767
+ setRuntimeBaseline,
1768
+ ...(typeof runtimeIdProvider === 'function' ? { captureRuntimeBaseline } : {}),
1769
+ backupPlan,
1770
+ markTombstone,
1771
+ scrubReferences,
1772
+ removeDeclarations,
1773
+ restart: typeof restart === 'function'
1774
+ ? async (plan) => restart(plan)
1775
+ : async () => ({ ok: false, code: 'PROVIDER_DELETE_RESTART_SUPERVISOR_UNAVAILABLE' }),
1776
+ restartRollback: typeof restart === 'function'
1777
+ ? async (plan) => restart(plan)
1778
+ : async () => ({ ok: false, code: 'PROVIDER_DELETE_RESTART_SUPERVISOR_UNAVAILABLE' }),
1779
+ verify,
1780
+ verifyRollback,
1781
+ recordTransaction,
1782
+ quarantine,
1783
+ rollback,
1784
+ release,
1785
+ };
1786
+ }