@dsh-enhanced/assistant-recovery 0.1.12
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/LICENSE +21 -0
- package/README.md +178 -0
- package/cordis.patch.yml +14 -0
- package/lib/attestation.d.ts +18 -0
- package/lib/attestation.d.ts.map +1 -0
- package/lib/attestation.js +80 -0
- package/lib/attestation.js.map +1 -0
- package/lib/automation-executor.d.ts +21 -0
- package/lib/automation-executor.d.ts.map +1 -0
- package/lib/automation-executor.js +141 -0
- package/lib/automation-executor.js.map +1 -0
- package/lib/catalog.d.ts +16 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/catalog.js +49 -0
- package/lib/catalog.js.map +1 -0
- package/lib/config.d.ts +34 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +101 -0
- package/lib/config.js.map +1 -0
- package/lib/executor.d.ts +50 -0
- package/lib/executor.d.ts.map +1 -0
- package/lib/executor.js +413 -0
- package/lib/executor.js.map +1 -0
- package/lib/index.d.ts +21 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +26 -0
- package/lib/index.js.map +1 -0
- package/lib/port.d.ts +125 -0
- package/lib/port.d.ts.map +1 -0
- package/lib/port.js +706 -0
- package/lib/port.js.map +1 -0
- package/lib/service.d.ts +46 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +549 -0
- package/lib/service.js.map +1 -0
- package/lib/sqlite.d.ts +9 -0
- package/lib/sqlite.d.ts.map +1 -0
- package/lib/sqlite.js +247 -0
- package/lib/sqlite.js.map +1 -0
- package/lib/store.d.ts +79 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/store.js +806 -0
- package/lib/store.js.map +1 -0
- package/lib/types.d.ts +157 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +2 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +2 -0
- package/lib/version.js.map +1 -0
- package/package.json +96 -0
package/lib/port.js
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalEvaluationHostScope, } from '@dsh-enhanced/assistant-evaluation';
|
|
3
|
+
import { canonicalEvolutionHostScope, } from '@dsh-enhanced/assistant-evolution';
|
|
4
|
+
import { canonicalPreferenceHostScope, } from '@dsh-enhanced/preference-learning';
|
|
5
|
+
import { RecoveryPortError } from './executor.js';
|
|
6
|
+
import { RECOVERY_RUNBOOK_VERSION, } from './types.js';
|
|
7
|
+
export const RECOVERY_SYSTEM_OWNER = 'dsh-enhanced-assistant-recovery';
|
|
8
|
+
const STABLE_CODE = /^[a-z\d][a-z\d.-]{0,63}$/u;
|
|
9
|
+
function canonicalJson(value) {
|
|
10
|
+
if (value === null || typeof value !== 'object')
|
|
11
|
+
return JSON.stringify(value) ?? 'null';
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
14
|
+
const record = value;
|
|
15
|
+
return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
|
|
16
|
+
}
|
|
17
|
+
function digest(value) {
|
|
18
|
+
return createHash('sha256').update(canonicalJson(value)).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
function operationId(context, stepId, phase) {
|
|
21
|
+
return `recovery:${phase}:${RECOVERY_RUNBOOK_VERSION}:${context.occurrenceId}:${stepId}`;
|
|
22
|
+
}
|
|
23
|
+
function throwIfAborted(signal) {
|
|
24
|
+
if (signal.aborted)
|
|
25
|
+
throw new RecoveryPortError('execution-cancelled', 'none');
|
|
26
|
+
}
|
|
27
|
+
function serviceCode(error, fallback) {
|
|
28
|
+
const candidate = typeof error === 'object' && error !== null && 'code' in error
|
|
29
|
+
? error.code
|
|
30
|
+
: undefined;
|
|
31
|
+
if (typeof candidate !== 'string')
|
|
32
|
+
return fallback;
|
|
33
|
+
const normalized = candidate.normalize('NFC').trim().toLowerCase();
|
|
34
|
+
return STABLE_CODE.test(normalized) ? normalized : fallback;
|
|
35
|
+
}
|
|
36
|
+
function portFailure(error, fallback, possibleEffect) {
|
|
37
|
+
if (error instanceof RecoveryPortError)
|
|
38
|
+
throw error;
|
|
39
|
+
const code = serviceCode(error, fallback);
|
|
40
|
+
const provenPreEffect = [
|
|
41
|
+
'conflict', 'disabled', 'disposed', 'forbidden', 'idempotency-conflict',
|
|
42
|
+
'invalid-input', 'invalid-scope', 'missing-binding', 'missing-principal', 'not-found',
|
|
43
|
+
'policy-denied', 'unauthorized-principal', 'unattested-signal', 'version-conflict',
|
|
44
|
+
].includes(code);
|
|
45
|
+
throw new RecoveryPortError(code, possibleEffect && !provenPreEffect ? 'possible' : 'none');
|
|
46
|
+
}
|
|
47
|
+
function exactJob(context, jobs, activationPlanDigests) {
|
|
48
|
+
const job = jobs.get(context.automationId);
|
|
49
|
+
if (job === undefined)
|
|
50
|
+
throw new RecoveryPortError('automation-not-configured', 'none');
|
|
51
|
+
const planDigest = activationPlanDigests.get(context.automationId);
|
|
52
|
+
if (job.workspace !== context.targetScope.workspace
|
|
53
|
+
|| job.preset !== context.targetScope.preset
|
|
54
|
+
|| job.principal !== context.principal
|
|
55
|
+
|| job.ownerRouteId !== context.ownerRouteId
|
|
56
|
+
|| job.activationNonce !== context.activationNonce
|
|
57
|
+
|| planDigest === undefined
|
|
58
|
+
|| planDigest !== context.activationPlanDigest
|
|
59
|
+
|| job.catalogDigest !== context.catalogDigest) {
|
|
60
|
+
throw new RecoveryPortError('authority-scope-mismatch', 'none');
|
|
61
|
+
}
|
|
62
|
+
if (context.executionMode === 'production' && job.activationState !== 'active') {
|
|
63
|
+
throw new RecoveryPortError('production-not-active', 'none');
|
|
64
|
+
}
|
|
65
|
+
if (context.executionMode === 'preview' && job.activationState !== 'preview') {
|
|
66
|
+
throw new RecoveryPortError('preview-not-enabled', 'none');
|
|
67
|
+
}
|
|
68
|
+
return job;
|
|
69
|
+
}
|
|
70
|
+
function projectionState(projection) {
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
owner: projection.owner,
|
|
73
|
+
automationId: projection.automationId,
|
|
74
|
+
automationStatus: projection.automationStatus,
|
|
75
|
+
definitionHash: projection.definitionHash,
|
|
76
|
+
definitionVersion: projection.definitionVersion,
|
|
77
|
+
currentCircuit: projection.currentCircuit === undefined ? null : {
|
|
78
|
+
definitionHash: projection.currentCircuit.definitionHash,
|
|
79
|
+
state: projection.currentCircuit.state,
|
|
80
|
+
failureClass: projection.currentCircuit.failureClass,
|
|
81
|
+
failurePhase: projection.currentCircuit.failurePhase,
|
|
82
|
+
failureCode: projection.currentCircuit.failureCode,
|
|
83
|
+
version: projection.currentCircuit.version,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function healthState(report) {
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
ready: report.ready,
|
|
90
|
+
severity: report.severity,
|
|
91
|
+
providers: report.providers.map(provider => ({
|
|
92
|
+
id: provider.id,
|
|
93
|
+
status: provider.status,
|
|
94
|
+
metrics: provider.metrics,
|
|
95
|
+
})),
|
|
96
|
+
assessments: report.assessments.map(value => ({
|
|
97
|
+
providerId: value.providerId,
|
|
98
|
+
severity: value.severity,
|
|
99
|
+
code: value.code,
|
|
100
|
+
})),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function assertRequiredProviders(report, phase) {
|
|
104
|
+
const byId = new Map(report.providers.map(provider => [provider.id, provider.status]));
|
|
105
|
+
const required = [
|
|
106
|
+
'assistantAutomations', 'assistantEvaluation', 'preferenceLearning',
|
|
107
|
+
'assistantEvolution', 'assistantRecovery',
|
|
108
|
+
];
|
|
109
|
+
const admissionRepairable = new Set([
|
|
110
|
+
'assistantAutomations:open-circuit-backlog',
|
|
111
|
+
'assistantAutomations:open-incident-backlog',
|
|
112
|
+
'assistantEvaluation:projection-retry-backlog',
|
|
113
|
+
'assistantRecovery:bootstrap-in-progress',
|
|
114
|
+
'assistantRecovery:incomplete-recovery',
|
|
115
|
+
]);
|
|
116
|
+
const unexpectedAssessment = report.assessments.find(assessment => {
|
|
117
|
+
if (assessment.severity !== 'degraded')
|
|
118
|
+
return true;
|
|
119
|
+
const key = `${assessment.providerId}:${assessment.code}`;
|
|
120
|
+
return phase === 'admission'
|
|
121
|
+
? !admissionRepairable.has(key)
|
|
122
|
+
: key !== 'assistantRecovery:bootstrap-in-progress';
|
|
123
|
+
});
|
|
124
|
+
const internallyInconsistentDegraded = report.severity === 'degraded'
|
|
125
|
+
&& report.assessments.length === 0;
|
|
126
|
+
if (!report.ready
|
|
127
|
+
|| report.severity === 'unhealthy'
|
|
128
|
+
|| unexpectedAssessment !== undefined
|
|
129
|
+
|| internallyInconsistentDegraded
|
|
130
|
+
|| required.some(id => byId.get(id) !== 'ready')) {
|
|
131
|
+
throw new RecoveryPortError('health-not-ready', 'none');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function candidateState(candidates) {
|
|
135
|
+
return candidates.map(candidate => Object.freeze({
|
|
136
|
+
kind: candidate.kind,
|
|
137
|
+
situation: candidate.situation,
|
|
138
|
+
ruleId: candidate.ruleId ?? null,
|
|
139
|
+
failures: candidate.stats.failures,
|
|
140
|
+
total: candidate.stats.total,
|
|
141
|
+
evidenceDigest: candidate.evidenceDigest,
|
|
142
|
+
evidenceTotal: candidate.evidenceTotal,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
function ruleState(rules) {
|
|
146
|
+
return rules.map(rule => Object.freeze({
|
|
147
|
+
id: rule.id,
|
|
148
|
+
situation: rule.situation,
|
|
149
|
+
generation: rule.generation,
|
|
150
|
+
status: rule.status,
|
|
151
|
+
version: rule.version,
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
function validateOwnerRoute(context, delivery, expectedAuthorityHash) {
|
|
155
|
+
const receipt = delivery.validateOwnerRoute({
|
|
156
|
+
authorityId: context.ownerRouteId,
|
|
157
|
+
principalId: context.principal,
|
|
158
|
+
workspace: context.targetScope.workspace,
|
|
159
|
+
agentPreset: context.targetScope.preset,
|
|
160
|
+
});
|
|
161
|
+
const principalRecordIdValid = typeof receipt.principalRecordId === 'string'
|
|
162
|
+
&& receipt.principalRecordId === receipt.principalRecordId.normalize('NFC').trim()
|
|
163
|
+
&& receipt.principalRecordId !== ''
|
|
164
|
+
&& Buffer.byteLength(receipt.principalRecordId, 'utf8') <= 500
|
|
165
|
+
&& ![...receipt.principalRecordId].some((character) => {
|
|
166
|
+
const point = character.codePointAt(0);
|
|
167
|
+
return point <= 0x1f || point === 0x7f;
|
|
168
|
+
});
|
|
169
|
+
if (receipt.receiptVersion !== 2
|
|
170
|
+
|| receipt.authorityId !== context.ownerRouteId
|
|
171
|
+
|| receipt.principalId !== context.principal
|
|
172
|
+
|| !principalRecordIdValid
|
|
173
|
+
|| !Number.isSafeInteger(receipt.principalVersion) || receipt.principalVersion < 1
|
|
174
|
+
|| receipt.workspace !== context.targetScope.workspace
|
|
175
|
+
|| receipt.agentPreset !== context.targetScope.preset
|
|
176
|
+
|| !Number.isSafeInteger(receipt.bindingVersion) || receipt.bindingVersion < 1
|
|
177
|
+
|| !Number.isSafeInteger(receipt.generation) || receipt.generation < 1
|
|
178
|
+
|| typeof receipt.authorityHash !== 'string' || !/^[a-f\d]{64}$/u.test(receipt.authorityHash)) {
|
|
179
|
+
throw new RecoveryPortError('owner-route-receipt-invalid', 'none');
|
|
180
|
+
}
|
|
181
|
+
if (!/^[a-f\d]{64}$/u.test(expectedAuthorityHash)
|
|
182
|
+
|| receipt.authorityHash !== expectedAuthorityHash) {
|
|
183
|
+
throw new RecoveryPortError('owner-route-authority-mismatch', 'none');
|
|
184
|
+
}
|
|
185
|
+
return receipt;
|
|
186
|
+
}
|
|
187
|
+
function routePrincipalLineage(route) {
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
principalRecordId: route.principalRecordId,
|
|
190
|
+
principalVersion: route.principalVersion,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function exactMaintenanceAction(action) {
|
|
194
|
+
const raw = action;
|
|
195
|
+
const lineage = raw.principalLineage;
|
|
196
|
+
if (!Number.isSafeInteger(raw.ownerGeneration) || raw.ownerGeneration < 1
|
|
197
|
+
|| typeof lineage !== 'object' || lineage === null
|
|
198
|
+
|| typeof lineage.principalRecordId !== 'string'
|
|
199
|
+
|| !Number.isSafeInteger(lineage.principalVersion) || lineage.principalVersion < 1) {
|
|
200
|
+
throw new RecoveryPortError('preference-maintenance-action-unfenced', 'none');
|
|
201
|
+
}
|
|
202
|
+
return action;
|
|
203
|
+
}
|
|
204
|
+
function exactPreferenceOwnerFence(value, principalLineage) {
|
|
205
|
+
const raw = value;
|
|
206
|
+
if (raw === null || typeof raw !== 'object'
|
|
207
|
+
|| !Number.isSafeInteger(raw.ownerGeneration) || raw.ownerGeneration < 1
|
|
208
|
+
|| raw.principalLineage === null || typeof raw.principalLineage !== 'object'
|
|
209
|
+
|| raw.principalLineage.principalRecordId !== principalLineage.principalRecordId
|
|
210
|
+
|| raw.principalLineage.principalVersion !== principalLineage.principalVersion) {
|
|
211
|
+
throw new RecoveryPortError('preference-maintenance-fence-receipt-invalid', 'none');
|
|
212
|
+
}
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
function exactMaintenanceReceipt(value, action) {
|
|
216
|
+
const raw = value;
|
|
217
|
+
if (raw === null || typeof raw !== 'object'
|
|
218
|
+
|| !Number.isSafeInteger(raw.deletedSignals) || raw.deletedSignals < 0
|
|
219
|
+
|| raw.deletedSignals > action.limit
|
|
220
|
+
|| raw.ownerGeneration !== action.ownerGeneration
|
|
221
|
+
|| raw.principalLineageId !== action.principalLineage.principalRecordId
|
|
222
|
+
|| raw.principalLineageVersion !== action.principalLineage.principalVersion
|
|
223
|
+
|| typeof raw.replayed !== 'boolean') {
|
|
224
|
+
throw new RecoveryPortError('preference-maintenance-receipt-invalid', 'possible');
|
|
225
|
+
}
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Concrete, model-free adapter over the narrow Host seams of the learning
|
|
230
|
+
* services. Planning may inspect only the configured scope; mutations are
|
|
231
|
+
* exact CAS/idempotent operations selected before Recovery persists intent.
|
|
232
|
+
*/
|
|
233
|
+
export class HostRecoveryRunbookPort {
|
|
234
|
+
runtime;
|
|
235
|
+
jobs;
|
|
236
|
+
activationPlanDigests;
|
|
237
|
+
authorityHashes;
|
|
238
|
+
constructor(jobs, runtime, activationPlanDigests, authorityHashes) {
|
|
239
|
+
this.runtime = runtime;
|
|
240
|
+
this.jobs = new Map(jobs);
|
|
241
|
+
this.activationPlanDigests = new Map(activationPlanDigests);
|
|
242
|
+
this.authorityHashes = new Map(authorityHashes);
|
|
243
|
+
}
|
|
244
|
+
validateOwnerRoute(context) {
|
|
245
|
+
const expectedAuthorityHash = this.authorityHashes.get(context.automationId);
|
|
246
|
+
if (expectedAuthorityHash === undefined) {
|
|
247
|
+
throw new RecoveryPortError('owner-route-authority-missing', 'none');
|
|
248
|
+
}
|
|
249
|
+
return validateOwnerRoute(context, this.runtime.delivery, expectedAuthorityHash);
|
|
250
|
+
}
|
|
251
|
+
async plan(context, stepId, signal) {
|
|
252
|
+
throwIfAborted(signal);
|
|
253
|
+
exactJob(context, this.jobs, this.activationPlanDigests);
|
|
254
|
+
switch (stepId) {
|
|
255
|
+
case 'authority-admission': return this.planAuthority(context, signal);
|
|
256
|
+
case 'ledger-reconcile': return this.planLedger(context);
|
|
257
|
+
case 'retention-maintenance': return this.planRetention(context);
|
|
258
|
+
case 't1-effects': return this.planPreferenceActivation(context);
|
|
259
|
+
case 'regression-rollback': return this.planEvolutionRollback(context);
|
|
260
|
+
case 'incident-review': return this.planCircuitProbe(context);
|
|
261
|
+
case 'verification': return this.planVerification(context, signal);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async execute(context, stepId, action, idempotencyKey, signal) {
|
|
265
|
+
throwIfAborted(signal);
|
|
266
|
+
exactJob(context, this.jobs, this.activationPlanDigests);
|
|
267
|
+
switch (action.kind) {
|
|
268
|
+
case 'verify-authority': return this.verifyAuthority(context, idempotencyKey, signal);
|
|
269
|
+
case 'project-evaluation': return this.projectEvaluation(context, action, idempotencyKey, signal);
|
|
270
|
+
case 'maintain-preferences': return this.maintainPreferences(context, action, idempotencyKey, signal);
|
|
271
|
+
case 'activate-preference': return this.activatePreference(context, action, idempotencyKey, signal);
|
|
272
|
+
case 'rollback-evolution': return this.rollbackEvolution(context, action, idempotencyKey, signal);
|
|
273
|
+
case 'probe-automation-circuit': return this.probeCircuit(context, action, idempotencyKey, signal);
|
|
274
|
+
case 'verify-health': return this.verifyHealth(context, idempotencyKey, signal);
|
|
275
|
+
case 'noop': return Object.freeze({
|
|
276
|
+
status: 'noop', resultCode: action.reasonCode, afterDigest: digest({ stepId, action }),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
planAuthority(context, signal) {
|
|
281
|
+
try {
|
|
282
|
+
const projection = this.runtime.automations.inspectSystemOwned({
|
|
283
|
+
owner: RECOVERY_SYSTEM_OWNER,
|
|
284
|
+
automationId: context.automationId,
|
|
285
|
+
});
|
|
286
|
+
if (projection.definitionHash !== context.definitionHash || projection.automationStatus !== 'active') {
|
|
287
|
+
throw new RecoveryPortError('automation-definition-mismatch', 'none');
|
|
288
|
+
}
|
|
289
|
+
const report = this.runtime.health.hostGlobalSnapshot({
|
|
290
|
+
principal: context.principal,
|
|
291
|
+
operationId: operationId(context, 'authority-admission', 'plan'),
|
|
292
|
+
});
|
|
293
|
+
const route = this.validateOwnerRoute(context);
|
|
294
|
+
throwIfAborted(signal);
|
|
295
|
+
assertRequiredProviders(report, 'admission');
|
|
296
|
+
return Object.freeze({
|
|
297
|
+
action: { kind: 'verify-authority' },
|
|
298
|
+
beforeDigest: digest({
|
|
299
|
+
job: exactJob(context, this.jobs, this.activationPlanDigests),
|
|
300
|
+
projection: projectionState(projection),
|
|
301
|
+
health: healthState(report),
|
|
302
|
+
route,
|
|
303
|
+
}),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
portFailure(error, 'authority-admission-failed', false);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
planLedger(context) {
|
|
311
|
+
try {
|
|
312
|
+
const capable = typeof this.runtime.evaluation.peekPendingProjection === 'function'
|
|
313
|
+
&& typeof this.runtime.evaluation.reconcileProjection === 'function';
|
|
314
|
+
const target = capable
|
|
315
|
+
? this.runtime.evaluation.peekPendingProjection({
|
|
316
|
+
scope: canonicalEvaluationHostScope(context.targetScope),
|
|
317
|
+
})
|
|
318
|
+
: undefined;
|
|
319
|
+
const action = !capable
|
|
320
|
+
? { kind: 'noop', reasonCode: 'projection-seam-unavailable' }
|
|
321
|
+
: target === undefined
|
|
322
|
+
? { kind: 'noop', reasonCode: 'no-quality-projection' }
|
|
323
|
+
: { kind: 'project-evaluation', evaluationId: target.evaluationId };
|
|
324
|
+
return Object.freeze({
|
|
325
|
+
action,
|
|
326
|
+
beforeDigest: digest({
|
|
327
|
+
scope: context.targetScope,
|
|
328
|
+
evaluation: this.runtime.evaluation.health(),
|
|
329
|
+
target: target ?? null,
|
|
330
|
+
capable,
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
portFailure(error, 'projection-plan-failed', false);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
planRetention(context) {
|
|
339
|
+
try {
|
|
340
|
+
const route = this.validateOwnerRoute(context);
|
|
341
|
+
const principalLineage = routePrincipalLineage(route);
|
|
342
|
+
const ownerFence = exactPreferenceOwnerFence(this.runtime.preference.hostOwnerFence({
|
|
343
|
+
scope: canonicalPreferenceHostScope(context.targetScope),
|
|
344
|
+
principal: context.principal,
|
|
345
|
+
principalLineage,
|
|
346
|
+
operationId: operationId(context, 'retention-maintenance', 'plan'),
|
|
347
|
+
}), principalLineage);
|
|
348
|
+
return Object.freeze({
|
|
349
|
+
action: {
|
|
350
|
+
kind: 'maintain-preferences',
|
|
351
|
+
limit: 1,
|
|
352
|
+
ownerGeneration: ownerFence.ownerGeneration,
|
|
353
|
+
principalLineage,
|
|
354
|
+
},
|
|
355
|
+
beforeDigest: digest({
|
|
356
|
+
scope: context.targetScope,
|
|
357
|
+
health: this.runtime.preference.health(),
|
|
358
|
+
route,
|
|
359
|
+
ownerFence,
|
|
360
|
+
}),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
portFailure(error, 'preference-maintenance-plan-failed', false);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
planPreferenceActivation(context) {
|
|
368
|
+
try {
|
|
369
|
+
const route = this.validateOwnerRoute(context);
|
|
370
|
+
const principalLineage = routePrincipalLineage(route);
|
|
371
|
+
const candidate = this.runtime.preference.hostActivationCandidate({
|
|
372
|
+
scope: canonicalPreferenceHostScope(context.targetScope),
|
|
373
|
+
principal: context.principal,
|
|
374
|
+
principalLineage,
|
|
375
|
+
operationId: operationId(context, 't1-effects', 'plan'),
|
|
376
|
+
});
|
|
377
|
+
let action;
|
|
378
|
+
if (candidate === undefined) {
|
|
379
|
+
action = { kind: 'noop', reasonCode: 'no-preference-candidate' };
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
if (!Number.isSafeInteger(candidate.ownerGeneration) || candidate.ownerGeneration < 1
|
|
383
|
+
|| candidate.principalLineage.principalRecordId !== principalLineage.principalRecordId
|
|
384
|
+
|| candidate.principalLineage.principalVersion !== principalLineage.principalVersion) {
|
|
385
|
+
throw new RecoveryPortError('preference-activation-candidate-receipt-invalid', 'none');
|
|
386
|
+
}
|
|
387
|
+
action = {
|
|
388
|
+
kind: 'activate-preference',
|
|
389
|
+
hypothesisId: candidate.hypothesisId,
|
|
390
|
+
expectedVersion: candidate.expectedVersion,
|
|
391
|
+
ownerGeneration: candidate.ownerGeneration,
|
|
392
|
+
principalLineage,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return Object.freeze({
|
|
396
|
+
action,
|
|
397
|
+
beforeDigest: digest({ scope: context.targetScope, route, candidate: candidate ?? null }),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
portFailure(error, 'preference-plan-failed', false);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
planEvolutionRollback(context) {
|
|
405
|
+
try {
|
|
406
|
+
const host = {
|
|
407
|
+
scope: canonicalEvolutionHostScope(context.targetScope),
|
|
408
|
+
principal: context.principal,
|
|
409
|
+
operationId: operationId(context, 'regression-rollback', 'plan'),
|
|
410
|
+
};
|
|
411
|
+
const candidates = this.runtime.evolution.hostCandidates(host);
|
|
412
|
+
const rules = this.runtime.evolution.hostListRules({ ...host, status: 'active' });
|
|
413
|
+
const target = candidates
|
|
414
|
+
.filter(candidate => candidate.kind === 'retire' && candidate.ruleId !== undefined)
|
|
415
|
+
.sort((left, right) => left.ruleId.localeCompare(right.ruleId))[0];
|
|
416
|
+
const rule = target === undefined ? undefined : rules.find(value => value.id === target.ruleId);
|
|
417
|
+
const action = rule === undefined
|
|
418
|
+
? { kind: 'noop', reasonCode: 'no-regression-candidate' }
|
|
419
|
+
: { kind: 'rollback-evolution', ruleId: rule.id, expectedVersion: rule.version };
|
|
420
|
+
return Object.freeze({
|
|
421
|
+
action,
|
|
422
|
+
beforeDigest: digest({
|
|
423
|
+
scope: context.targetScope,
|
|
424
|
+
candidates: candidateState(candidates),
|
|
425
|
+
rules: ruleState(rules),
|
|
426
|
+
}),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
portFailure(error, 'evolution-plan-failed', false);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
planCircuitProbe(context) {
|
|
434
|
+
try {
|
|
435
|
+
if (typeof this.runtime.automations.probeCircuitAndScheduleCanary !== 'function') {
|
|
436
|
+
return Object.freeze({
|
|
437
|
+
action: { kind: 'noop', reasonCode: 'circuit-canary-seam-unavailable' },
|
|
438
|
+
beforeDigest: digest({ capable: false }),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
const candidates = [...this.jobs.keys()]
|
|
442
|
+
.filter(automationId => automationId !== context.automationId)
|
|
443
|
+
.sort()
|
|
444
|
+
.map(automationId => this.runtime.automations.inspectSystemOwned({
|
|
445
|
+
owner: RECOVERY_SYSTEM_OWNER,
|
|
446
|
+
automationId,
|
|
447
|
+
}))
|
|
448
|
+
.filter(projection => projection.automationStatus === 'active'
|
|
449
|
+
&& projection.currentCircuit?.state === 'open')
|
|
450
|
+
.map(projection => ({ projection, circuit: projection.currentCircuit }));
|
|
451
|
+
const target = candidates[0];
|
|
452
|
+
const action = target === undefined
|
|
453
|
+
? { kind: 'noop', reasonCode: 'no-circuit-candidate' }
|
|
454
|
+
: {
|
|
455
|
+
kind: 'probe-automation-circuit',
|
|
456
|
+
automationId: target.projection.automationId,
|
|
457
|
+
definitionHash: target.circuit.definitionHash,
|
|
458
|
+
expectedVersion: target.circuit.version,
|
|
459
|
+
};
|
|
460
|
+
return Object.freeze({
|
|
461
|
+
action,
|
|
462
|
+
beforeDigest: digest(candidates.map(value => projectionState(value.projection))),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
portFailure(error, 'circuit-plan-failed', false);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
planVerification(context, signal) {
|
|
470
|
+
try {
|
|
471
|
+
const report = this.runtime.health.hostGlobalSnapshot({
|
|
472
|
+
principal: context.principal,
|
|
473
|
+
operationId: operationId(context, 'verification', 'plan'),
|
|
474
|
+
});
|
|
475
|
+
const route = this.validateOwnerRoute(context);
|
|
476
|
+
throwIfAborted(signal);
|
|
477
|
+
return Object.freeze({
|
|
478
|
+
action: { kind: 'verify-health' },
|
|
479
|
+
beforeDigest: digest({ health: healthState(report), route }),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
portFailure(error, 'health-plan-failed', false);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
verifyAuthority(context, idempotencyKey, signal) {
|
|
487
|
+
try {
|
|
488
|
+
const projection = this.runtime.automations.inspectSystemOwned({
|
|
489
|
+
owner: RECOVERY_SYSTEM_OWNER,
|
|
490
|
+
automationId: context.automationId,
|
|
491
|
+
});
|
|
492
|
+
if (projection.definitionHash !== context.definitionHash || projection.automationStatus !== 'active') {
|
|
493
|
+
throw new RecoveryPortError('automation-definition-mismatch', 'none');
|
|
494
|
+
}
|
|
495
|
+
const report = this.runtime.health.hostGlobalSnapshot({
|
|
496
|
+
principal: context.principal,
|
|
497
|
+
operationId: idempotencyKey,
|
|
498
|
+
});
|
|
499
|
+
const route = this.validateOwnerRoute(context);
|
|
500
|
+
throwIfAborted(signal);
|
|
501
|
+
assertRequiredProviders(report, 'admission');
|
|
502
|
+
return Object.freeze({
|
|
503
|
+
status: 'succeeded',
|
|
504
|
+
resultCode: 'authority-verified',
|
|
505
|
+
afterDigest: digest({ projection: projectionState(projection), health: healthState(report), route }),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
portFailure(error, 'authority-verification-failed', false);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async projectEvaluation(context, action, idempotencyKey, _signal) {
|
|
513
|
+
if (typeof this.runtime.evaluation.reconcileProjection !== 'function') {
|
|
514
|
+
throw new RecoveryPortError('projection-seam-unavailable', 'none');
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
this.validateOwnerRoute(context);
|
|
518
|
+
const result = await this.runtime.evaluation.reconcileProjection({
|
|
519
|
+
scope: canonicalEvaluationHostScope(context.targetScope),
|
|
520
|
+
evaluationId: action.evaluationId,
|
|
521
|
+
operationId: idempotencyKey,
|
|
522
|
+
});
|
|
523
|
+
if (result.evaluationId !== action.evaluationId
|
|
524
|
+
|| !Number.isSafeInteger(result.attemptCount) || result.attemptCount < 0
|
|
525
|
+
|| (result.status !== 'recorded' && result.status !== 'deferred')) {
|
|
526
|
+
throw new RecoveryPortError('projection-receipt-invalid', 'possible');
|
|
527
|
+
}
|
|
528
|
+
if (result.status === 'deferred') {
|
|
529
|
+
throw new RecoveryPortError('projection-deferred', 'possible');
|
|
530
|
+
}
|
|
531
|
+
return Object.freeze({
|
|
532
|
+
status: 'succeeded',
|
|
533
|
+
resultCode: 'quality-projected',
|
|
534
|
+
afterDigest: digest(result),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
portFailure(error, 'projection-reconcile-failed', true);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
maintainPreferences(context, rawAction, idempotencyKey, _signal) {
|
|
542
|
+
try {
|
|
543
|
+
const action = exactMaintenanceAction(rawAction);
|
|
544
|
+
const route = this.validateOwnerRoute(context);
|
|
545
|
+
if (action.principalLineage.principalRecordId !== route.principalRecordId
|
|
546
|
+
|| action.principalLineage.principalVersion !== route.principalVersion) {
|
|
547
|
+
throw new RecoveryPortError('owner-route-lineage-mismatch', 'none');
|
|
548
|
+
}
|
|
549
|
+
const result = exactMaintenanceReceipt(this.runtime.preference.hostMaintainOne({
|
|
550
|
+
scope: canonicalPreferenceHostScope(context.targetScope),
|
|
551
|
+
principal: context.principal,
|
|
552
|
+
principalLineage: action.principalLineage,
|
|
553
|
+
ownerGeneration: action.ownerGeneration,
|
|
554
|
+
operationId: idempotencyKey,
|
|
555
|
+
}), action);
|
|
556
|
+
return Object.freeze({
|
|
557
|
+
status: result.deletedSignals === 0 ? 'noop' : 'succeeded',
|
|
558
|
+
resultCode: result.deletedSignals === 0 ? 'no-expired-preference' : 'preference-retained',
|
|
559
|
+
afterDigest: digest(result),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
portFailure(error, 'preference-maintenance-failed', true);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
activatePreference(context, action, idempotencyKey, _signal) {
|
|
567
|
+
try {
|
|
568
|
+
const scope = canonicalPreferenceHostScope(context.targetScope);
|
|
569
|
+
const route = this.validateOwnerRoute(context);
|
|
570
|
+
if (action.principalLineage.principalRecordId !== route.principalRecordId
|
|
571
|
+
|| action.principalLineage.principalVersion !== route.principalVersion) {
|
|
572
|
+
throw new RecoveryPortError('owner-route-lineage-mismatch', 'none');
|
|
573
|
+
}
|
|
574
|
+
// Do not re-peek here. On crash-after-commit the candidate is no longer
|
|
575
|
+
// pending, while hostActivateOne's operation receipt is exactly what can
|
|
576
|
+
// prove and replay the prior mutation.
|
|
577
|
+
const result = this.runtime.preference.hostActivateOne({
|
|
578
|
+
scope,
|
|
579
|
+
principal: context.principal,
|
|
580
|
+
principalLineage: action.principalLineage,
|
|
581
|
+
ownerGeneration: action.ownerGeneration,
|
|
582
|
+
operationId: idempotencyKey,
|
|
583
|
+
hypothesisId: action.hypothesisId,
|
|
584
|
+
expectedVersion: action.expectedVersion,
|
|
585
|
+
});
|
|
586
|
+
if (result.hypothesisId !== action.hypothesisId
|
|
587
|
+
|| result.expectedVersion !== action.expectedVersion
|
|
588
|
+
|| result.resultVersion !== action.expectedVersion + 1
|
|
589
|
+
|| result.ownerGeneration !== action.ownerGeneration
|
|
590
|
+
|| result.principalLineageId !== action.principalLineage.principalRecordId
|
|
591
|
+
|| result.principalLineageVersion !== action.principalLineage.principalVersion
|
|
592
|
+
|| typeof result.replayed !== 'boolean') {
|
|
593
|
+
throw new RecoveryPortError('preference-activation-receipt-invalid', 'possible');
|
|
594
|
+
}
|
|
595
|
+
return Object.freeze({
|
|
596
|
+
status: 'succeeded', resultCode: 'preference-activated', afterDigest: digest(result),
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
portFailure(error, 'preference-activation-failed', true);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
rollbackEvolution(context, action, idempotencyKey, _signal) {
|
|
604
|
+
try {
|
|
605
|
+
const scope = canonicalEvolutionHostScope(context.targetScope);
|
|
606
|
+
this.validateOwnerRoute(context);
|
|
607
|
+
// The Evolution store recomputes evidence/CAS on first execution and
|
|
608
|
+
// returns its immutable rollback receipt on replay. A fresh candidate
|
|
609
|
+
// lookup here would incorrectly reject a successful prior retirement.
|
|
610
|
+
const result = this.runtime.evolution.hostRollbackOne({
|
|
611
|
+
scope,
|
|
612
|
+
principal: context.principal,
|
|
613
|
+
operationId: idempotencyKey,
|
|
614
|
+
ruleId: action.ruleId,
|
|
615
|
+
expectedVersion: action.expectedVersion,
|
|
616
|
+
});
|
|
617
|
+
if (result.rollback.ruleId !== action.ruleId
|
|
618
|
+
|| result.rollback.expectedVersion !== action.expectedVersion
|
|
619
|
+
|| result.rollback.resultVersion !== action.expectedVersion + 1
|
|
620
|
+
|| result.rule.id !== action.ruleId
|
|
621
|
+
|| result.rule.version !== result.rollback.resultVersion
|
|
622
|
+
|| result.rule.status !== 'retired'
|
|
623
|
+
|| typeof result.replayed !== 'boolean'
|
|
624
|
+
|| !/^[a-f\d]{64}$/u.test(result.rollback.evidence.digest)) {
|
|
625
|
+
throw new RecoveryPortError('evolution-rollback-receipt-invalid', 'possible');
|
|
626
|
+
}
|
|
627
|
+
return Object.freeze({
|
|
628
|
+
status: 'succeeded', resultCode: 'evolution-rolled-back', afterDigest: digest({
|
|
629
|
+
ruleId: result.rule.id,
|
|
630
|
+
version: result.rule.version,
|
|
631
|
+
replayed: result.replayed,
|
|
632
|
+
evidenceDigest: result.rollback.evidence.digest,
|
|
633
|
+
}),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
portFailure(error, 'evolution-rollback-failed', true);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
probeCircuit(context, action, idempotencyKey, _signal) {
|
|
641
|
+
if (typeof this.runtime.automations.probeCircuitAndScheduleCanary !== 'function') {
|
|
642
|
+
throw new RecoveryPortError('circuit-canary-seam-unavailable', 'none');
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
this.validateOwnerRoute(context);
|
|
646
|
+
// The sink atomically arms the exact circuit and durably schedules a
|
|
647
|
+
// production canary. Replaying this operation never re-inspects `open`.
|
|
648
|
+
const result = this.runtime.automations.probeCircuitAndScheduleCanary({
|
|
649
|
+
owner: RECOVERY_SYSTEM_OWNER,
|
|
650
|
+
operationId: idempotencyKey,
|
|
651
|
+
automationId: action.automationId,
|
|
652
|
+
definitionHash: action.definitionHash,
|
|
653
|
+
expectedCircuitVersion: action.expectedVersion,
|
|
654
|
+
});
|
|
655
|
+
if (result.operationId !== idempotencyKey
|
|
656
|
+
|| result.circuit.automationId !== action.automationId
|
|
657
|
+
|| result.circuit.definitionHash !== action.definitionHash
|
|
658
|
+
|| result.circuit.state !== 'half-open'
|
|
659
|
+
|| result.circuit.version !== action.expectedVersion + 1
|
|
660
|
+
|| result.executionMode !== 'production'
|
|
661
|
+
|| typeof result.occurrenceId !== 'string' || result.occurrenceId === ''
|
|
662
|
+
|| typeof result.taskId !== 'string' || result.taskId === ''
|
|
663
|
+
|| typeof result.replayed !== 'boolean') {
|
|
664
|
+
throw new RecoveryPortError('circuit-canary-receipt-invalid', 'possible');
|
|
665
|
+
}
|
|
666
|
+
return Object.freeze({
|
|
667
|
+
status: 'succeeded', resultCode: 'circuit-canary-scheduled', afterDigest: digest({
|
|
668
|
+
operationId: result.operationId,
|
|
669
|
+
automationId: result.circuit.automationId,
|
|
670
|
+
definitionHash: result.circuit.definitionHash,
|
|
671
|
+
state: result.circuit.state,
|
|
672
|
+
version: result.circuit.version,
|
|
673
|
+
canary: {
|
|
674
|
+
occurrenceId: result.occurrenceId,
|
|
675
|
+
taskId: result.taskId,
|
|
676
|
+
executionMode: result.executionMode,
|
|
677
|
+
},
|
|
678
|
+
replayed: result.replayed,
|
|
679
|
+
}),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
portFailure(error, 'circuit-probe-failed', true);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
verifyHealth(context, idempotencyKey, signal) {
|
|
687
|
+
try {
|
|
688
|
+
const report = this.runtime.health.hostGlobalSnapshot({
|
|
689
|
+
principal: context.principal,
|
|
690
|
+
operationId: idempotencyKey,
|
|
691
|
+
});
|
|
692
|
+
const route = this.validateOwnerRoute(context);
|
|
693
|
+
throwIfAborted(signal);
|
|
694
|
+
assertRequiredProviders(report, 'verification');
|
|
695
|
+
return Object.freeze({
|
|
696
|
+
status: 'succeeded', resultCode: 'health-verified', afterDigest: digest({
|
|
697
|
+
health: healthState(report), route,
|
|
698
|
+
}),
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
portFailure(error, 'health-verification-failed', false);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
//# sourceMappingURL=port.js.map
|