@ran-sh/dsh-crew 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/README.zh.md +4 -0
- package/lib/client.js +183 -6
- package/official-web-bridge/lib/client.js +183 -6
- package/package.json +1 -1
- package/src/client/index.tsx +92 -4
- package/src/credential-reference.mjs +38 -0
- package/src/hub/index.mjs +483 -14
- package/src/install/npx-lifecycle.mjs +456 -398
- package/src/model-catalog.mjs +8 -1
- package/src/provider-delete-adapters.mjs +165 -17
- package/src/provider-inventory.mjs +17 -3
- package/src/provider-layer-migration-adapters.mjs +759 -0
- package/src/provider-layer-migration.mjs +198 -0
- package/src/provider-lifecycle-state.mjs +4 -1
- package/src/provider-profile-store.mjs +193 -4
- package/src/provider-settings-store.mjs +82 -6
- package/src/provider-store-lock.mjs +67 -0
- package/src/runtime-identity.mjs +2 -1
package/src/model-catalog.mjs
CHANGED
|
@@ -20,7 +20,14 @@ function catalogError() {
|
|
|
20
20
|
function normalizeProvider(raw) {
|
|
21
21
|
if (!raw || typeof raw.id !== 'string' || raw.id.trim() === '') return null;
|
|
22
22
|
const id = raw.id.trim();
|
|
23
|
-
|
|
23
|
+
const adapterOwned = typeof raw.adapter_owned === 'boolean'
|
|
24
|
+
? raw.adapter_owned
|
|
25
|
+
: typeof raw.adapterOwned === 'boolean' ? raw.adapterOwned : undefined;
|
|
26
|
+
return {
|
|
27
|
+
id,
|
|
28
|
+
name: typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : id,
|
|
29
|
+
...(adapterOwned === undefined ? {} : { adapter_owned: adapterOwned }),
|
|
30
|
+
};
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
function normalizeModels(raw, provider) {
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
normalizeProviderLifecycleState,
|
|
39
39
|
recordProviderTransaction,
|
|
40
40
|
} from './provider-lifecycle-state.mjs';
|
|
41
|
+
import { acquireProviderStoreLock } from './provider-store-lock.mjs';
|
|
41
42
|
|
|
42
43
|
const FILE_KEYS = Object.freeze(['profile', 'config', 'lifecycle']);
|
|
43
44
|
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
@@ -66,6 +67,18 @@ function safePlanId(value) {
|
|
|
66
67
|
return typeof value === 'string' && /^[0-9a-f-]{36}$/i.test(value) ? value : null;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function validTimestamp(value) {
|
|
71
|
+
return typeof value === 'string' && value.length <= 128 && !/[\r\n]/u.test(value) && Number.isFinite(Date.parse(value));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const DELETE_PHASES = new Set([
|
|
75
|
+
'PLANNED', 'TOMBSTONE_APPLYING', 'TOMBSTONE_APPLIED',
|
|
76
|
+
'REFERENCES_APPLYING', 'REFERENCES_APPLIED', 'DECLARATIONS_APPLYING',
|
|
77
|
+
'DECLARATIONS_APPLIED', 'DELETE_RESTART_PENDING', 'RESTART_PENDING',
|
|
78
|
+
'ROLLBACK_PENDING', 'ROLLBACK_APPLYING', 'ROLLBACK_RESTART_PENDING',
|
|
79
|
+
'ROLLBACK_RESTORED', 'AUDIT_APPLYING', 'VERIFIED', 'ROLLED_BACK', 'FAILED',
|
|
80
|
+
]);
|
|
81
|
+
|
|
69
82
|
function safeRecoveryEntryName(value) {
|
|
70
83
|
return typeof value === 'string' && value.length > 0 && value.length <= 255
|
|
71
84
|
&& value !== '.' && value !== '..' && !/[\\/\u0000]/u.test(value) ? value : null;
|
|
@@ -88,7 +101,7 @@ function atomicWrite(file, content, managedRoot = null, { replace = true } = {})
|
|
|
88
101
|
if (managedRoot) assertManagedPath(file, managedRoot);
|
|
89
102
|
if (replace) renameSync(temp, file);
|
|
90
103
|
else {
|
|
91
|
-
|
|
104
|
+
publishHardlink(temp, file, managedRoot);
|
|
92
105
|
rmSync(temp, { force: true });
|
|
93
106
|
}
|
|
94
107
|
} catch (error) {
|
|
@@ -111,6 +124,17 @@ function defaultReadConfig(configFile, managedRoot = null) {
|
|
|
111
124
|
return readJson(configFile, {});
|
|
112
125
|
}
|
|
113
126
|
|
|
127
|
+
function publishHardlink(source, target, managedRoot) {
|
|
128
|
+
// Check both operands and every ancestor at the last possible moment. The
|
|
129
|
+
// post-link check catches a destination that was swapped during publication.
|
|
130
|
+
assertManagedPath(source, managedRoot);
|
|
131
|
+
assertManagedPath(dirname(source), managedRoot);
|
|
132
|
+
assertManagedPath(target, managedRoot);
|
|
133
|
+
assertManagedPath(dirname(target), managedRoot);
|
|
134
|
+
linkSync(source, target);
|
|
135
|
+
assertManagedPath(target, managedRoot);
|
|
136
|
+
}
|
|
137
|
+
|
|
114
138
|
function atomicCreateWithWitness(file, content, managedRoot, witness) {
|
|
115
139
|
if (!managedRoot || typeof witness !== 'string' || !witness.trim()) {
|
|
116
140
|
throw Object.assign(new Error('exclusive provider create witness is unavailable'), { code: 'PROVIDER_DELETE_LOCK_UNAVAILABLE' });
|
|
@@ -126,7 +150,7 @@ function atomicCreateWithWitness(file, content, managedRoot, witness) {
|
|
|
126
150
|
if (sha256(readFileSync(witness)) !== sha256(content)) throw Object.assign(new Error('provider create witness content changed'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
|
|
127
151
|
} else writeFileSync(witness, content, { flag: 'wx' });
|
|
128
152
|
assertManagedPath(witness, managedRoot);
|
|
129
|
-
|
|
153
|
+
publishHardlink(witness, file, managedRoot);
|
|
130
154
|
assertManagedPath(file, managedRoot);
|
|
131
155
|
return witness;
|
|
132
156
|
} catch (error) {
|
|
@@ -149,8 +173,9 @@ function atomicReplaceWithWitness(file, content, managedRoot, witness) {
|
|
|
149
173
|
if (witnessExisted) {
|
|
150
174
|
if (sha256(readFileSync(witness)) !== sha256(content)) throw Object.assign(new Error('provider replace witness content changed'), { code: 'PROVIDER_DELETE_STATE_CHANGED' });
|
|
151
175
|
} else writeFileSync(witness, content, { flag: 'wx' });
|
|
152
|
-
|
|
176
|
+
publishHardlink(witness, stage, managedRoot);
|
|
153
177
|
assertManagedPath(file, managedRoot);
|
|
178
|
+
assertManagedPath(dirname(file), managedRoot);
|
|
154
179
|
renameSync(stage, file);
|
|
155
180
|
published = true;
|
|
156
181
|
assertManagedPath(file, managedRoot);
|
|
@@ -174,6 +199,11 @@ function hasOwn(value, key) {
|
|
|
174
199
|
return !!value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key);
|
|
175
200
|
}
|
|
176
201
|
|
|
202
|
+
function onlyKeys(value, allowed) {
|
|
203
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
204
|
+
&& Object.keys(value).every((key) => allowed.includes(key));
|
|
205
|
+
}
|
|
206
|
+
|
|
177
207
|
function safeModelRef(value) {
|
|
178
208
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return value ?? null;
|
|
179
209
|
const result = {};
|
|
@@ -187,6 +217,32 @@ function safeModelRefList(value) {
|
|
|
187
217
|
return Array.isArray(value) ? value.map((entry) => safeModelRef(entry)) : value;
|
|
188
218
|
}
|
|
189
219
|
|
|
220
|
+
function validModelRef(value) {
|
|
221
|
+
return onlyKeys(value, ['provider', 'model', 'reasoningEffort'])
|
|
222
|
+
&& Object.values(value).every((entry) => typeof entry === 'string' && entry.length > 0 && entry.length <= 256 && !/[\r\n]/u.test(entry));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validConfigProjection(value) {
|
|
226
|
+
if (!onlyKeys(value, ['schema_version', 'fields', 'present_fields']) || value.schema_version !== 1 || !Array.isArray(value.present_fields)
|
|
227
|
+
|| value.present_fields.some((key) => !['flash_model_priority', 'pro_model_priority', 'harness_default', 'agent_default_model', 'agentDefaultModel', 'worker', 'review'].includes(key))) return false;
|
|
228
|
+
if (!onlyKeys(value.fields, ['flash_model_priority', 'pro_model_priority', 'harness_default', 'agent_default_model', 'agentDefaultModel', 'worker', 'review'])) return false;
|
|
229
|
+
for (const key of ['flash_model_priority', 'pro_model_priority']) {
|
|
230
|
+
if (value.fields[key] !== undefined && (!Array.isArray(value.fields[key]) || value.fields[key].some((entry) => !validModelRef(entry)))) return false;
|
|
231
|
+
}
|
|
232
|
+
for (const key of ['harness_default', 'agent_default_model', 'agentDefaultModel']) {
|
|
233
|
+
if (value.fields[key] !== undefined && !validModelRef(value.fields[key])) return false;
|
|
234
|
+
}
|
|
235
|
+
for (const scope of ['worker', 'review']) {
|
|
236
|
+
const entry = value.fields[scope];
|
|
237
|
+
if (entry === undefined) continue;
|
|
238
|
+
if (!onlyKeys(entry, ['model_policy']) || !onlyKeys(entry.model_policy, ['priority', 'escalation_priority'])) return false;
|
|
239
|
+
for (const list of ['priority', 'escalation_priority']) {
|
|
240
|
+
if (entry.model_policy[list] !== undefined && (!Array.isArray(entry.model_policy[list]) || entry.model_policy[list].some((model) => !validModelRef(model)))) return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
190
246
|
function configProjection(config) {
|
|
191
247
|
const projection = { schema_version: 1, fields: {}, present_fields: [] };
|
|
192
248
|
for (const key of ['flash_model_priority', 'pro_model_priority', 'harness_default', 'agent_default_model', 'agentDefaultModel']) {
|
|
@@ -408,9 +464,29 @@ function recoveryDescriptor(manifest) {
|
|
|
408
464
|
|
|
409
465
|
function validateBackupManifest(manifest, { root, fs, paths, managedRoot, expectedProviderId }) {
|
|
410
466
|
const backupId = basename(root);
|
|
467
|
+
const manifestKeys = [
|
|
468
|
+
'schema_version', 'provider_id', 'plan', 'files', 'backup_digests',
|
|
469
|
+
'ownership_witnesses', 'mutation_journal', 'phase_journal',
|
|
470
|
+
'config_projection', 'config_revision', 'lifecycle_revision',
|
|
471
|
+
'routing_projection_digest', 'lifecycle_projection_digest',
|
|
472
|
+
'recovery_descriptor_digest', 'profile_revision', 'settings_revision',
|
|
473
|
+
'applied_profile_revision', 'applied_settings_revision',
|
|
474
|
+
'applied_config_revision', 'applied_lifecycle_revision',
|
|
475
|
+
'created_profile', 'created_settings', 'created_config', 'created_lifecycle',
|
|
476
|
+
'created_profile_identity', 'created_settings_identity',
|
|
477
|
+
'created_config_identity', 'created_lifecycle_identity',
|
|
478
|
+
];
|
|
479
|
+
const planKeys = [
|
|
480
|
+
'plan_id', 'provider_id', 'expected_revision', 'replacement_default',
|
|
481
|
+
'was_harness_default', 'replacement_default_model', 'harness_default_before',
|
|
482
|
+
'harness_default_authority', 'runtime_id_before',
|
|
483
|
+
'delete_runtime_id_before_restart', 'rollback_runtime_id_before_restart',
|
|
484
|
+
'declaration_authorities', 'expected_revisions', 'credential_refs',
|
|
485
|
+
];
|
|
411
486
|
if (!manifest || manifest.schema_version !== 1 || !validProviderId(manifest.provider_id)
|
|
487
|
+
|| !onlyKeys(manifest, manifestKeys)
|
|
412
488
|
|| (expectedProviderId && manifest.provider_id !== expectedProviderId)
|
|
413
|
-
|| !manifest.plan || manifest.plan.plan_id !== backupId
|
|
489
|
+
|| !onlyKeys(manifest.plan, planKeys) || !manifest.plan || manifest.plan.plan_id !== backupId
|
|
414
490
|
|| !safePlanId(manifest.plan.plan_id) || manifest.plan.provider_id !== manifest.provider_id
|
|
415
491
|
|| !Array.isArray(manifest.plan.declaration_authorities)
|
|
416
492
|
|| !manifest.files || typeof manifest.files !== 'object' || Array.isArray(manifest.files)
|
|
@@ -424,17 +500,55 @@ function validateBackupManifest(manifest, { root, fs, paths, managedRoot, expect
|
|
|
424
500
|
} catch {
|
|
425
501
|
throw backupInvalid();
|
|
426
502
|
}
|
|
503
|
+
for (const authority of manifest.plan.declaration_authorities) {
|
|
504
|
+
if (!onlyKeys(authority, ['kind', 'locator']) || typeof authority.kind !== 'string' || authority.kind.length > 64 || typeof authority.locator !== 'string' || authority.locator.length === 0 || authority.locator.length > 256) throw backupInvalid();
|
|
505
|
+
}
|
|
506
|
+
for (const ref of manifest.plan.credential_refs) {
|
|
507
|
+
if (!onlyKeys(ref, ['kind', 'name_or_handle', 'ownership']) || typeof ref.kind !== 'string' || ref.kind.length > 32 || typeof ref.name_or_handle !== 'string' || ref.name_or_handle.length === 0 || ref.name_or_handle.length > 128 || typeof ref.ownership !== 'string' || ref.ownership.length > 32) throw backupInvalid();
|
|
508
|
+
}
|
|
509
|
+
for (const key of ['harness_default_before', 'harness_default_authority']) {
|
|
510
|
+
const value = manifest.plan[key];
|
|
511
|
+
if (value !== undefined && (!onlyKeys(value, key === 'harness_default_before' ? ['provider', 'model'] : ['kind', 'locator']))) throw backupInvalid();
|
|
512
|
+
}
|
|
513
|
+
if (typeof manifest.plan.was_harness_default !== 'boolean') throw backupInvalid();
|
|
514
|
+
if (manifest.plan.replacement_default !== null && !validProviderId(manifest.plan.replacement_default)) throw backupInvalid();
|
|
515
|
+
if (manifest.plan.replacement_default_model !== null
|
|
516
|
+
&& (typeof manifest.plan.replacement_default_model !== 'string' || manifest.plan.replacement_default_model.length === 0 || manifest.plan.replacement_default_model.length > 256 || /[\r\n]/u.test(manifest.plan.replacement_default_model))) throw backupInvalid();
|
|
517
|
+
if (manifest.plan.was_harness_default === true
|
|
518
|
+
&& (!validProviderId(manifest.plan.replacement_default) || typeof manifest.plan.replacement_default_model !== 'string'
|
|
519
|
+
|| !manifest.plan.replacement_default_model.trim() || !manifest.plan.harness_default_before
|
|
520
|
+
|| typeof manifest.plan.harness_default_before.provider !== 'string' || !validProviderId(manifest.plan.harness_default_before.provider)
|
|
521
|
+
|| typeof manifest.plan.harness_default_before.model !== 'string' || !manifest.plan.harness_default_before.model.trim()
|
|
522
|
+
|| !manifest.plan.harness_default_authority || manifest.plan.harness_default_authority.kind !== 'harness-settings'
|
|
523
|
+
|| manifest.plan.harness_default_authority.locator !== 'agent-default-model')) throw backupInvalid();
|
|
524
|
+
for (const key of ['runtime_id_before', 'delete_runtime_id_before_restart', 'rollback_runtime_id_before_restart']) {
|
|
525
|
+
if (manifest.plan[key] !== undefined && (typeof manifest.plan[key] !== 'string' || manifest.plan[key].length === 0 || manifest.plan[key].length > 128 || /[\r\n]/u.test(manifest.plan[key]))) throw backupInvalid();
|
|
526
|
+
}
|
|
427
527
|
if (manifest.plan.expected_revision !== null && !/^[a-f0-9]{64}$/i.test(manifest.plan.expected_revision)) throw backupInvalid();
|
|
428
528
|
if (manifest.plan.expected_revisions !== undefined) {
|
|
429
529
|
if (!manifest.plan.expected_revisions || typeof manifest.plan.expected_revisions !== 'object' || Array.isArray(manifest.plan.expected_revisions)
|
|
530
|
+
|| !onlyKeys(manifest.plan.expected_revisions, ['profile', 'settings'])
|
|
430
531
|
|| !validRevision(manifest.plan.expected_revisions.profile) || !validRevision(manifest.plan.expected_revisions.settings)) throw backupInvalid();
|
|
431
532
|
}
|
|
533
|
+
if (manifest.plan.declaration_authorities.length > 16 || !Array.isArray(manifest.plan.credential_refs) || manifest.plan.credential_refs.length > 32) throw backupInvalid();
|
|
534
|
+
if (manifest.plan.harness_default_before !== undefined
|
|
535
|
+
&& (typeof manifest.plan.harness_default_before.provider !== 'string' || !validProviderId(manifest.plan.harness_default_before.provider)
|
|
536
|
+
|| typeof manifest.plan.harness_default_before.model !== 'string' || manifest.plan.harness_default_before.model.length === 0 || manifest.plan.harness_default_before.model.length > 256)) throw backupInvalid();
|
|
537
|
+
if (manifest.plan.harness_default_authority !== undefined
|
|
538
|
+
&& (typeof manifest.plan.harness_default_authority.kind !== 'string' || manifest.plan.harness_default_authority.kind.length > 64
|
|
539
|
+
|| typeof manifest.plan.harness_default_authority.locator !== 'string' || manifest.plan.harness_default_authority.locator.length > 256)) throw backupInvalid();
|
|
540
|
+
if (!onlyKeys(manifest.backup_digests, ['profile', 'settings', 'lifecycle'])) throw backupInvalid();
|
|
541
|
+
if (!onlyKeys(manifest.files, ['profile', 'settings', 'config', 'lifecycle'])) throw backupInvalid();
|
|
542
|
+
if (manifest.ownership_witnesses !== undefined && !onlyKeys(manifest.ownership_witnesses, ['profile', 'settings', 'config', 'lifecycle'])) throw backupInvalid();
|
|
543
|
+
if (manifest.phase_journal !== undefined && (!onlyKeys(manifest.phase_journal, ['phase', 'updated_at']) || !DELETE_PHASES.has(manifest.phase_journal.phase) || !validTimestamp(manifest.phase_journal.updated_at))) throw backupInvalid();
|
|
544
|
+
if (manifest.config_projection !== undefined && (!validConfigProjection(manifest.config_projection)
|
|
545
|
+
|| new Set(manifest.config_projection.present_fields).size !== manifest.config_projection.present_fields.length)) throw backupInvalid();
|
|
432
546
|
if (!Array.isArray(manifest.plan.credential_refs) || manifest.plan.credential_refs.some((ref) => !ref || typeof ref !== 'object'
|
|
433
547
|
|| typeof ref.kind !== 'string' || typeof ref.name_or_handle !== 'string' || typeof ref.ownership !== 'string')) throw backupInvalid();
|
|
434
548
|
const authorityKinds = new Set(manifest.plan.declaration_authorities.map((authority) => authority?.kind));
|
|
435
549
|
for (const key of FILE_KEYS) {
|
|
436
550
|
const entry = manifest.files[key];
|
|
437
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)
|
|
551
|
+
if (!onlyKeys(entry, ['existed', 'managed']) || !entry || typeof entry !== 'object' || Array.isArray(entry)
|
|
438
552
|
|| typeof entry.existed !== 'boolean' || typeof entry.managed !== 'boolean') {
|
|
439
553
|
throw backupInvalid();
|
|
440
554
|
}
|
|
@@ -472,7 +586,7 @@ function validateBackupManifest(manifest, { root, fs, paths, managedRoot, expect
|
|
|
472
586
|
}
|
|
473
587
|
if (paths.settings) {
|
|
474
588
|
const entry = manifest.files.settings;
|
|
475
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)
|
|
589
|
+
if (!onlyKeys(entry, ['existed', 'managed']) || !entry || typeof entry !== 'object' || Array.isArray(entry)
|
|
476
590
|
|| typeof entry.existed !== 'boolean' || typeof entry.managed !== 'boolean') throw backupInvalid();
|
|
477
591
|
const expectedManaged = authorityKinds.has('harness-settings') || manifest.plan.was_harness_default === true;
|
|
478
592
|
if (entry.managed !== expectedManaged || (!entry.existed && manifest.backup_digests.settings !== undefined)) throw backupInvalid();
|
|
@@ -493,11 +607,11 @@ function validateBackupManifest(manifest, { root, fs, paths, managedRoot, expect
|
|
|
493
607
|
if (applied !== undefined && !/^[a-f0-9]{64}$/i.test(applied)) throw backupInvalid();
|
|
494
608
|
if (created !== undefined && typeof created !== 'boolean') throw backupInvalid();
|
|
495
609
|
const identity = manifest[`created_${key}_identity`];
|
|
496
|
-
if (identity !== undefined && (!identity
|
|
610
|
+
if (identity !== undefined && (!onlyKeys(identity, ['dev', 'ino']) || typeof identity.dev !== 'string' || !/^\d+$/u.test(identity.dev) || typeof identity.ino !== 'string' || !/^\d+$/u.test(identity.ino))) throw backupInvalid();
|
|
497
611
|
if (created === true && !identity) throw backupInvalid();
|
|
498
612
|
}
|
|
499
|
-
if (manifest.mutation_journal !== undefined && (
|
|
500
|
-
if (manifest.phase_journal !== undefined && (
|
|
613
|
+
if (manifest.mutation_journal !== undefined && (!onlyKeys(manifest.mutation_journal, ['profile', 'settings', 'config', 'lifecycle']))) throw backupInvalid();
|
|
614
|
+
if (manifest.phase_journal !== undefined && (!onlyKeys(manifest.phase_journal, ['phase', 'updated_at']) || !DELETE_PHASES.has(manifest.phase_journal.phase) || !validTimestamp(manifest.phase_journal.updated_at))) throw backupInvalid();
|
|
501
615
|
if (manifest.ownership_witnesses !== undefined && (typeof manifest.ownership_witnesses !== 'object' || Array.isArray(manifest.ownership_witnesses))) throw backupInvalid();
|
|
502
616
|
for (const key of ['profile', 'settings', 'config', 'lifecycle']) {
|
|
503
617
|
const witnessName = manifest.ownership_witnesses?.[key];
|
|
@@ -520,8 +634,8 @@ function validateBackupManifest(manifest, { root, fs, paths, managedRoot, expect
|
|
|
520
634
|
}
|
|
521
635
|
}
|
|
522
636
|
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();
|
|
637
|
+
if (!['profile', 'settings', 'config', 'lifecycle'].includes(key) || !onlyKeys(mutation, ['next_revision', 'created', 'witness', 'prepared_at']) || !mutation || typeof mutation !== 'object'
|
|
638
|
+
|| !/^[a-f0-9]{64}$/i.test(mutation.next_revision) || typeof mutation.created !== 'boolean' || !validTimestamp(mutation.prepared_at)) throw backupInvalid();
|
|
525
639
|
if (mutation.witness !== undefined) {
|
|
526
640
|
if (typeof mutation.witness !== 'string') throw backupInvalid();
|
|
527
641
|
assertManagedPath(mutation.witness, managedRoot);
|
|
@@ -575,16 +689,19 @@ export function createProviderDeleteFileHooks({
|
|
|
575
689
|
afterLockAcquired = null,
|
|
576
690
|
afterOwnedReplacePublished = null,
|
|
577
691
|
afterMutationJournaled = null,
|
|
692
|
+
sharedLockFile = null,
|
|
578
693
|
fs = { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, lstatSync },
|
|
579
694
|
} = {}) {
|
|
580
695
|
if (typeof backupDir !== 'string' || !backupDir.trim()) throw new TypeError('provider delete backup directory is required');
|
|
581
696
|
const paths = fileMap({ profileFile, settingsFile, configFile, lifecycleFile });
|
|
582
697
|
validatePaths(paths);
|
|
583
698
|
const managedRoot = dirname(resolvePath(backupDir));
|
|
699
|
+
const providerStoreLockFile = sharedLockFile ? resolvePath(sharedLockFile) : null;
|
|
584
700
|
for (const key of [...FILE_KEYS, ...(settingsFile ? ['settings'] : [])]) {
|
|
585
701
|
if (paths[key]) assertManagedPath(paths[key], managedRoot);
|
|
586
702
|
}
|
|
587
703
|
assertManagedPath(backupDir, managedRoot);
|
|
704
|
+
if (providerStoreLockFile) assertManagedPath(providerStoreLockFile, managedRoot);
|
|
588
705
|
let activeBackup = null;
|
|
589
706
|
let lockPath = null;
|
|
590
707
|
let lockOwned = false;
|
|
@@ -606,8 +723,7 @@ export function createProviderDeleteFileHooks({
|
|
|
606
723
|
try {
|
|
607
724
|
fs.writeFileSync(stage, content, { flag: 'wx' });
|
|
608
725
|
assertManagedPath(stage, managedRoot);
|
|
609
|
-
|
|
610
|
-
assertManagedPath(file, managedRoot);
|
|
726
|
+
publishHardlink(stage, file, managedRoot);
|
|
611
727
|
} finally {
|
|
612
728
|
try { fs.rmSync(stage, { force: true }); } catch {}
|
|
613
729
|
}
|
|
@@ -637,7 +753,7 @@ export function createProviderDeleteFileHooks({
|
|
|
637
753
|
}
|
|
638
754
|
};
|
|
639
755
|
|
|
640
|
-
const
|
|
756
|
+
const recoverDeleteLock = async ({ force = false } = {}) => {
|
|
641
757
|
const guardPath = join(backupDir, '.delete.reclaim.lock');
|
|
642
758
|
const canonicalPath = join(backupDir, '.delete.lock');
|
|
643
759
|
const recoveryPath = join(backupDir, '.delete.recovery.lock');
|
|
@@ -778,7 +894,7 @@ export function createProviderDeleteFileHooks({
|
|
|
778
894
|
}
|
|
779
895
|
};
|
|
780
896
|
|
|
781
|
-
const
|
|
897
|
+
const acquireDeleteLock = async () => {
|
|
782
898
|
if (lockOwned) return;
|
|
783
899
|
assertManagedPath(backupDir, managedRoot);
|
|
784
900
|
const backupParent = dirname(backupDir);
|
|
@@ -1056,7 +1172,7 @@ export function createProviderDeleteFileHooks({
|
|
|
1056
1172
|
const witness = durableWitnessPath(key);
|
|
1057
1173
|
const previous = activeBackup.manifest.ownership_witnesses?.[key];
|
|
1058
1174
|
try {
|
|
1059
|
-
|
|
1175
|
+
publishHardlink(source, witness, managedRoot);
|
|
1060
1176
|
const sourceIdentity = fileIdentity(source);
|
|
1061
1177
|
const witnessIdentity = fileIdentity(witness);
|
|
1062
1178
|
if (!sameFileIdentity(sourceIdentity, witnessIdentity)) {
|
|
@@ -1291,6 +1407,16 @@ export function createProviderDeleteFileHooks({
|
|
|
1291
1407
|
if (authorityKinds.has('crew-profile') && sourceRevisions.profile) manifest.profile_revision = sourceRevisions.profile;
|
|
1292
1408
|
if ((authorityKinds.has('harness-settings') || managesHarnessDefault) && sourceRevisions.settings) manifest.settings_revision = sourceRevisions.settings;
|
|
1293
1409
|
manifest.recovery_descriptor_digest = sha256(JSON.stringify(recoveryDescriptor(manifest)));
|
|
1410
|
+
// Validate the exact durable record before returning success. A
|
|
1411
|
+
// parseable-but-unsupported config projection must fail before any
|
|
1412
|
+
// destructive phase can start, rather than becoming unreopenable after a
|
|
1413
|
+
// crash.
|
|
1414
|
+
try {
|
|
1415
|
+
validateBackupManifest(manifest, { root, fs, paths, managedRoot, expectedProviderId: plan.provider_id });
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
try { fs.rmSync(root, { recursive: true, force: true }); } catch {}
|
|
1418
|
+
throw error;
|
|
1419
|
+
}
|
|
1294
1420
|
activeBackup = { root, manifest };
|
|
1295
1421
|
persistManifest();
|
|
1296
1422
|
return { ok: true, backup_id: planId };
|
|
@@ -1623,7 +1749,7 @@ export function createProviderDeleteFileHooks({
|
|
|
1623
1749
|
setPhase('ROLLBACK_RESTORED');
|
|
1624
1750
|
};
|
|
1625
1751
|
|
|
1626
|
-
const
|
|
1752
|
+
const releaseDeleteLock = async () => {
|
|
1627
1753
|
if (!lockOwned || !lockPath) return;
|
|
1628
1754
|
try {
|
|
1629
1755
|
let owner = null;
|
|
@@ -1634,6 +1760,28 @@ export function createProviderDeleteFileHooks({
|
|
|
1634
1760
|
} finally { lockOwned = false; lockPath = null; lockOwnerToken = null; }
|
|
1635
1761
|
};
|
|
1636
1762
|
|
|
1763
|
+
let sharedStoreLock = null;
|
|
1764
|
+
const acquireLock = async () => {
|
|
1765
|
+
if (lockOwned) return;
|
|
1766
|
+
if (!providerStoreLockFile) return acquireDeleteLock();
|
|
1767
|
+
sharedStoreLock = await acquireProviderStoreLock(providerStoreLockFile);
|
|
1768
|
+
try { return await acquireDeleteLock(); }
|
|
1769
|
+
catch (error) {
|
|
1770
|
+
if (sharedStoreLock) { await sharedStoreLock.release(); sharedStoreLock = null; }
|
|
1771
|
+
throw error;
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
const release = async () => {
|
|
1775
|
+
await releaseDeleteLock();
|
|
1776
|
+
if (sharedStoreLock) { await sharedStoreLock.release(); sharedStoreLock = null; }
|
|
1777
|
+
};
|
|
1778
|
+
const recoverLock = async (options = {}) => {
|
|
1779
|
+
if (!providerStoreLockFile) return recoverDeleteLock(options);
|
|
1780
|
+
const shared = await acquireProviderStoreLock(providerStoreLockFile);
|
|
1781
|
+
try { return await recoverDeleteLock(options); }
|
|
1782
|
+
finally { await shared.release(); }
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1637
1785
|
const quarantine = async (entryName) => {
|
|
1638
1786
|
const safeName = safeRecoveryEntryName(entryName);
|
|
1639
1787
|
if (!safeName) throw Object.assign(new Error('provider recovery entry name is invalid'), { code: 'PROVIDER_DELETE_PLAN_INVALID' });
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// and (where explicitly marked) profile provenance. This module only joins
|
|
5
5
|
// those observations into a bounded record set for lifecycle/readiness UIs.
|
|
6
6
|
|
|
7
|
+
import { classifyCredentialReference } from './credential-reference.mjs';
|
|
8
|
+
|
|
7
9
|
const LIFECYCLE_ORIGINS = new Set(['builtin', 'profile-managed', 'dynamic', 'unknown']);
|
|
8
10
|
const OWNERSHIPS = new Set(['harness', 'crew-managed-profile', 'user-managed-profile', 'dynamic-user', 'unknown']);
|
|
9
11
|
const MUTABLE_AUTHORITY_KINDS = new Set(['crew-profile', 'harness-settings']);
|
|
@@ -21,12 +23,15 @@ function normalizeCredentialRefs(declarations) {
|
|
|
21
23
|
const entries = Array.isArray(declarations) ? declarations : [declarations];
|
|
22
24
|
const seen = new Map();
|
|
23
25
|
const refs = [];
|
|
26
|
+
let redacted = false;
|
|
24
27
|
for (const declaration of entries) {
|
|
25
28
|
const raw = declaration?.credential_refs ?? declaration?.credential_ref;
|
|
26
29
|
const values = Array.isArray(raw) ? raw : raw === undefined ? [] : [raw];
|
|
27
30
|
for (const candidate of values) {
|
|
28
|
-
const name = text(typeof candidate === 'object' ? candidate.name_or_handle ?? candidate.name : candidate);
|
|
29
31
|
const kind = text(typeof candidate === 'object' ? candidate.kind : null) ?? 'env';
|
|
32
|
+
const classified = classifyCredentialReference(candidate, { kind });
|
|
33
|
+
if (classified.redacted) redacted = true;
|
|
34
|
+
const name = classified.value;
|
|
30
35
|
const ownership = text(typeof candidate === 'object' ? candidate.ownership : null) ?? 'crew';
|
|
31
36
|
const normalized = {
|
|
32
37
|
kind: ['env', 'crew-store', 'harness-store', 'unknown'].includes(kind) ? kind : 'unknown',
|
|
@@ -44,7 +49,7 @@ function normalizeCredentialRefs(declarations) {
|
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
|
-
return refs;
|
|
52
|
+
return { refs, redacted };
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
function findPriorityIndex(policy, key, providerId) {
|
|
@@ -105,6 +110,8 @@ export function buildProviderInventory({ catalog = {}, declarations = [], policy
|
|
|
105
110
|
const records = ids.map((id) => {
|
|
106
111
|
const catalogEntry = catalogFor(catalogProviders, id);
|
|
107
112
|
const providerDeclarations = declarationsFor(safeDeclarations, id);
|
|
113
|
+
const baseDeclarations = providerDeclarations.filter((entry) => normalizeAuthority(entry?.declaration_authority)?.kind === 'crew-profile');
|
|
114
|
+
const userDeclarations = providerDeclarations.filter((entry) => normalizeAuthority(entry?.declaration_authority)?.kind === 'harness-settings');
|
|
108
115
|
const declaration = providerDeclarations[0] ?? null;
|
|
109
116
|
const declared = declaration !== null;
|
|
110
117
|
const catalogued = catalogEntry !== null;
|
|
@@ -129,6 +136,7 @@ export function buildProviderInventory({ catalog = {}, declarations = [], policy
|
|
|
129
136
|
const deleteBlocker = immutable ? 'PROVIDER_BUILTIN_IMMUTABLE' : deleteCapability === 'supported' ? null : 'PROVIDER_DELETE_SOURCE_UNRESOLVED';
|
|
130
137
|
const origin = normalizeOrigin(declaration?.origin ?? (immutable ? 'builtin' : declared ? 'profile-managed' : 'unknown'));
|
|
131
138
|
const ownership = normalizeOwnership(declaration?.ownership ?? (immutable ? 'harness' : declared ? 'crew-managed-profile' : 'unknown'));
|
|
139
|
+
const credentialProjection = normalizeCredentialRefs(providerDeclarations);
|
|
132
140
|
return {
|
|
133
141
|
id,
|
|
134
142
|
display_name: text(declaration?.display_name) ?? text(declaration?.name) ?? text(catalogEntry?.name) ?? id,
|
|
@@ -141,6 +149,11 @@ export function buildProviderInventory({ catalog = {}, declarations = [], policy
|
|
|
141
149
|
present: declared,
|
|
142
150
|
},
|
|
143
151
|
declaration_authorities: uniqueAuthorities,
|
|
152
|
+
layers: {
|
|
153
|
+
base: [...new Set(baseDeclarations.map((entry) => normalizeAuthority(entry.declaration_authority)?.kind).filter(Boolean))],
|
|
154
|
+
user: [...new Set(userDeclarations.map((entry) => normalizeAuthority(entry.declaration_authority)?.kind).filter(Boolean))],
|
|
155
|
+
},
|
|
156
|
+
native_removable: userDeclarations.length > 0 && baseDeclarations.length === 0,
|
|
144
157
|
delete_capability: deleteCapability,
|
|
145
158
|
...(deleteBlocker ? { delete_blocker: deleteBlocker } : {}),
|
|
146
159
|
models: modelIds(catalogEntry),
|
|
@@ -150,7 +163,8 @@ export function buildProviderInventory({ catalog = {}, declarations = [], policy
|
|
|
150
163
|
enabled: !tombstoned && declaration?.enabled !== false,
|
|
151
164
|
catalogued,
|
|
152
165
|
},
|
|
153
|
-
credential_refs:
|
|
166
|
+
credential_refs: credentialProjection.refs,
|
|
167
|
+
...(credentialProjection.redacted ? { credential_status: 'present-redacted' } : {}),
|
|
154
168
|
references: {
|
|
155
169
|
harness_default: harnessDefault === id,
|
|
156
170
|
...(harnessDefault === id && normalizeAuthority(catalog?.harness_default_authority) ? { harness_default_authority: normalizeAuthority(catalog.harness_default_authority) } : {}),
|