@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.
- package/README.md +14 -3
- package/docs/operations.md +8 -0
- package/docs/setup.md +3 -0
- package/lib/adapter.d.ts +7 -0
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +18 -4
- package/lib/adapter.js.map +1 -1
- package/lib/config.d.ts +3 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +8 -0
- package/lib/config.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/sdk.d.ts +17 -2
- package/lib/sdk.d.ts.map +1 -1
- package/lib/sdk.js +170 -76
- package/lib/sdk.js.map +1 -1
- package/lib/service.d.ts +17 -0
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +68 -5
- package/lib/service.js.map +1 -1
- package/lib/setup-profile.d.ts.map +1 -1
- package/lib/setup-profile.js +1 -0
- package/lib/setup-profile.js.map +1 -1
- package/lib/setup.d.ts +4 -0
- package/lib/setup.d.ts.map +1 -1
- package/lib/setup.js +313 -242
- package/lib/setup.js.map +1 -1
- package/lib/supervised-growth-profile.d.ts +27 -0
- package/lib/supervised-growth-profile.d.ts.map +1 -1
- package/lib/supervised-growth-profile.js +112 -0
- package/lib/supervised-growth-profile.js.map +1 -1
- package/lib/supervised-growth-setup.d.ts +82 -1
- package/lib/supervised-growth-setup.d.ts.map +1 -1
- package/lib/supervised-growth-setup.js +430 -85
- package/lib/supervised-growth-setup.js.map +1 -1
- package/lib/types.d.ts +12 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- 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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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: '
|
|
925
|
+
activationState: 'preview',
|
|
614
926
|
activationNonce,
|
|
615
927
|
recoveryCatalogDigest: RECOVERY_CATALOG_DIGEST,
|
|
616
|
-
})
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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 => {
|