@dsh-enhanced/lark-channel 0.1.23 → 0.1.30

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 (43) hide show
  1. package/README.md +14 -3
  2. package/docs/operations.md +8 -0
  3. package/docs/setup.md +3 -0
  4. package/lib/adapter.d.ts +7 -0
  5. package/lib/adapter.d.ts.map +1 -1
  6. package/lib/adapter.js +18 -4
  7. package/lib/adapter.js.map +1 -1
  8. package/lib/config.d.ts +3 -0
  9. package/lib/config.d.ts.map +1 -1
  10. package/lib/config.js +8 -0
  11. package/lib/config.js.map +1 -1
  12. package/lib/index.d.ts +2 -1
  13. package/lib/index.d.ts.map +1 -1
  14. package/lib/index.js +1 -1
  15. package/lib/index.js.map +1 -1
  16. package/lib/sdk.d.ts +17 -2
  17. package/lib/sdk.d.ts.map +1 -1
  18. package/lib/sdk.js +170 -76
  19. package/lib/sdk.js.map +1 -1
  20. package/lib/service.d.ts +17 -0
  21. package/lib/service.d.ts.map +1 -1
  22. package/lib/service.js +68 -5
  23. package/lib/service.js.map +1 -1
  24. package/lib/setup-profile.d.ts.map +1 -1
  25. package/lib/setup-profile.js +1 -0
  26. package/lib/setup-profile.js.map +1 -1
  27. package/lib/setup.d.ts +4 -0
  28. package/lib/setup.d.ts.map +1 -1
  29. package/lib/setup.js +313 -242
  30. package/lib/setup.js.map +1 -1
  31. package/lib/supervised-growth-profile.d.ts +27 -0
  32. package/lib/supervised-growth-profile.d.ts.map +1 -1
  33. package/lib/supervised-growth-profile.js +112 -0
  34. package/lib/supervised-growth-profile.js.map +1 -1
  35. package/lib/supervised-growth-setup.d.ts +82 -1
  36. package/lib/supervised-growth-setup.d.ts.map +1 -1
  37. package/lib/supervised-growth-setup.js +430 -85
  38. package/lib/supervised-growth-setup.js.map +1 -1
  39. package/lib/types.d.ts +12 -0
  40. package/lib/types.d.ts.map +1 -1
  41. package/lib/version.d.ts +1 -1
  42. package/lib/version.js +1 -1
  43. package/package.json +14 -14
@@ -7,7 +7,7 @@ import { dirname, isAbsolute, join } from 'node:path';
7
7
  import { isDeepStrictEqual } from 'node:util';
8
8
  import { isMap, isSeq, parseDocument } from 'yaml';
9
9
  import { installDshResidentService, residentServiceKind } from './resident.js';
10
- import { isMainEntry } from './setup.js';
10
+ import { isMainEntry, withDshHomeLifecycleLock } from './setup.js';
11
11
  import { assertEffectiveSupervisedGrowthConfig, configureSupervisedGrowthProfilePatch, supervisedGrowthDatabasePaths, supervisedGrowthBindingQuery, supervisedGrowthAnalystRuntimeConfig, supervisedGrowthRecoveryRuntimeConfig, } from './supervised-growth-profile.js';
12
12
  const keyPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
13
13
  function argumentValue(argv, index, option) {
@@ -240,6 +240,349 @@ export async function restartAndVerifySupervisedGrowthResident(input) {
240
240
  await (input.verify ?? verifySupervisedGrowthResidentService)(service);
241
241
  return service;
242
242
  }
243
+ export const SUPERVISED_GROWTH_LIFECYCLE_ATTESTATION_PROTOCOL = 'dsh-enhanced/supervised-growth-lifecycle-attestation/v1';
244
+ export const SUPERVISED_GROWTH_MANAGED_AUTOMATION_PROJECTION_PROTOCOL = 'dsh-enhanced/supervised-growth-managed-automations/v1';
245
+ function canonicalJson(value) {
246
+ if (value === null || typeof value !== 'object')
247
+ return JSON.stringify(value) ?? 'null';
248
+ if (Array.isArray(value))
249
+ return `[${value.map(canonicalJson).join(',')}]`;
250
+ const record = value;
251
+ return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
252
+ }
253
+ function digestJson(value) {
254
+ return createHash('sha256').update(canonicalJson(value)).digest('hex');
255
+ }
256
+ function deepFreeze(value) {
257
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
258
+ for (const child of Object.values(value))
259
+ deepFreeze(child);
260
+ Object.freeze(value);
261
+ }
262
+ return value;
263
+ }
264
+ function sortedAutomationRecords(records) {
265
+ return Object.freeze([...records].sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
266
+ }
267
+ function effectivePluginConfig(effectiveConfig, id) {
268
+ const document = parseDocument(effectiveConfig, { uniqueKeys: true });
269
+ if (document.errors.length > 0 || !isSeq(document.contents)) {
270
+ throw new Error('supervised-growth lifecycle: effective profile is invalid');
271
+ }
272
+ const rows = document.contents.items.filter(item => isMap(item)
273
+ && item.get('id') === id);
274
+ if (rows.length !== 1 || rows[0].get('disabled') === true) {
275
+ throw new Error(`supervised-growth lifecycle: ${id} must be uniquely enabled`);
276
+ }
277
+ const config = rows[0].get('config', true);
278
+ if (!isMap(config))
279
+ throw new Error(`supervised-growth lifecycle: ${id} config is invalid`);
280
+ return config;
281
+ }
282
+ function assertLifecycleProviderContract(input) {
283
+ const expected = input.stage === 'preview' ? ['larkChannel'] : [];
284
+ if (!Array.isArray(input.externalProviderExemptions)
285
+ || !isDeepStrictEqual(input.externalProviderExemptions, expected)) {
286
+ throw new Error(`supervised-growth lifecycle: ${input.stage} external provider exemptions are invalid`);
287
+ }
288
+ const lark = effectivePluginConfig(input.effectiveConfig, 'dsh-enhanced-lark-channel');
289
+ const health = effectivePluginConfig(input.effectiveConfig, 'dsh-enhanced-assistant-health');
290
+ const providers = health.get('requiredProviders', true);
291
+ const requiredProviders = isSeq(providers) ? providers.toJSON() : undefined;
292
+ if (input.stage === 'preview') {
293
+ if (lark.get('enabled') !== false) {
294
+ throw new Error('supervised-growth lifecycle: preview Lark channel must be disabled');
295
+ }
296
+ if (requiredProviders === undefined || requiredProviders.includes('larkChannel')) {
297
+ throw new Error('supervised-growth lifecycle: preview Health must exempt larkChannel');
298
+ }
299
+ }
300
+ else {
301
+ if (lark.get('enabled') !== true) {
302
+ throw new Error('supervised-growth lifecycle: active Lark channel must be enabled');
303
+ }
304
+ if (requiredProviders === undefined || !requiredProviders.includes('larkChannel')) {
305
+ throw new Error('supervised-growth lifecycle: active Health must require larkChannel');
306
+ }
307
+ }
308
+ return Object.freeze([...expected]);
309
+ }
310
+ function sameRecoveryObservation(left, right) {
311
+ return left.snapshotDigest === right.snapshotDigest
312
+ && left.database.device === right.database.device
313
+ && left.database.inode === right.database.inode
314
+ && left.database.digest === right.database.digest;
315
+ }
316
+ function sameAutomationObservation(left, right) {
317
+ return left.inventoryDigest === right.inventoryDigest
318
+ && left.database.device === right.database.device
319
+ && left.database.inode === right.database.inode
320
+ && left.database.digest === right.database.digest
321
+ && isDeepStrictEqual(left.sidecars, right.sidecars)
322
+ && left.inFlightCount === right.inFlightCount
323
+ && isDeepStrictEqual(left.records, right.records);
324
+ }
325
+ function recoveryJobIdentity(effectiveConfig) {
326
+ const config = supervisedGrowthRecoveryRuntimeConfig(effectiveConfig);
327
+ const jobs = config['jobs'];
328
+ if (!Array.isArray(jobs))
329
+ throw new Error('supervised-growth lifecycle: Recovery jobs are invalid');
330
+ const matches = jobs.filter((job) => typeof job === 'object'
331
+ && job !== null && !Array.isArray(job) && job['id'] === 'supervised-growth');
332
+ if (matches.length !== 1) {
333
+ throw new Error('supervised-growth lifecycle: exact Recovery job is missing or duplicated');
334
+ }
335
+ const job = matches[0];
336
+ if ((job['activationState'] !== 'preview' && job['activationState'] !== 'active')
337
+ || typeof job['activationNonce'] !== 'string' || job['activationNonce'].trim() === ''
338
+ || !/^[a-f\d]{64}$/u.test(String(job['catalogDigest']))) {
339
+ throw new Error('supervised-growth lifecycle: Recovery activation identity is invalid');
340
+ }
341
+ return {
342
+ activationState: job['activationState'],
343
+ activationNonce: job['activationNonce'],
344
+ catalogDigest: String(job['catalogDigest']),
345
+ };
346
+ }
347
+ function configuredRecoveryIdentities(effectiveConfig) {
348
+ const jobs = supervisedGrowthRecoveryRuntimeConfig(effectiveConfig)['jobs'];
349
+ if (!Array.isArray(jobs) || jobs.length > 100) {
350
+ throw new Error('supervised-growth lifecycle: Recovery jobs are invalid');
351
+ }
352
+ const result = new Map();
353
+ for (const [index, raw] of jobs.entries()) {
354
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
355
+ throw new Error(`supervised-growth lifecycle: Recovery job ${index} is invalid`);
356
+ }
357
+ const job = raw;
358
+ const id = job['id'];
359
+ const activationState = job['activationState'] ?? 'paused';
360
+ const activationNonce = job['activationNonce'];
361
+ if (typeof id !== 'string' || id.trim() === ''
362
+ || (activationState !== 'active' && activationState !== 'paused' && activationState !== 'preview')
363
+ || typeof activationNonce !== 'string' || activationNonce.trim() === '') {
364
+ throw new Error(`supervised-growth lifecycle: Recovery job ${index} identity is invalid`);
365
+ }
366
+ const automationId = `recovery:${id.normalize('NFC').trim()}`;
367
+ if (result.has(automationId)) {
368
+ throw new Error('supervised-growth lifecycle: Recovery automation ids are duplicated');
369
+ }
370
+ result.set(automationId, Object.freeze({
371
+ activationState, activationNonce: activationNonce.normalize('NFC').trim(),
372
+ }));
373
+ }
374
+ return result;
375
+ }
376
+ async function lifecycleDependencies(dependencies) {
377
+ const [automations, recovery] = await Promise.all([
378
+ import('@dsh-enhanced/assistant-automations'),
379
+ import('@dsh-enhanced/assistant-recovery'),
380
+ ]);
381
+ return {
382
+ inspectRecovery: dependencies.inspectRecovery ?? recovery.inspectRecoveryOperatorSnapshot,
383
+ inspectAutomations: dependencies.inspectAutomations ?? automations.inspectAutomationsOperatorSnapshot,
384
+ expectedBootstrap: dependencies.expectedBootstrap ?? expectedSupervisedGrowthRecoveryBootstrap,
385
+ expectedAnalystDefinition: dependencies.expectedAnalystDefinition
386
+ ?? expectedSupervisedGrowthAnalystDefinition,
387
+ definitionDigest: dependencies.definitionDigest ?? automations.automationDefinitionDigest,
388
+ };
389
+ }
390
+ async function expectedManagedAutomationRecords(input, dependencies) {
391
+ const recovery = await import('@dsh-enhanced/assistant-recovery');
392
+ const normalized = recovery.normalizeRecoveryConfig({
393
+ ...supervisedGrowthRecoveryRuntimeConfig(input.effectiveConfig),
394
+ databasePath: input.recoveryDatabasePath,
395
+ });
396
+ const supervisedJob = normalized.jobs.find(candidate => candidate.id === 'supervised-growth');
397
+ if (supervisedJob === undefined || supervisedJob.activationState !== input.stage) {
398
+ throw new Error('supervised-growth lifecycle: effective Recovery stage does not match the requested target');
399
+ }
400
+ const analystDefinition = await dependencies.expectedAnalystDefinition(input.effectiveConfig);
401
+ const records = normalized.jobs.map(job => [`recovery:${job.id}`, Object.freeze({
402
+ owner: 'dsh-enhanced-assistant-recovery',
403
+ status: job.activationState === 'active' ? 'active' : 'paused',
404
+ definitionHash: dependencies.definitionDigest(recovery.recoveryAutomationDefinition(job, normalized.maxStepDurationMs, job.activationState === 'preview' ? 'preview' : 'production')),
405
+ })]);
406
+ records.push(['heartbeat:supervised-growth-analyst', Object.freeze({
407
+ owner: 'assistant-heartbeat',
408
+ status: input.stage === 'active' ? 'active' : 'paused',
409
+ definitionHash: dependencies.definitionDigest(analystDefinition),
410
+ })]);
411
+ return new Map(records);
412
+ }
413
+ /** Build the exact content-free managed projection before a Host is started. */
414
+ export async function expectedSupervisedGrowthManagedAutomationDigest(input, supplied = {}) {
415
+ const dependencies = await lifecycleDependencies(supplied);
416
+ const expected = await expectedManagedAutomationRecords(input, dependencies);
417
+ const records = Object.freeze([...expected.entries()].map(([id, record]) => Object.freeze({
418
+ id, owner: record.owner, status: record.status, definitionHash: record.definitionHash,
419
+ })).sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
420
+ const unsigned = Object.freeze({
421
+ protocol: SUPERVISED_GROWTH_MANAGED_AUTOMATION_PROJECTION_PROTOCOL,
422
+ records,
423
+ });
424
+ return deepFreeze({ ...unsigned, digest: digestJson(unsigned) });
425
+ }
426
+ function assertManagedAutomationInventory(input) {
427
+ if (input.snapshot.inFlightCount !== 0
428
+ || input.snapshot.records.some(record => record.runningTaskCount !== 0)) {
429
+ throw new Error('supervised-growth lifecycle: Automations has in-flight work');
430
+ }
431
+ const records = new Map(input.snapshot.records.map(record => [record.id, record]));
432
+ for (const [id, expected] of input.expected) {
433
+ const actual = records.get(id);
434
+ if (actual === undefined || actual.owner !== expected.owner || actual.status !== expected.status
435
+ || actual.definitionHash !== expected.definitionHash) {
436
+ throw new Error(`supervised-growth lifecycle: managed automation ${id} does not match the exact ${input.stage} definition`);
437
+ }
438
+ }
439
+ const legacy = records.get('heartbeat:supervised-growth');
440
+ if (legacy?.status === 'active') {
441
+ throw new Error('supervised-growth lifecycle: legacy supervised-growth automation is active');
442
+ }
443
+ for (const record of input.snapshot.records) {
444
+ if (record.status !== 'active' || record.owner !== 'dsh-enhanced-assistant-recovery')
445
+ continue;
446
+ if (!input.expected.has(record.id)) {
447
+ throw new Error(`supervised-growth lifecycle: unexpected active managed automation ${record.id}`);
448
+ }
449
+ }
450
+ }
451
+ function assertRecoverySnapshot(input) {
452
+ const bootstrap = input.snapshot.bootstrap;
453
+ if (input.previousGeneration !== undefined
454
+ && bootstrap.generation <= input.previousGeneration) {
455
+ throw new Error('supervised-growth lifecycle: Recovery bootstrap generation did not strictly advance');
456
+ }
457
+ if (bootstrap.status !== 'succeeded' || !bootstrap.attestationValid
458
+ || bootstrap.attestationSetDigest !== input.expected.attestationSetDigest
459
+ || !isDeepStrictEqual(bootstrap.attestations, input.expected.attestations)) {
460
+ throw new Error(`supervised-growth lifecycle: Recovery ${input.stage} proof does not match the exact effective plan`);
461
+ }
462
+ const configured = configuredRecoveryIdentities(input.effectiveConfig);
463
+ if (bootstrap.attestations.length !== configured.size) {
464
+ throw new Error('supervised-growth lifecycle: Recovery attestation set is incomplete');
465
+ }
466
+ for (const attestation of bootstrap.attestations) {
467
+ const job = configured.get(attestation.automationId);
468
+ if (job === undefined || job.activationState !== attestation.activationState
469
+ || job.activationNonce !== attestation.activationNonce) {
470
+ throw new Error('supervised-growth lifecycle: Recovery attestation identity does not match effective config');
471
+ }
472
+ }
473
+ }
474
+ async function observeLifecycle(input, dependencies) {
475
+ const expectedBootstrap = await dependencies.expectedBootstrap(input.effectiveConfig, input.recoveryDatabasePath);
476
+ const expectedManaged = await expectedManagedAutomationRecords(input, dependencies);
477
+ const [recovery, automations] = await Promise.all([
478
+ dependencies.inspectRecovery(input.recoveryDatabasePath),
479
+ dependencies.inspectAutomations(input.automationsDatabasePath),
480
+ ]);
481
+ return { recovery, automations, expectedBootstrap, expectedManaged };
482
+ }
483
+ function createLifecycleAttestation(input, observed) {
484
+ const identity = recoveryJobIdentity(input.effectiveConfig);
485
+ const externalProviderExemptions = assertLifecycleProviderContract(input);
486
+ const recoveryAttestation = observed.expectedBootstrap.attestations.find(value => value.automationId === 'recovery:supervised-growth');
487
+ if (recoveryAttestation === undefined || recoveryAttestation.activationState !== input.stage) {
488
+ throw new Error('supervised-growth lifecycle: supervised Recovery attestation is missing');
489
+ }
490
+ const recoveryDefinition = observed.expectedManaged.get('recovery:supervised-growth');
491
+ if (recoveryDefinition === undefined)
492
+ throw new Error('supervised-growth lifecycle: Recovery definition is missing');
493
+ // The content-free contract carries the runbook identity through the
494
+ // definition digest, while explicit catalog/nonce digests make upgrade
495
+ // policy checks possible without persisting either secret-free raw value.
496
+ const unsigned = deepFreeze({
497
+ protocol: SUPERVISED_GROWTH_LIFECYCLE_ATTESTATION_PROTOCOL,
498
+ schemaVersion: 1,
499
+ profile: input.profile.normalize('NFC').trim(),
500
+ stage: input.stage,
501
+ externalProviderExemptions,
502
+ effectiveConfigDigest: createHash('sha256').update(input.effectiveConfig).digest('hex'),
503
+ recovery: deepFreeze({
504
+ schemaVersion: observed.recovery.schemaVersion,
505
+ database: observed.recovery.database,
506
+ snapshotDigest: observed.recovery.snapshotDigest,
507
+ bootstrapGeneration: observed.recovery.bootstrap.generation,
508
+ bootstrapAttestationSetDigest: observed.recovery.bootstrap.attestationSetDigest,
509
+ bootstrapAttestations: observed.recovery.bootstrap.attestations,
510
+ activationNonceDigest: createHash('sha256').update(identity.activationNonce).digest('hex'),
511
+ catalogDigest: identity.catalogDigest,
512
+ }),
513
+ automations: deepFreeze({
514
+ schemaVersion: observed.automations.schemaVersion,
515
+ database: observed.automations.database,
516
+ sidecars: observed.automations.sidecars,
517
+ inventoryDigest: observed.automations.inventoryDigest,
518
+ snapshotDigest: digestJson(observed.automations),
519
+ inFlightCount: 0,
520
+ records: sortedAutomationRecords(observed.automations.records),
521
+ }),
522
+ });
523
+ return deepFreeze({ ...unsigned, attestationDigest: digestJson(unsigned) });
524
+ }
525
+ export function supervisedGrowthLifecycleAttestationDigest(value) {
526
+ return digestJson(value);
527
+ }
528
+ /**
529
+ * Capture one stable, content-free supervised lifecycle baseline. This function
530
+ * does not lock, stop, restart, patch, migrate, or otherwise mutate a service.
531
+ */
532
+ export async function captureSupervisedGrowthLifecycleAttestation(input, supplied = {}) {
533
+ if (!keyPattern.test(input.profile))
534
+ throw new Error('supervised-growth lifecycle: invalid profile');
535
+ assertLifecycleProviderContract(input);
536
+ const dependencies = await lifecycleDependencies(supplied);
537
+ const first = await observeLifecycle(input, dependencies);
538
+ assertRecoverySnapshot({
539
+ stage: input.stage, effectiveConfig: input.effectiveConfig,
540
+ snapshot: first.recovery, expected: first.expectedBootstrap,
541
+ });
542
+ assertManagedAutomationInventory({ stage: input.stage, snapshot: first.automations, expected: first.expectedManaged });
543
+ const second = await observeLifecycle(input, dependencies);
544
+ if (!sameRecoveryObservation(first.recovery, second.recovery)
545
+ || !sameAutomationObservation(first.automations, second.automations)
546
+ || !isDeepStrictEqual(first.expectedBootstrap, second.expectedBootstrap)
547
+ || !isDeepStrictEqual([...first.expectedManaged], [...second.expectedManaged])) {
548
+ throw new Error('supervised-growth lifecycle: operator state changed while capturing the baseline');
549
+ }
550
+ assertRecoverySnapshot({
551
+ stage: input.stage, effectiveConfig: input.effectiveConfig,
552
+ snapshot: second.recovery, expected: second.expectedBootstrap,
553
+ });
554
+ assertManagedAutomationInventory({ stage: input.stage, snapshot: second.automations, expected: second.expectedManaged });
555
+ return createLifecycleAttestation(input, second);
556
+ }
557
+ /**
558
+ * Verify a stable successor against a prior accepted lifecycle proof. The new
559
+ * Recovery generation must be strictly newer; a changed runbook/catalog must
560
+ * also carry a fresh activation nonce so old preview authority cannot be reused.
561
+ */
562
+ export async function verifySupervisedGrowthLifecycleSuccessor(input, supplied = {}) {
563
+ const { attestationDigest, ...unsignedBaseline } = input.baseline;
564
+ if (input.baseline.protocol !== SUPERVISED_GROWTH_LIFECYCLE_ATTESTATION_PROTOCOL
565
+ || input.baseline.schemaVersion !== 1
566
+ || attestationDigest !== supervisedGrowthLifecycleAttestationDigest(unsignedBaseline)
567
+ || input.target.profile !== input.baseline.profile) {
568
+ throw new Error('supervised-growth lifecycle: baseline contract is invalid');
569
+ }
570
+ const successor = await captureSupervisedGrowthLifecycleAttestation(input.target, supplied);
571
+ if (successor.recovery.bootstrapGeneration <= input.baseline.recovery.bootstrapGeneration) {
572
+ throw new Error('supervised-growth lifecycle: Recovery successor generation did not strictly advance');
573
+ }
574
+ // Preview and active intentionally share one nonce even though their exact
575
+ // Automation definitions differ (preview is paused and uses a one-shot
576
+ // schedule; active is scheduled). The catalog digest is the stable identity
577
+ // of the compiled Recovery runbook, so only a catalog change requires a new
578
+ // nonce across lifecycle generations.
579
+ const runbookChanged = successor.recovery.catalogDigest !== input.baseline.recovery.catalogDigest;
580
+ if (runbookChanged
581
+ && successor.recovery.activationNonceDigest === input.baseline.recovery.activationNonceDigest) {
582
+ throw new Error('supervised-growth lifecycle: changed Recovery runbook reused the old activation nonce');
583
+ }
584
+ return successor;
585
+ }
243
586
  async function inspectRecoveryBootstrap(databasePath) {
244
587
  const { RecoveryStore } = await import('@dsh-enhanced/assistant-recovery');
245
588
  const store = new RecoveryStore({ path: databasePath });
@@ -561,95 +904,97 @@ export async function runSupervisedGrowthSetup(argv = process.argv.slice(2)) {
561
904
  const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh');
562
905
  if (!isAbsolute(dshHome))
563
906
  throw new Error('supervised-growth setup: DSH_HOME must be absolute');
564
- const patchPath = join(dshHome, 'profiles', args.profile, 'cordis.patch.yml');
565
- const originalPatch = await readFile(patchPath, 'utf8');
566
- const effectiveBefore = dumpProfile(args.profile);
567
- const query = supervisedGrowthBindingQuery(effectiveBefore, dshHome);
568
- const databases = supervisedGrowthDatabasePaths(effectiveBefore, dshHome);
569
- const binding = await awaitOwnerBinding({
570
- databasePath: databases.deliveryDatabasePath, query, timeoutMs: args.timeoutMs,
571
- });
572
- const { listActiveAutomationsLocally } = await import('@dsh-enhanced/assistant-automations');
573
- assertSupervisedGrowthAutomationGuard(listActiveAutomationsLocally(databases.automationsDatabasePath), args.ackExistingAutomations);
574
- const { RECOVERY_CATALOG_DIGEST } = await import('@dsh-enhanced/assistant-recovery');
575
- const activationNonce = randomUUID();
576
- const previewPatch = configureSupervisedGrowthProfilePatch({
577
- profilePatch: originalPatch,
578
- effectiveConfig: effectiveBefore,
579
- dshHome,
580
- binding,
581
- activationState: 'preview',
582
- activationNonce,
583
- recoveryCatalogDigest: RECOVERY_CATALOG_DIGEST,
584
- });
585
- await assertSelectedOwnerBindingCurrent({
586
- databasePath: databases.deliveryDatabasePath,
587
- query,
588
- selected: binding,
589
- });
590
- let service;
591
- let acceptedBootstrapGeneration;
592
- await activateSupervisedGrowthRecoveryStages({
593
- patchPath,
594
- originalPatch,
595
- previewPatch,
596
- validate: stage => {
597
- const effectiveConfig = dumpProfile(args.profile);
598
- assertEffectiveSupervisedGrowthConfig({
599
- effectiveConfig,
600
- dshHome,
601
- binding,
602
- activationState: stage,
603
- activationNonce,
604
- recoveryCatalogDigest: RECOVERY_CATALOG_DIGEST,
605
- });
606
- return effectiveConfig;
607
- },
608
- buildActivePatch: previewEffective => configureSupervisedGrowthProfilePatch({
609
- profilePatch: previewPatch,
610
- effectiveConfig: previewEffective,
907
+ return withDshHomeLifecycleLock(dshHome, async () => {
908
+ const patchPath = join(dshHome, 'profiles', args.profile, 'cordis.patch.yml');
909
+ const originalPatch = await readFile(patchPath, 'utf8');
910
+ const effectiveBefore = dumpProfile(args.profile);
911
+ const query = supervisedGrowthBindingQuery(effectiveBefore, dshHome);
912
+ const databases = supervisedGrowthDatabasePaths(effectiveBefore, dshHome);
913
+ const binding = await awaitOwnerBinding({
914
+ databasePath: databases.deliveryDatabasePath, query, timeoutMs: args.timeoutMs,
915
+ });
916
+ const { listActiveAutomationsLocally } = await import('@dsh-enhanced/assistant-automations');
917
+ assertSupervisedGrowthAutomationGuard(listActiveAutomationsLocally(databases.automationsDatabasePath), args.ackExistingAutomations);
918
+ const { RECOVERY_CATALOG_DIGEST } = await import('@dsh-enhanced/assistant-recovery');
919
+ const activationNonce = randomUUID();
920
+ const previewPatch = configureSupervisedGrowthProfilePatch({
921
+ profilePatch: originalPatch,
922
+ effectiveConfig: effectiveBefore,
611
923
  dshHome,
612
924
  binding,
613
- activationState: 'active',
925
+ activationState: 'preview',
614
926
  activationNonce,
615
927
  recoveryCatalogDigest: RECOVERY_CATALOG_DIGEST,
616
- }),
617
- afterStage: async (stage, effectiveConfig) => {
618
- const effectiveDatabases = supervisedGrowthDatabasePaths(effectiveConfig, dshHome);
619
- if (effectiveDatabases.deliveryDatabasePath !== databases.deliveryDatabasePath
620
- || effectiveDatabases.automationsDatabasePath !== databases.automationsDatabasePath
621
- || effectiveDatabases.recoveryDatabasePath !== databases.recoveryDatabasePath) {
622
- throw new Error('supervised-growth setup: a higher-priority profile layer changed a guarded local database path');
623
- }
624
- // The binding is re-read after the atomic write and again immediately
625
- // before restart. A rotation/revocation/version change must never become
626
- // a background delivery grant merely because its old snapshot was valid.
627
- await assertSelectedOwnerBindingCurrent({
628
- databasePath: databases.deliveryDatabasePath,
629
- query,
630
- selected: binding,
631
- });
632
- const previousBootstrapGeneration = acceptedBootstrapGeneration
633
- ?? await recoveryBootstrapGenerationBeforeRestart(databases.recoveryDatabasePath);
634
- service = await restartAndVerifySupervisedGrowthResident({ dshHome, profile: args.profile });
635
- acceptedBootstrapGeneration = await verifySupervisedGrowthRecoveryStage({
636
- stage,
637
- effectiveConfig,
638
- recoveryDatabasePath: databases.recoveryDatabasePath,
639
- automationsDatabasePath: databases.automationsDatabasePath,
640
- previousBootstrapGeneration,
641
- timeoutMs: args.timeoutMs,
642
- });
643
- },
644
- restore: async () => {
645
- dumpProfile(args.profile);
646
- await restartAndVerifySupervisedGrowthResident({ dshHome, profile: args.profile });
647
- },
928
+ });
929
+ await assertSelectedOwnerBindingCurrent({
930
+ databasePath: databases.deliveryDatabasePath,
931
+ query,
932
+ selected: binding,
933
+ });
934
+ let service;
935
+ let acceptedBootstrapGeneration;
936
+ await activateSupervisedGrowthRecoveryStages({
937
+ patchPath,
938
+ originalPatch,
939
+ previewPatch,
940
+ validate: stage => {
941
+ const effectiveConfig = dumpProfile(args.profile);
942
+ assertEffectiveSupervisedGrowthConfig({
943
+ effectiveConfig,
944
+ dshHome,
945
+ binding,
946
+ activationState: stage,
947
+ activationNonce,
948
+ recoveryCatalogDigest: RECOVERY_CATALOG_DIGEST,
949
+ });
950
+ return effectiveConfig;
951
+ },
952
+ buildActivePatch: previewEffective => configureSupervisedGrowthProfilePatch({
953
+ profilePatch: previewPatch,
954
+ effectiveConfig: previewEffective,
955
+ dshHome,
956
+ binding,
957
+ activationState: 'active',
958
+ activationNonce,
959
+ recoveryCatalogDigest: RECOVERY_CATALOG_DIGEST,
960
+ }),
961
+ afterStage: async (stage, effectiveConfig) => {
962
+ const effectiveDatabases = supervisedGrowthDatabasePaths(effectiveConfig, dshHome);
963
+ if (effectiveDatabases.deliveryDatabasePath !== databases.deliveryDatabasePath
964
+ || effectiveDatabases.automationsDatabasePath !== databases.automationsDatabasePath
965
+ || effectiveDatabases.recoveryDatabasePath !== databases.recoveryDatabasePath) {
966
+ throw new Error('supervised-growth setup: a higher-priority profile layer changed a guarded local database path');
967
+ }
968
+ // The binding is re-read after the atomic write and again immediately
969
+ // before restart. A rotation/revocation/version change must never become
970
+ // a background delivery grant merely because its old snapshot was valid.
971
+ await assertSelectedOwnerBindingCurrent({
972
+ databasePath: databases.deliveryDatabasePath,
973
+ query,
974
+ selected: binding,
975
+ });
976
+ const previousBootstrapGeneration = acceptedBootstrapGeneration
977
+ ?? await recoveryBootstrapGenerationBeforeRestart(databases.recoveryDatabasePath);
978
+ service = await restartAndVerifySupervisedGrowthResident({ dshHome, profile: args.profile });
979
+ acceptedBootstrapGeneration = await verifySupervisedGrowthRecoveryStage({
980
+ stage,
981
+ effectiveConfig,
982
+ recoveryDatabasePath: databases.recoveryDatabasePath,
983
+ automationsDatabasePath: databases.automationsDatabasePath,
984
+ previousBootstrapGeneration,
985
+ timeoutMs: args.timeoutMs,
986
+ });
987
+ },
988
+ restore: async () => {
989
+ dumpProfile(args.profile);
990
+ await restartAndVerifySupervisedGrowthResident({ dshHome, profile: args.profile });
991
+ },
992
+ });
993
+ if (service === undefined)
994
+ throw new Error('supervised-growth setup: resident restart did not return a service');
995
+ process.stdout.write(`supervised-growth/v2 Recovery 已启用:owner binding ${binding.id},Recovery 每日上限 7;独立 adoption analyst 每日上限 1。\n`
996
+ + `DSH Host 已由 ${service.kind} 重启并通过健康检查。状态:${service.statusCommand}\n日志:${service.logCommand}\n`);
648
997
  });
649
- if (service === undefined)
650
- throw new Error('supervised-growth setup: resident restart did not return a service');
651
- process.stdout.write(`supervised-growth/v2 Recovery 已启用:owner binding ${binding.id},Recovery 每日上限 7;独立 adoption analyst 每日上限 1。\n`
652
- + `DSH Host 已由 ${service.kind} 重启并通过健康检查。状态:${service.statusCommand}\n日志:${service.logCommand}\n`);
653
998
  }
654
999
  if (process.argv[1] !== undefined && isMainEntry(import.meta.url, process.argv[1])) {
655
1000
  void runSupervisedGrowthSetup().catch(error => {