@unson/brainbase-mcp 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/contracts/judgment-dag/digest.json +2 -2
- package/dist/canonical-task-contract.d.ts +9 -0
- package/dist/canonical-task-contract.js +39 -0
- package/dist/canonical-task-principal.d.ts +16 -0
- package/dist/canonical-task-principal.js +36 -0
- package/dist/canonical-task-service.d.ts +251 -0
- package/dist/canonical-task-service.js +809 -0
- package/dist/company-os-evaluation.d.ts +193 -0
- package/dist/company-os-evaluation.js +1150 -0
- package/dist/company-os-impact-review.d.ts +248 -0
- package/dist/company-os-impact-review.js +663 -0
- package/dist/company-os-learning-adoption.d.ts +288 -0
- package/dist/company-os-learning-adoption.js +944 -0
- package/dist/company-os-objectives.d.ts +52 -0
- package/dist/company-os-objectives.js +197 -0
- package/dist/company-os-receipt-adapter.d.ts +109 -0
- package/dist/company-os-receipt-adapter.js +636 -0
- package/dist/constraint-exception-store.d.ts +23 -0
- package/dist/constraint-exception-store.js +108 -0
- package/dist/constraint-resolution.d.ts +175 -0
- package/dist/constraint-resolution.js +922 -0
- package/dist/decision-adapter.d.ts +145 -0
- package/dist/decision-adapter.js +586 -0
- package/dist/durable-wait-http.d.ts +51 -0
- package/dist/durable-wait-http.js +497 -0
- package/dist/durable-waits.d.ts +234 -0
- package/dist/durable-waits.js +944 -0
- package/dist/execution-authority-http.d.ts +28 -0
- package/dist/execution-authority-http.js +359 -0
- package/dist/execution-authority.d.ts +201 -0
- package/dist/execution-authority.js +634 -0
- package/dist/foundation-catalog.d.ts +26 -0
- package/dist/foundation-catalog.js +206 -0
- package/dist/foundation-constraint-store.d.ts +50 -0
- package/dist/foundation-constraint-store.js +215 -0
- package/dist/foundation-http.d.ts +90 -0
- package/dist/foundation-http.js +439 -0
- package/dist/foundation-store.d.ts +132 -0
- package/dist/foundation-store.js +654 -0
- package/dist/judgment-dag-composition.d.ts +224 -0
- package/dist/judgment-dag-composition.js +731 -0
- package/dist/judgment-dag.d.ts +2 -0
- package/dist/judgment-dag.js +1 -0
- package/dist/judgment-problem-snapshot.d.ts +185 -0
- package/dist/judgment-problem-snapshot.js +777 -0
- package/dist/judgment-view-http.d.ts +22 -0
- package/dist/judgment-view-http.js +128 -0
- package/dist/judgment-view.d.ts +222 -0
- package/dist/judgment-view.js +627 -0
- package/dist/knowledge-adapter.d.ts +112 -0
- package/dist/knowledge-adapter.js +691 -0
- package/dist/ontology-foundation.d.ts +209 -0
- package/dist/ontology-foundation.js +1000 -0
- package/dist/ontology.d.ts +13 -0
- package/dist/ontology.js +14 -0
- package/dist/organization-connection.d.ts +164 -0
- package/dist/organization-connection.js +428 -0
- package/dist/personal-knowledge.d.ts +120 -0
- package/dist/personal-knowledge.js +750 -0
- package/dist/problem-candidates.d.ts +149 -0
- package/dist/problem-candidates.js +898 -0
- package/dist/problem-selection.d.ts +238 -0
- package/dist/problem-selection.js +995 -0
- package/dist/resource-reservation-http.d.ts +31 -0
- package/dist/resource-reservation-http.js +371 -0
- package/dist/resource-reservations.d.ts +221 -0
- package/dist/resource-reservations.js +577 -0
- package/dist/server.d.ts +97 -0
- package/dist/server.js +180 -1
- package/dist/ssot.d.ts +6 -1
- package/dist/ssot.js +63 -5
- package/dist/streamable-http.d.ts +42 -0
- package/dist/streamable-http.js +288 -0
- package/dist/types.d.ts +20 -0
- package/dist/world-model.d.ts +181 -0
- package/dist/world-model.js +873 -0
- package/package.json +145 -3
- package/ui/icons/mana/LICENSE.tabler.txt +21 -0
- package/ui/icons/mana/affiliate.svg +20 -0
- package/ui/icons/mana/brand-github.svg +15 -0
- package/ui/icons/mana/brand-google-drive.svg +17 -0
- package/ui/icons/mana/brand-slack.svg +18 -0
- package/ui/icons/mana/cpu.svg +24 -0
- package/ui/judgment-view.css +238 -0
- package/ui/judgment-view.js +508 -0
- package/ui/objective-editor.css +180 -0
- package/ui/objective-editor.js +1139 -0
- package/ui/outcome-knowledge.css +365 -0
- package/ui/outcome-knowledge.js +1570 -0
- package/ui/outcome-mana.css +255 -0
- package/ui/outcome-mana.js +2325 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { initializePersonalOs, mutatePersonalOsWithSidecar, readPersonalOsSidecar } from './ssot.js';
|
|
3
|
+
const ledgerVersion = 1;
|
|
4
|
+
const defaultSidecarPath = 'execution-authority/intents.json';
|
|
5
|
+
const snapshotIdPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
6
|
+
export class ExecutionAuthorityError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'ExecutionAuthorityError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Persists the execution intent before crossing either the reservation or
|
|
16
|
+
* external-effect boundary. The reservation and intent sidecars are separate
|
|
17
|
+
* transactions; callers must reconcile recovery_required/unknown states.
|
|
18
|
+
*/
|
|
19
|
+
export class ExecutionAuthorityService {
|
|
20
|
+
dataDir;
|
|
21
|
+
authority;
|
|
22
|
+
approval;
|
|
23
|
+
constraint;
|
|
24
|
+
reservation;
|
|
25
|
+
readAccess;
|
|
26
|
+
effect;
|
|
27
|
+
clock;
|
|
28
|
+
sidecarPath;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
assertText(options.dataDir, 'dataDir');
|
|
31
|
+
if (!options.authority || typeof options.authority.verify !== 'function')
|
|
32
|
+
throw new TypeError('authority port is required');
|
|
33
|
+
if (!options.approval || typeof options.approval.verify !== 'function')
|
|
34
|
+
throw new TypeError('approval port is required');
|
|
35
|
+
if (!options.constraint || typeof options.constraint.verify !== 'function')
|
|
36
|
+
throw new TypeError('constraint port is required');
|
|
37
|
+
if (!options.reservation || typeof options.reservation.verify !== 'function' || typeof options.reservation.markStarted !== 'function') {
|
|
38
|
+
throw new TypeError('reservation port is required');
|
|
39
|
+
}
|
|
40
|
+
if (!options.readAccess || typeof options.readAccess.verify !== 'function')
|
|
41
|
+
throw new TypeError('read access port is required');
|
|
42
|
+
if (!options.effect || typeof options.effect.start !== 'function')
|
|
43
|
+
throw new TypeError('effect port is required');
|
|
44
|
+
this.dataDir = options.dataDir;
|
|
45
|
+
this.authority = options.authority;
|
|
46
|
+
this.approval = options.approval;
|
|
47
|
+
this.constraint = options.constraint;
|
|
48
|
+
this.reservation = options.reservation;
|
|
49
|
+
this.readAccess = options.readAccess;
|
|
50
|
+
this.effect = options.effect;
|
|
51
|
+
this.clock = options.clock ?? (() => new Date());
|
|
52
|
+
this.sidecarPath = options.sidecarPath ?? defaultSidecarPath;
|
|
53
|
+
assertSidecarPath(this.sidecarPath);
|
|
54
|
+
}
|
|
55
|
+
async initialize() {
|
|
56
|
+
await initializePersonalOs(this.dataDir);
|
|
57
|
+
}
|
|
58
|
+
async start(input) {
|
|
59
|
+
const normalized = normalizeInput(input);
|
|
60
|
+
const payloadDigest = digest(normalized);
|
|
61
|
+
await this.initialize();
|
|
62
|
+
await this.verifyReadAccess(normalized);
|
|
63
|
+
const existing = await this.findIntent(normalized);
|
|
64
|
+
if (existing)
|
|
65
|
+
return this.handleExisting(existing, payloadDigest);
|
|
66
|
+
const initial = await this.verifyAll(normalized, 'initial');
|
|
67
|
+
if (initial.failure)
|
|
68
|
+
throw errorFromFailure(initial.failure);
|
|
69
|
+
const recorded = await this.recordIntent(normalized, payloadDigest, initial.checks);
|
|
70
|
+
if (!recorded.created)
|
|
71
|
+
return this.handleExisting(recorded.intent, payloadDigest);
|
|
72
|
+
const expectedRevisions = Object.fromEntries(initial.checks.map((check) => [check.check, check.revision ?? '']));
|
|
73
|
+
const final = await this.verifyAll(normalized, 'final', expectedRevisions);
|
|
74
|
+
if (final.failure) {
|
|
75
|
+
await this.tryUpdateIntent(normalized, payloadDigest, (intent) => {
|
|
76
|
+
intent.verification.final = final.checks;
|
|
77
|
+
intent.phase = 'rejected';
|
|
78
|
+
intent.failure = final.failure;
|
|
79
|
+
});
|
|
80
|
+
throw errorFromFailure(final.failure);
|
|
81
|
+
}
|
|
82
|
+
if (!final.capability) {
|
|
83
|
+
const failure = {
|
|
84
|
+
code: 'fencing_unsupported',
|
|
85
|
+
reason: 'Final execution checks did not provide a fencing capability',
|
|
86
|
+
};
|
|
87
|
+
await this.tryUpdateIntent(normalized, payloadDigest, (intent) => {
|
|
88
|
+
intent.verification.final = final.checks;
|
|
89
|
+
intent.phase = 'rejected';
|
|
90
|
+
intent.failure = failure;
|
|
91
|
+
});
|
|
92
|
+
throw errorFromFailure(failure);
|
|
93
|
+
}
|
|
94
|
+
await this.updateIntent(normalized, payloadDigest, (intent) => {
|
|
95
|
+
intent.verification.final = final.checks;
|
|
96
|
+
intent.capability = {
|
|
97
|
+
expiresAt: final.capability.expiresAt,
|
|
98
|
+
digest: digest(final.capability),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
let mark;
|
|
102
|
+
try {
|
|
103
|
+
const reservationCheck = final.checks.find((check) => check.check === 'reservation');
|
|
104
|
+
if (!reservationCheck || reservationCheck.status !== 'approved' || reservationCheck.revision === null) {
|
|
105
|
+
throw new ExecutionAuthorityError('ledger_invalid', 'Reservation verification did not provide a start fence');
|
|
106
|
+
}
|
|
107
|
+
mark = normalizeMarkResult(await this.reservation.markStarted({
|
|
108
|
+
...normalized,
|
|
109
|
+
expectedRevision: reservationCheck.revision,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
await this.persistRecovery(normalized, payloadDigest, {
|
|
114
|
+
code: 'reservation_mark_unknown',
|
|
115
|
+
reason: 'reservation start mark may have committed before the provider failed',
|
|
116
|
+
reservationMark: 'unknown',
|
|
117
|
+
effect: 'unknown',
|
|
118
|
+
});
|
|
119
|
+
throw new ExecutionAuthorityError('reservation_mark_unknown', 'Reservation start could not be determined; reconciliation is required');
|
|
120
|
+
}
|
|
121
|
+
if (mark.status !== 'started') {
|
|
122
|
+
await this.persistRecovery(normalized, payloadDigest, {
|
|
123
|
+
code: 'reservation_mark_unknown',
|
|
124
|
+
reason: 'reservation provider returned an unknown start state',
|
|
125
|
+
reservationMark: 'unknown',
|
|
126
|
+
effect: 'unknown',
|
|
127
|
+
});
|
|
128
|
+
throw new ExecutionAuthorityError('reservation_mark_unknown', 'Reservation start is unknown; reconciliation is required');
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
await this.updateIntent(normalized, payloadDigest, (intent) => {
|
|
132
|
+
intent.phase = 'reservation_marked';
|
|
133
|
+
intent.reservationMark = 'started';
|
|
134
|
+
// A crash after this write and during effect.start is ambiguous.
|
|
135
|
+
intent.effect.status = 'unknown';
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new ExecutionAuthorityError('recovery_required', 'Reservation started but the execution intent could not be advanced');
|
|
140
|
+
}
|
|
141
|
+
let effectResult;
|
|
142
|
+
try {
|
|
143
|
+
effectResult = normalizeEffectResult(await this.effect.start({ ...normalized, capability: final.capability }));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
await this.persistRecovery(normalized, payloadDigest, {
|
|
147
|
+
code: 'effect_unknown',
|
|
148
|
+
reason: 'effect provider failed after the reservation start boundary',
|
|
149
|
+
reservationMark: 'started',
|
|
150
|
+
effect: 'unknown',
|
|
151
|
+
phase: 'unknown',
|
|
152
|
+
});
|
|
153
|
+
throw new ExecutionAuthorityError('effect_unknown', 'External effect start is unknown; reconciliation is required');
|
|
154
|
+
}
|
|
155
|
+
if (effectResult.status !== 'started') {
|
|
156
|
+
await this.persistRecovery(normalized, payloadDigest, {
|
|
157
|
+
code: 'effect_unknown',
|
|
158
|
+
reason: 'effect provider returned an unknown start state',
|
|
159
|
+
reservationMark: 'started',
|
|
160
|
+
effect: 'unknown',
|
|
161
|
+
phase: 'unknown',
|
|
162
|
+
});
|
|
163
|
+
throw new ExecutionAuthorityError('effect_unknown', 'External effect start is unknown; reconciliation is required');
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const intent = await this.updateIntent(normalized, payloadDigest, (next) => {
|
|
167
|
+
next.phase = 'started';
|
|
168
|
+
next.reservationMark = 'started';
|
|
169
|
+
next.effect.status = 'started';
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
intent,
|
|
173
|
+
effect: { operationId: normalized.operationId, status: 'started' },
|
|
174
|
+
replayed: false,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
throw new ExecutionAuthorityError('recovery_required', 'Effect started but the execution intent could not be finalized');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async readIntent(input) {
|
|
182
|
+
const lookup = normalizeLookup(input);
|
|
183
|
+
await this.initialize();
|
|
184
|
+
await this.verifyReadAccess(lookup);
|
|
185
|
+
const ledger = parseLedger(await readPersonalOsSidecar(this.dataDir, this.sidecarPath));
|
|
186
|
+
const intent = ledger.intents.find((candidate) => candidate.tenantId === lookup.tenantId && candidate.operationId === lookup.operationId);
|
|
187
|
+
if (!intent)
|
|
188
|
+
return undefined;
|
|
189
|
+
assertIntentScope(lookup, intent);
|
|
190
|
+
return clone(intent);
|
|
191
|
+
}
|
|
192
|
+
async findIntent(input) {
|
|
193
|
+
const ledger = parseLedger(await readPersonalOsSidecar(this.dataDir, this.sidecarPath));
|
|
194
|
+
const intent = ledger.intents.find((candidate) => candidate.tenantId === input.tenantId && candidate.operationId === input.operationId);
|
|
195
|
+
if (!intent)
|
|
196
|
+
return undefined;
|
|
197
|
+
assertIntentScope(input, intent);
|
|
198
|
+
return clone(intent);
|
|
199
|
+
}
|
|
200
|
+
async verifyReadAccess(input) {
|
|
201
|
+
let result;
|
|
202
|
+
try {
|
|
203
|
+
result = await this.readAccess.verify({ ...input, phase: 'read' });
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
result = { status: 'unknown', revision: null };
|
|
207
|
+
}
|
|
208
|
+
const status = result && typeof result === 'object' && ['approved', 'revoked', 'expired', 'unknown'].includes(result.status)
|
|
209
|
+
? result.status
|
|
210
|
+
: 'unknown';
|
|
211
|
+
if (status === 'approved')
|
|
212
|
+
return;
|
|
213
|
+
const code = status === 'revoked' ? 'read_revoked' : status === 'expired' ? 'read_expired' : 'read_unknown';
|
|
214
|
+
throw new ExecutionAuthorityError(code, `Current execution intent read access is ${status}`);
|
|
215
|
+
}
|
|
216
|
+
async recordIntent(input, payloadDigest, checks) {
|
|
217
|
+
return mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, async (current, sidecarContent) => {
|
|
218
|
+
const ledger = parseLedger(sidecarContent);
|
|
219
|
+
const existing = ledger.intents.find((candidate) => candidate.tenantId === input.tenantId && candidate.operationId === input.operationId);
|
|
220
|
+
if (existing) {
|
|
221
|
+
assertIntentScope(input, existing);
|
|
222
|
+
if (existing.payloadDigest !== payloadDigest) {
|
|
223
|
+
throw new ExecutionAuthorityError('operation_conflict', `Operation ${input.operationId} was already used with a different payload`);
|
|
224
|
+
}
|
|
225
|
+
return { next: current, sidecarContent: serializeLedger(ledger), result: { intent: clone(existing), created: false } };
|
|
226
|
+
}
|
|
227
|
+
const now = this.now().toISOString();
|
|
228
|
+
const intent = {
|
|
229
|
+
version: ledgerVersion,
|
|
230
|
+
operationId: input.operationId,
|
|
231
|
+
tenantId: input.tenantId,
|
|
232
|
+
principal: input.principal,
|
|
233
|
+
scopeId: input.scopeId,
|
|
234
|
+
runId: input.runId,
|
|
235
|
+
reservationId: input.reservationId,
|
|
236
|
+
approvalId: input.approvalId,
|
|
237
|
+
authorityRef: input.authorityRef,
|
|
238
|
+
constraintRefs: [...input.constraintRefs],
|
|
239
|
+
problem: { ...input.problem },
|
|
240
|
+
payloadDigest,
|
|
241
|
+
// This is deliberately a non-replayable durable state. The current
|
|
242
|
+
// call may continue in memory; a later caller must reconcile it.
|
|
243
|
+
phase: 'recovery_required',
|
|
244
|
+
reservationMark: 'not_started',
|
|
245
|
+
effect: { operationId: input.operationId, status: 'not_started' },
|
|
246
|
+
verification: { initial: clone(checks) },
|
|
247
|
+
createdAt: now,
|
|
248
|
+
updatedAt: now,
|
|
249
|
+
};
|
|
250
|
+
ledger.intents.push(intent);
|
|
251
|
+
return { next: current, sidecarContent: serializeLedger(ledger), result: { intent: clone(intent), created: true } };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async updateIntent(input, payloadDigest, mutation) {
|
|
255
|
+
return mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, async (current, sidecarContent) => {
|
|
256
|
+
const ledger = parseLedger(sidecarContent);
|
|
257
|
+
const index = ledger.intents.findIndex((candidate) => candidate.tenantId === input.tenantId && candidate.operationId === input.operationId);
|
|
258
|
+
if (index < 0)
|
|
259
|
+
throw new ExecutionAuthorityError('ledger_invalid', `Execution intent ${input.operationId} was not found`);
|
|
260
|
+
const existing = ledger.intents[index];
|
|
261
|
+
assertIntentScope(input, existing);
|
|
262
|
+
if (existing.payloadDigest !== payloadDigest) {
|
|
263
|
+
throw new ExecutionAuthorityError('operation_conflict', `Operation ${input.operationId} was already used with a different payload`);
|
|
264
|
+
}
|
|
265
|
+
const nextIntent = clone(existing);
|
|
266
|
+
mutation(nextIntent);
|
|
267
|
+
nextIntent.updatedAt = this.now().toISOString();
|
|
268
|
+
ledger.intents[index] = nextIntent;
|
|
269
|
+
return { next: current, sidecarContent: serializeLedger(ledger), result: clone(nextIntent) };
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async tryUpdateIntent(input, payloadDigest, mutation) {
|
|
273
|
+
try {
|
|
274
|
+
await this.updateIntent(input, payloadDigest, mutation);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// The durable recovery_required state is safer than retrying a failed
|
|
278
|
+
// cross-sidecar transition. Preserve the original denial to the caller.
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async persistRecovery(input, payloadDigest, options) {
|
|
282
|
+
await this.tryUpdateIntent(input, payloadDigest, (intent) => {
|
|
283
|
+
intent.phase = options.phase ?? 'recovery_required';
|
|
284
|
+
intent.reservationMark = options.reservationMark;
|
|
285
|
+
intent.effect.status = options.effect;
|
|
286
|
+
intent.failure = { code: options.code, reason: options.reason };
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
async verifyAll(input, phase, expectedRevisions) {
|
|
290
|
+
const providers = [
|
|
291
|
+
['authority', this.authority],
|
|
292
|
+
['approval', this.approval],
|
|
293
|
+
['constraint', this.constraint],
|
|
294
|
+
['reservation', this.reservation],
|
|
295
|
+
];
|
|
296
|
+
const normalizedChecks = await Promise.all(providers.map(async ([check, provider]) => {
|
|
297
|
+
let result;
|
|
298
|
+
try {
|
|
299
|
+
result = await provider.verify({
|
|
300
|
+
...input,
|
|
301
|
+
check,
|
|
302
|
+
phase,
|
|
303
|
+
expectedRevision: expectedRevisions?.[check],
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
result = { status: 'unknown', revision: null };
|
|
308
|
+
}
|
|
309
|
+
return normalizeCheckResult(check, result);
|
|
310
|
+
}));
|
|
311
|
+
const checks = normalizedChecks.map(({ record }) => record);
|
|
312
|
+
const denied = checks.find((check) => check.status !== 'approved');
|
|
313
|
+
if (denied) {
|
|
314
|
+
return {
|
|
315
|
+
checks,
|
|
316
|
+
failure: {
|
|
317
|
+
code: checkFailureCode(denied.check, denied.status),
|
|
318
|
+
reason: `${denied.check} provider returned ${denied.status}`,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (expectedRevisions) {
|
|
323
|
+
const changed = checks.find((check) => check.revision !== expectedRevisions[check.check]);
|
|
324
|
+
if (changed) {
|
|
325
|
+
return { checks, failure: { code: 'revision_conflict', reason: `${changed.check} revision changed before execution start` } };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (phase !== 'final')
|
|
329
|
+
return { checks };
|
|
330
|
+
const missingFence = normalizedChecks.find(({ fencingToken, expiresAt }) => fencingToken === null || expiresAt === null);
|
|
331
|
+
if (missingFence) {
|
|
332
|
+
return { checks, failure: { code: 'fencing_unsupported', reason: `${missingFence.record.check} provider did not return an execution fencing capability` } };
|
|
333
|
+
}
|
|
334
|
+
const now = this.now();
|
|
335
|
+
const expirationTimes = normalizedChecks.map(({ expiresAt }) => Date.parse(expiresAt));
|
|
336
|
+
const earliestExpiration = Math.min(...expirationTimes);
|
|
337
|
+
if (!Number.isFinite(earliestExpiration) || earliestExpiration <= now.getTime()) {
|
|
338
|
+
return { checks, failure: { code: 'capability_expired', reason: 'Execution fencing capability is already expired' } };
|
|
339
|
+
}
|
|
340
|
+
const capability = {
|
|
341
|
+
kind: 'execution-start',
|
|
342
|
+
operationId: input.operationId,
|
|
343
|
+
tenantId: input.tenantId,
|
|
344
|
+
principal: input.principal,
|
|
345
|
+
scopeId: input.scopeId,
|
|
346
|
+
issuedAt: now.toISOString(),
|
|
347
|
+
expiresAt: new Date(earliestExpiration).toISOString(),
|
|
348
|
+
checks: Object.fromEntries(normalizedChecks.map(({ record, fencingToken, expiresAt }) => [record.check, {
|
|
349
|
+
revision: record.revision,
|
|
350
|
+
fencingToken: fencingToken,
|
|
351
|
+
expiresAt: expiresAt,
|
|
352
|
+
}])),
|
|
353
|
+
};
|
|
354
|
+
return { checks, capability };
|
|
355
|
+
}
|
|
356
|
+
handleExisting(intent, payloadDigest) {
|
|
357
|
+
if (intent.payloadDigest !== payloadDigest) {
|
|
358
|
+
throw new ExecutionAuthorityError('operation_conflict', `Operation ${intent.operationId} was already used with a different payload`);
|
|
359
|
+
}
|
|
360
|
+
if (intent.phase === 'started' && intent.effect.status === 'started') {
|
|
361
|
+
return {
|
|
362
|
+
intent: clone(intent),
|
|
363
|
+
effect: { operationId: intent.operationId, status: 'started' },
|
|
364
|
+
replayed: true,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (intent.phase === 'rejected' && intent.failure)
|
|
368
|
+
throw errorFromFailure(intent.failure);
|
|
369
|
+
if (intent.phase === 'unknown' && intent.failure?.code === 'effect_unknown') {
|
|
370
|
+
throw errorFromFailure(intent.failure);
|
|
371
|
+
}
|
|
372
|
+
throw new ExecutionAuthorityError('recovery_required', `Operation ${intent.operationId} requires reconciliation before another attempt`);
|
|
373
|
+
}
|
|
374
|
+
now() {
|
|
375
|
+
const now = this.clock();
|
|
376
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
377
|
+
throw new ExecutionAuthorityError('validation_error', 'Clock returned an invalid date');
|
|
378
|
+
return new Date(now);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Binds the existing reservation store to the execution port without calling
|
|
383
|
+
* it from inside the execution sidecar transaction. The store remains the
|
|
384
|
+
* owner of its current authorization and reservation-state checks.
|
|
385
|
+
*/
|
|
386
|
+
export function createResourceReservationExecutionPort(options) {
|
|
387
|
+
if (!options || !options.service || typeof options.service.readLedger !== 'function' || typeof options.service.startExternalAction !== 'function') {
|
|
388
|
+
throw new TypeError('resource reservation service is required');
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
async verify(input) {
|
|
392
|
+
try {
|
|
393
|
+
const snapshot = await options.service.readLedger({
|
|
394
|
+
tenantId: input.tenantId,
|
|
395
|
+
principal: input.principal,
|
|
396
|
+
scopeId: input.scopeId,
|
|
397
|
+
});
|
|
398
|
+
const reservation = snapshot.reservations.find((candidate) => candidate.reservationId === input.reservationId);
|
|
399
|
+
if (!reservation || reservation.state !== 'reserved')
|
|
400
|
+
return { status: 'revoked', revision: 'missing' };
|
|
401
|
+
const revision = `${reservation.reservationId}:${reservation.updatedAt}`;
|
|
402
|
+
if (reservation.externalAction.status !== 'not_started')
|
|
403
|
+
return { status: 'unknown', revision };
|
|
404
|
+
const expiresAt = reservation.approval.expiresAt;
|
|
405
|
+
if (!expiresAt)
|
|
406
|
+
return { status: 'unknown', revision };
|
|
407
|
+
return {
|
|
408
|
+
status: 'approved',
|
|
409
|
+
revision,
|
|
410
|
+
fencingToken: `reservation:${revision}`,
|
|
411
|
+
expiresAt,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return { status: 'unknown', revision: null };
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
async markStarted(input) {
|
|
419
|
+
try {
|
|
420
|
+
const snapshot = await options.service.readLedger({
|
|
421
|
+
tenantId: input.tenantId,
|
|
422
|
+
principal: input.principal,
|
|
423
|
+
scopeId: input.scopeId,
|
|
424
|
+
});
|
|
425
|
+
const reservation = snapshot.reservations.find((candidate) => candidate.reservationId === input.reservationId);
|
|
426
|
+
if (!reservation || reservation.state !== 'reserved' || reservation.externalAction.status !== 'not_started') {
|
|
427
|
+
return { status: 'unknown' };
|
|
428
|
+
}
|
|
429
|
+
const currentRevision = `${reservation.reservationId}:${reservation.updatedAt}`;
|
|
430
|
+
if (currentRevision !== input.expectedRevision)
|
|
431
|
+
return { status: 'unknown' };
|
|
432
|
+
const result = await options.service.startExternalAction(toReservationMutationInput(input));
|
|
433
|
+
return { status: result.reservation.externalAction.status === 'started' ? 'started' : 'unknown' };
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return { status: 'unknown' };
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function toReservationMutationInput(input) {
|
|
442
|
+
return {
|
|
443
|
+
operationId: input.operationId,
|
|
444
|
+
tenantId: input.tenantId,
|
|
445
|
+
principal: input.principal,
|
|
446
|
+
scopeId: input.scopeId,
|
|
447
|
+
reservationId: input.reservationId,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function normalizeInput(input) {
|
|
451
|
+
if (!input || typeof input !== 'object')
|
|
452
|
+
throw new ExecutionAuthorityError('validation_error', 'execution input is required');
|
|
453
|
+
assertText(input.operationId, 'operationId');
|
|
454
|
+
assertText(input.tenantId, 'tenantId');
|
|
455
|
+
assertText(input.principal, 'principal');
|
|
456
|
+
assertText(input.scopeId, 'scopeId');
|
|
457
|
+
assertText(input.runId, 'runId');
|
|
458
|
+
assertText(input.reservationId, 'reservationId');
|
|
459
|
+
assertText(input.approvalId, 'approvalId');
|
|
460
|
+
assertText(input.authorityRef, 'authorityRef');
|
|
461
|
+
if (!Array.isArray(input.constraintRefs))
|
|
462
|
+
throw new ExecutionAuthorityError('validation_error', 'constraintRefs must be an array');
|
|
463
|
+
const constraintRefs = input.constraintRefs.map((ref, index) => {
|
|
464
|
+
assertText(ref, `constraintRefs[${index}]`);
|
|
465
|
+
return ref;
|
|
466
|
+
});
|
|
467
|
+
const problem = normalizeProblem(input.problem);
|
|
468
|
+
return {
|
|
469
|
+
operationId: input.operationId,
|
|
470
|
+
tenantId: input.tenantId,
|
|
471
|
+
principal: input.principal,
|
|
472
|
+
scopeId: input.scopeId,
|
|
473
|
+
runId: input.runId,
|
|
474
|
+
reservationId: input.reservationId,
|
|
475
|
+
approvalId: input.approvalId,
|
|
476
|
+
authorityRef: input.authorityRef,
|
|
477
|
+
constraintRefs,
|
|
478
|
+
problem,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function normalizeLookup(input) {
|
|
482
|
+
if (!input || typeof input !== 'object')
|
|
483
|
+
throw new ExecutionAuthorityError('validation_error', 'execution lookup is required');
|
|
484
|
+
assertText(input.operationId, 'operationId');
|
|
485
|
+
assertText(input.tenantId, 'tenantId');
|
|
486
|
+
assertText(input.principal, 'principal');
|
|
487
|
+
assertText(input.scopeId, 'scopeId');
|
|
488
|
+
return { operationId: input.operationId, tenantId: input.tenantId, principal: input.principal, scopeId: input.scopeId };
|
|
489
|
+
}
|
|
490
|
+
function normalizeProblem(problem) {
|
|
491
|
+
assertText(problem?.problem_snapshot_id, 'problem.problem_snapshot_id');
|
|
492
|
+
assertText(problem?.problem_id, 'problem.problem_id');
|
|
493
|
+
assertText(problem?.revision, 'problem.revision');
|
|
494
|
+
if (!snapshotIdPattern.test(problem.problem_snapshot_id)) {
|
|
495
|
+
throw new ExecutionAuthorityError('validation_error', 'problem.problem_snapshot_id must be a sha256 snapshot ID');
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
problem_snapshot_id: problem.problem_snapshot_id,
|
|
499
|
+
problem_id: problem.problem_id,
|
|
500
|
+
revision: problem.revision,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function normalizeCheckResult(check, result) {
|
|
504
|
+
const status = result && typeof result === 'object' && ['approved', 'revoked', 'expired', 'unknown'].includes(result.status)
|
|
505
|
+
? result.status
|
|
506
|
+
: 'unknown';
|
|
507
|
+
const revision = typeof result?.revision === 'string' && result.revision.length > 0 ? result.revision : null;
|
|
508
|
+
const fencingToken = isText(result?.fencingToken) ? result.fencingToken : null;
|
|
509
|
+
const expiresAt = normalizeExpiration(result?.expiresAt);
|
|
510
|
+
const evidence = normalizeEvidence(result?.evidence);
|
|
511
|
+
if (status === 'approved' && revision === null) {
|
|
512
|
+
return {
|
|
513
|
+
record: { check, status: 'unknown', revision: null, fencingTokenDigest: fencingToken ? digest(fencingToken) : undefined, expiresAt: expiresAt ?? undefined, evidence },
|
|
514
|
+
fencingToken,
|
|
515
|
+
expiresAt,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
record: { check, status, revision, fencingTokenDigest: fencingToken ? digest(fencingToken) : undefined, expiresAt: expiresAt ?? undefined, evidence },
|
|
520
|
+
fencingToken,
|
|
521
|
+
expiresAt,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function normalizeExpiration(value) {
|
|
525
|
+
if (!isText(value))
|
|
526
|
+
return null;
|
|
527
|
+
const time = Date.parse(value);
|
|
528
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
|
529
|
+
}
|
|
530
|
+
function normalizeMarkResult(result) {
|
|
531
|
+
if (!result || (result.status !== 'started' && result.status !== 'unknown'))
|
|
532
|
+
return { status: 'unknown' };
|
|
533
|
+
return { status: result.status };
|
|
534
|
+
}
|
|
535
|
+
function normalizeEffectResult(result) {
|
|
536
|
+
if (!result || (result.status !== 'started' && result.status !== 'unknown'))
|
|
537
|
+
return { status: 'unknown' };
|
|
538
|
+
return { status: result.status };
|
|
539
|
+
}
|
|
540
|
+
function normalizeEvidence(evidence) {
|
|
541
|
+
if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence))
|
|
542
|
+
return {};
|
|
543
|
+
const normalized = {};
|
|
544
|
+
for (const [key, value] of Object.entries(evidence)) {
|
|
545
|
+
if (isText(key) && isText(value))
|
|
546
|
+
normalized[key] = value;
|
|
547
|
+
}
|
|
548
|
+
return normalized;
|
|
549
|
+
}
|
|
550
|
+
function checkFailureCode(check, status) {
|
|
551
|
+
if (status === 'revoked')
|
|
552
|
+
return `${check}_revoked`;
|
|
553
|
+
if (status === 'expired')
|
|
554
|
+
return `${check}_expired`;
|
|
555
|
+
return `${check}_unknown`;
|
|
556
|
+
}
|
|
557
|
+
function errorFromFailure(failure) {
|
|
558
|
+
return new ExecutionAuthorityError(failure.code, failure.reason);
|
|
559
|
+
}
|
|
560
|
+
function assertIntentScope(input, intent) {
|
|
561
|
+
if (input.tenantId !== intent.tenantId || input.principal !== intent.principal || input.scopeId !== intent.scopeId) {
|
|
562
|
+
throw new ExecutionAuthorityError('scope_mismatch', `Execution intent ${intent.operationId} is outside the requested scope`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function parseLedger(content) {
|
|
566
|
+
if (!content || content.trim() === '')
|
|
567
|
+
return { version: ledgerVersion, intents: [] };
|
|
568
|
+
let value;
|
|
569
|
+
try {
|
|
570
|
+
value = JSON.parse(content);
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
throw new ExecutionAuthorityError('ledger_invalid', 'Execution intent ledger is not valid JSON');
|
|
574
|
+
}
|
|
575
|
+
if (!isRecord(value) || value.version !== ledgerVersion || !Array.isArray(value.intents)) {
|
|
576
|
+
throw new ExecutionAuthorityError('ledger_invalid', 'Execution intent ledger has an unsupported shape');
|
|
577
|
+
}
|
|
578
|
+
for (const intent of value.intents)
|
|
579
|
+
validateStoredIntent(intent);
|
|
580
|
+
return value;
|
|
581
|
+
}
|
|
582
|
+
function validateStoredIntent(value) {
|
|
583
|
+
if (!isRecord(value)
|
|
584
|
+
|| value.version !== ledgerVersion
|
|
585
|
+
|| !isText(value.operationId)
|
|
586
|
+
|| !isText(value.tenantId)
|
|
587
|
+
|| !isText(value.principal)
|
|
588
|
+
|| !isText(value.scopeId)
|
|
589
|
+
|| !isText(value.payloadDigest)
|
|
590
|
+
|| !['recovery_required', 'reservation_marked', 'started', 'unknown', 'rejected'].includes(value.phase)
|
|
591
|
+
|| !['not_started', 'started', 'unknown'].includes(value.reservationMark)
|
|
592
|
+
|| !isRecord(value.effect)
|
|
593
|
+
|| value.effect.operationId !== value.operationId
|
|
594
|
+
|| !['not_started', 'started', 'unknown'].includes(value.effect.status)
|
|
595
|
+
|| !isRecord(value.verification)
|
|
596
|
+
|| !Array.isArray(value.verification.initial)
|
|
597
|
+
|| !Array.isArray(value.constraintRefs)
|
|
598
|
+
|| !isRecord(value.problem)
|
|
599
|
+
|| !isText(value.createdAt)
|
|
600
|
+
|| !isText(value.updatedAt)) {
|
|
601
|
+
throw new ExecutionAuthorityError('ledger_invalid', 'Execution intent ledger contains an invalid record');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function serializeLedger(ledger) {
|
|
605
|
+
return `${JSON.stringify(ledger, null, 2)}\n`;
|
|
606
|
+
}
|
|
607
|
+
function digest(value) {
|
|
608
|
+
return createHash('sha256').update(JSON.stringify(sortJson(value))).digest('hex');
|
|
609
|
+
}
|
|
610
|
+
function sortJson(value) {
|
|
611
|
+
if (Array.isArray(value))
|
|
612
|
+
return value.map((item) => sortJson(item));
|
|
613
|
+
if (!isRecord(value))
|
|
614
|
+
return value;
|
|
615
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, sortJson(item)]));
|
|
616
|
+
}
|
|
617
|
+
function assertText(value, label) {
|
|
618
|
+
if (!isText(value))
|
|
619
|
+
throw new ExecutionAuthorityError('validation_error', `${label} must be a bounded text value`);
|
|
620
|
+
}
|
|
621
|
+
function isText(value) {
|
|
622
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 512 && !/[\u0000-\u001f\u007f]/u.test(value);
|
|
623
|
+
}
|
|
624
|
+
function assertSidecarPath(path) {
|
|
625
|
+
if (!path || path.startsWith('/') || path.includes('\\') || path.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
626
|
+
throw new ExecutionAuthorityError('validation_error', 'sidecarPath must remain relative to the SSOT root');
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function isRecord(value) {
|
|
630
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
631
|
+
}
|
|
632
|
+
function clone(value) {
|
|
633
|
+
return structuredClone(value);
|
|
634
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FoundationDefinition, FoundationRevision, FoundationType } from './ontology-foundation.js';
|
|
2
|
+
import type { FoundationCatalog, FoundationCatalogRecord } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The key is an encoded tuple rather than `${type}:${id}` so IDs containing a
|
|
5
|
+
* colon cannot alias another logical resource.
|
|
6
|
+
*/
|
|
7
|
+
export declare function foundationKey(reference: Pick<FoundationRevision, 'type' | 'id'>): string;
|
|
8
|
+
export declare function createEmptyFoundationCatalog(): FoundationCatalog;
|
|
9
|
+
export declare function cloneFoundationCatalog(catalog: FoundationCatalog): FoundationCatalog;
|
|
10
|
+
export declare function cloneFoundationDefinition<T extends FoundationDefinition>(definition: T): T;
|
|
11
|
+
export interface FoundationRef extends FoundationRevision {
|
|
12
|
+
digest: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function foundationRef(record: FoundationCatalogRecord): FoundationRef;
|
|
15
|
+
/**
|
|
16
|
+
* Hashes the complete definition, with object keys sorted recursively. Array
|
|
17
|
+
* order remains significant because criteria, provenance, and authorized uses
|
|
18
|
+
* are ordered contracts in the shared ontology.
|
|
19
|
+
*/
|
|
20
|
+
export declare function digestFoundationDefinition(definition: FoundationDefinition): string;
|
|
21
|
+
export declare function nextFoundationRevision(revision: string): string;
|
|
22
|
+
export declare function assertFoundationRevision(value: unknown, label?: string): asserts value is FoundationRevision;
|
|
23
|
+
export declare function assertFoundationCatalog(value: unknown): asserts value is FoundationCatalog | undefined;
|
|
24
|
+
export declare function foundationCatalogOrEmpty(value: FoundationCatalog | undefined): FoundationCatalog;
|
|
25
|
+
export declare function findFoundationRecord(catalog: FoundationCatalog, reference: FoundationRevision): FoundationCatalogRecord | undefined;
|
|
26
|
+
export declare function findLatestFoundationRecord(catalog: FoundationCatalog, type: FoundationType, id: string): FoundationCatalogRecord | undefined;
|