@ran-sh/dsh-crew 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +4 -0
  2. package/README.zh.md +4 -0
  3. package/codex/AGENTS.md +101 -92
  4. package/docs/ui-surfaces.md +48 -21
  5. package/lib/client.js +235 -34
  6. package/official-web-bridge/lib/client.js +368 -4532
  7. package/official-web-bridge/package.json +1 -2
  8. package/package.json +130 -67
  9. package/scripts/build-client.mjs +30 -15
  10. package/scripts/remove-legacy-official-bridge.ps1 +89 -0
  11. package/scripts/setup.mjs +152 -140
  12. package/scripts/verify-npm-install.mjs +311 -310
  13. package/scripts/verify-official-bridge-e2e.mjs +12 -1
  14. package/src/client/host-readiness.mjs +62 -59
  15. package/src/client/index.tsx +117 -24
  16. package/src/client/quick-entry.tsx +10 -0
  17. package/src/client/quick-panel.tsx +295 -0
  18. package/src/client/surface-detection.mjs +43 -30
  19. package/src/credential-reference.mjs +38 -0
  20. package/src/dsh-cli-runtime.mjs +500 -40
  21. package/src/dsh-cohort.mjs +20 -0
  22. package/src/hub/index.mjs +1602 -1016
  23. package/src/install/npx-lifecycle.mjs +1983 -468
  24. package/src/install/official-web.mjs +36 -73
  25. package/src/jobs.mjs +20 -24
  26. package/src/model-catalog.mjs +8 -1
  27. package/src/official-web-bridge.mjs +329 -249
  28. package/src/provider-delete-adapters.mjs +165 -17
  29. package/src/provider-inventory.mjs +17 -3
  30. package/src/provider-layer-migration-adapters.mjs +759 -0
  31. package/src/provider-layer-migration.mjs +198 -0
  32. package/src/provider-lifecycle-state.mjs +4 -1
  33. package/src/provider-profile-store.mjs +193 -4
  34. package/src/provider-settings-store.mjs +82 -6
  35. package/src/provider-store-lock.mjs +67 -0
  36. package/src/runtime-identity.mjs +24 -1
  37. package/src/supervisor/restart-request.mjs +202 -0
  38. package/windows/start-dsh-crew.ps1 +783 -370
  39. package/worker.cordis.yml +32 -46
  40. package/zcode/AGENTS.md +35 -26
@@ -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
- linkSync(temp, file);
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
- linkSync(witness, file);
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
- linkSync(witness, stage);
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 || typeof identity !== 'object' || typeof identity.dev !== 'string' || typeof identity.ino !== 'string')) throw backupInvalid();
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 && (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();
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
- linkSync(stage, file);
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 recoverLock = async ({ force = false } = {}) => {
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 acquireLock = async () => {
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
- linkSync(source, witness);
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 release = async () => {
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: normalizeCredentialRefs(providerDeclarations),
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) } : {}),