@unson/brainbase-mcp 0.10.15 → 0.12.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/contracts/judgment-value-proof/schema.json +19 -1
- package/dist/execution-authority-http.d.ts +4 -2
- package/dist/execution-authority-http.js +101 -6
- package/dist/execution-authority.d.ts +89 -8
- package/dist/execution-authority.js +360 -127
- package/dist/judgment-value-proof.d.ts +15 -0
- package/dist/judgment-value-proof.js +17 -0
- package/dist/knowledge-promotion.d.ts +61 -3
- package/dist/knowledge-promotion.js +112 -1
- package/dist/local-web-host.d.ts +4 -4
- package/dist/local-web-host.js +137 -12
- package/dist/server-support.d.ts +140 -0
- package/dist/server-support.js +158 -0
- package/package.json +6 -2
- package/ui/graph-registry-view.js +27 -1
- package/ui/local-web-shell.js +80 -8
- package/ui/project-workspace.js +1 -1
- package/ui/value-proof-review.css +54 -0
- package/ui/value-proof-review.js +127 -5
- package/ui/world-model-view.css +61 -0
- package/ui/world-model-view.js +215 -10
|
@@ -13,11 +13,11 @@ export class ExecutionAuthorityError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Persists the execution intent before crossing either the reservation or
|
|
16
|
-
* external-effect boundary. The reservation and intent
|
|
16
|
+
* external-effect boundary. The reservation and intent stores are separate
|
|
17
17
|
* transactions; callers must reconcile recovery_required/unknown states.
|
|
18
18
|
*/
|
|
19
19
|
export class ExecutionAuthorityService {
|
|
20
|
-
|
|
20
|
+
store;
|
|
21
21
|
authority;
|
|
22
22
|
approval;
|
|
23
23
|
constraint;
|
|
@@ -25,9 +25,7 @@ export class ExecutionAuthorityService {
|
|
|
25
25
|
readAccess;
|
|
26
26
|
effect;
|
|
27
27
|
clock;
|
|
28
|
-
sidecarPath;
|
|
29
28
|
constructor(options) {
|
|
30
|
-
assertText(options.dataDir, 'dataDir');
|
|
31
29
|
if (!options.authority || typeof options.authority.verify !== 'function')
|
|
32
30
|
throw new TypeError('authority port is required');
|
|
33
31
|
if (!options.approval || typeof options.approval.verify !== 'function')
|
|
@@ -39,9 +37,20 @@ export class ExecutionAuthorityService {
|
|
|
39
37
|
}
|
|
40
38
|
if (!options.readAccess || typeof options.readAccess.verify !== 'function')
|
|
41
39
|
throw new TypeError('read access port is required');
|
|
42
|
-
if (!options.effect || typeof options.effect.start !== 'function')
|
|
43
|
-
throw new TypeError('effect port
|
|
44
|
-
|
|
40
|
+
if (options.effect !== undefined && (!options.effect || typeof options.effect.start !== 'function')) {
|
|
41
|
+
throw new TypeError('effect port must implement start');
|
|
42
|
+
}
|
|
43
|
+
if (options.store) {
|
|
44
|
+
if (typeof options.store.read !== 'function' || typeof options.store.insert !== 'function' || typeof options.store.update !== 'function') {
|
|
45
|
+
throw new TypeError('store must implement read, insert, and update');
|
|
46
|
+
}
|
|
47
|
+
this.store = options.store;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
if (!isText(options.dataDir))
|
|
51
|
+
throw new TypeError('store or dataDir is required');
|
|
52
|
+
this.store = createSidecarExecutionIntentStore({ dataDir: options.dataDir, sidecarPath: options.sidecarPath });
|
|
53
|
+
}
|
|
45
54
|
this.authority = options.authority;
|
|
46
55
|
this.approval = options.approval;
|
|
47
56
|
this.constraint = options.constraint;
|
|
@@ -49,28 +58,155 @@ export class ExecutionAuthorityService {
|
|
|
49
58
|
this.readAccess = options.readAccess;
|
|
50
59
|
this.effect = options.effect;
|
|
51
60
|
this.clock = options.clock ?? (() => new Date());
|
|
52
|
-
this.sidecarPath = options.sidecarPath ?? defaultSidecarPath;
|
|
53
|
-
assertSidecarPath(this.sidecarPath);
|
|
54
61
|
}
|
|
55
62
|
async initialize() {
|
|
56
|
-
await
|
|
63
|
+
await this.storeCall(async () => { await this.store.initialize?.(); });
|
|
64
|
+
}
|
|
65
|
+
/** Runs the checks, marks the reservation, and calls the in-process effect port. */
|
|
66
|
+
async start(input, options = {}) {
|
|
67
|
+
if (!this.effect)
|
|
68
|
+
throw new ExecutionAuthorityError('effect_unconfigured', 'This host has no effect port; use register instead');
|
|
69
|
+
const normalized = normalizeInput(input);
|
|
70
|
+
const attribution = normalizeAttribution(options.attribution, normalized.principal);
|
|
71
|
+
const payloadDigest = digest(normalized);
|
|
72
|
+
await this.initialize();
|
|
73
|
+
await this.verifyReadAccess(normalized);
|
|
74
|
+
const existing = await this.findIntent(normalized);
|
|
75
|
+
if (existing)
|
|
76
|
+
return this.handleExisting(existing, payloadDigest, attribution);
|
|
77
|
+
const prepared = await this.prepare(normalized, payloadDigest, attribution, 'start');
|
|
78
|
+
if ('replay' in prepared)
|
|
79
|
+
return this.handleExisting(prepared.replay, payloadDigest, attribution);
|
|
80
|
+
let effectResult;
|
|
81
|
+
try {
|
|
82
|
+
effectResult = normalizeEffectResult(await this.effect.start({ ...normalized, capability: prepared.capability }));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
await this.persistRecovery(normalized, payloadDigest, {
|
|
86
|
+
code: 'effect_unknown',
|
|
87
|
+
reason: 'effect provider failed after the reservation start boundary',
|
|
88
|
+
reservationMark: 'started',
|
|
89
|
+
effect: 'unknown',
|
|
90
|
+
phase: 'unknown',
|
|
91
|
+
});
|
|
92
|
+
throw new ExecutionAuthorityError('effect_unknown', 'External effect start is unknown; reconciliation is required');
|
|
93
|
+
}
|
|
94
|
+
if (effectResult.status !== 'started') {
|
|
95
|
+
await this.persistRecovery(normalized, payloadDigest, {
|
|
96
|
+
code: 'effect_unknown',
|
|
97
|
+
reason: 'effect provider returned an unknown start state',
|
|
98
|
+
reservationMark: 'started',
|
|
99
|
+
effect: 'unknown',
|
|
100
|
+
phase: 'unknown',
|
|
101
|
+
});
|
|
102
|
+
throw new ExecutionAuthorityError('effect_unknown', 'External effect start is unknown; reconciliation is required');
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const intent = await this.updateIntent(normalized, payloadDigest, (next) => {
|
|
106
|
+
next.phase = 'started';
|
|
107
|
+
next.reservationMark = 'started';
|
|
108
|
+
next.effect.status = 'started';
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
intent: publicIntent(intent),
|
|
112
|
+
effect: { operationId: normalized.operationId, status: 'started' },
|
|
113
|
+
replayed: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new ExecutionAuthorityError('recovery_required', 'Effect started but the execution intent could not be finalized');
|
|
118
|
+
}
|
|
57
119
|
}
|
|
58
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Registers the execution: runs the checks, marks the reservation, and
|
|
122
|
+
* issues the permit. The effect owner reports the outcome with reportEffect.
|
|
123
|
+
*/
|
|
124
|
+
async register(input, options = {}) {
|
|
59
125
|
const normalized = normalizeInput(input);
|
|
126
|
+
const attribution = normalizeAttribution(options.attribution, normalized.principal);
|
|
60
127
|
const payloadDigest = digest(normalized);
|
|
61
128
|
await this.initialize();
|
|
62
129
|
await this.verifyReadAccess(normalized);
|
|
63
130
|
const existing = await this.findIntent(normalized);
|
|
64
131
|
if (existing)
|
|
65
|
-
return this.
|
|
66
|
-
const
|
|
132
|
+
return this.replayRegistration(existing, payloadDigest, attribution);
|
|
133
|
+
const prepared = await this.prepare(normalized, payloadDigest, attribution, 'register');
|
|
134
|
+
if ('replay' in prepared)
|
|
135
|
+
return this.replayRegistration(prepared.replay, payloadDigest, attribution);
|
|
136
|
+
return {
|
|
137
|
+
intent: publicIntent(prepared.intent),
|
|
138
|
+
capability: clone(prepared.capability),
|
|
139
|
+
capabilityDigest: prepared.capabilityDigest,
|
|
140
|
+
replayed: false,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Records the effect owner's outcome for a registered execution. */
|
|
144
|
+
async reportEffect(report) {
|
|
145
|
+
const lookup = normalizeLookup(report);
|
|
146
|
+
if (report.status !== 'started' && report.status !== 'unknown') {
|
|
147
|
+
throw new ExecutionAuthorityError('validation_error', 'status must be started or unknown');
|
|
148
|
+
}
|
|
149
|
+
assertText(report.capabilityDigest, 'capabilityDigest');
|
|
150
|
+
await this.initialize();
|
|
151
|
+
await this.verifyReadAccess(lookup);
|
|
152
|
+
const updated = await this.storeCall(() => this.store.update(intentKey(lookup), (current) => {
|
|
153
|
+
validateStoredIntent(current);
|
|
154
|
+
assertIntentScope(lookup, current);
|
|
155
|
+
if (current.capability?.digest !== report.capabilityDigest) {
|
|
156
|
+
throw new ExecutionAuthorityError('capability_mismatch', 'The report does not match the issued permit');
|
|
157
|
+
}
|
|
158
|
+
const next = clone(current);
|
|
159
|
+
if (report.status === 'started') {
|
|
160
|
+
if (current.phase === 'started')
|
|
161
|
+
return current;
|
|
162
|
+
if (current.phase !== 'registered' && current.phase !== 'unknown') {
|
|
163
|
+
throw new ExecutionAuthorityError('recovery_required', `Operation ${current.operationId} requires reconciliation before a report`);
|
|
164
|
+
}
|
|
165
|
+
next.phase = 'started';
|
|
166
|
+
next.effect.status = 'started';
|
|
167
|
+
delete next.failure;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
if (current.phase === 'unknown')
|
|
171
|
+
return current;
|
|
172
|
+
if (current.phase === 'started') {
|
|
173
|
+
throw new ExecutionAuthorityError('operation_conflict', `Operation ${current.operationId} already reported a started effect`);
|
|
174
|
+
}
|
|
175
|
+
if (current.phase !== 'registered') {
|
|
176
|
+
throw new ExecutionAuthorityError('recovery_required', `Operation ${current.operationId} requires reconciliation before a report`);
|
|
177
|
+
}
|
|
178
|
+
next.phase = 'unknown';
|
|
179
|
+
next.effect.status = 'unknown';
|
|
180
|
+
next.failure = { code: 'effect_unknown', reason: 'effect owner reported an unknown outcome' };
|
|
181
|
+
}
|
|
182
|
+
next.updatedAt = this.now().toISOString();
|
|
183
|
+
return next;
|
|
184
|
+
}));
|
|
185
|
+
if (!updated)
|
|
186
|
+
throw new ExecutionAuthorityError('intent_not_found', `Execution intent ${lookup.operationId} was not found`);
|
|
187
|
+
validateStoredIntent(updated);
|
|
188
|
+
return publicIntent(updated);
|
|
189
|
+
}
|
|
190
|
+
async readIntent(input) {
|
|
191
|
+
const lookup = normalizeLookup(input);
|
|
192
|
+
await this.initialize();
|
|
193
|
+
await this.verifyReadAccess(lookup);
|
|
194
|
+
const intent = await this.findIntent(lookup);
|
|
195
|
+
return intent ? publicIntent(intent) : undefined;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Shared path for start and register: initial checks, durable intent,
|
|
199
|
+
* fenced final checks, permit, and the reservation mark.
|
|
200
|
+
*/
|
|
201
|
+
async prepare(normalized, payloadDigest, attribution, mode) {
|
|
202
|
+
const initial = await this.verifyAll(normalized, 'initial', undefined, attribution);
|
|
67
203
|
if (initial.failure)
|
|
68
204
|
throw errorFromFailure(initial.failure);
|
|
69
|
-
const recorded = await this.recordIntent(normalized, payloadDigest, initial.checks);
|
|
205
|
+
const recorded = await this.recordIntent(normalized, payloadDigest, initial.checks, attribution);
|
|
70
206
|
if (!recorded.created)
|
|
71
|
-
return
|
|
207
|
+
return { replay: recorded.intent };
|
|
72
208
|
const expectedRevisions = Object.fromEntries(initial.checks.map((check) => [check.check, check.revision ?? '']));
|
|
73
|
-
const final = await this.verifyAll(normalized, 'final', expectedRevisions);
|
|
209
|
+
const final = await this.verifyAll(normalized, 'final', expectedRevisions, attribution);
|
|
74
210
|
if (final.failure) {
|
|
75
211
|
await this.tryUpdateIntent(normalized, payloadDigest, (intent) => {
|
|
76
212
|
intent.verification.final = final.checks;
|
|
@@ -91,11 +227,14 @@ export class ExecutionAuthorityService {
|
|
|
91
227
|
});
|
|
92
228
|
throw errorFromFailure(failure);
|
|
93
229
|
}
|
|
230
|
+
const capability = final.capability;
|
|
231
|
+
const capabilityDigest = digest(capability);
|
|
94
232
|
await this.updateIntent(normalized, payloadDigest, (intent) => {
|
|
95
233
|
intent.verification.final = final.checks;
|
|
96
234
|
intent.capability = {
|
|
97
|
-
expiresAt:
|
|
98
|
-
digest:
|
|
235
|
+
expiresAt: capability.expiresAt,
|
|
236
|
+
digest: capabilityDigest,
|
|
237
|
+
...(mode === 'register' ? { grant: clone(capability) } : {}),
|
|
99
238
|
};
|
|
100
239
|
});
|
|
101
240
|
let mark;
|
|
@@ -127,73 +266,31 @@ export class ExecutionAuthorityService {
|
|
|
127
266
|
});
|
|
128
267
|
throw new ExecutionAuthorityError('reservation_mark_unknown', 'Reservation start is unknown; reconciliation is required');
|
|
129
268
|
}
|
|
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
269
|
try {
|
|
166
270
|
const intent = await this.updateIntent(normalized, payloadDigest, (next) => {
|
|
167
|
-
next.phase = 'started';
|
|
168
271
|
next.reservationMark = 'started';
|
|
169
|
-
|
|
272
|
+
if (mode === 'register') {
|
|
273
|
+
// The permit is issued; the effect owner has not started yet.
|
|
274
|
+
next.phase = 'registered';
|
|
275
|
+
next.effect.status = 'not_started';
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
next.phase = 'reservation_marked';
|
|
279
|
+
// A crash after this write and during effect.start is ambiguous.
|
|
280
|
+
next.effect.status = 'unknown';
|
|
281
|
+
}
|
|
170
282
|
});
|
|
171
|
-
return {
|
|
172
|
-
intent,
|
|
173
|
-
effect: { operationId: normalized.operationId, status: 'started' },
|
|
174
|
-
replayed: false,
|
|
175
|
-
};
|
|
283
|
+
return { intent, capability, capabilityDigest };
|
|
176
284
|
}
|
|
177
285
|
catch {
|
|
178
|
-
throw new ExecutionAuthorityError('recovery_required', '
|
|
286
|
+
throw new ExecutionAuthorityError('recovery_required', 'Reservation started but the execution intent could not be advanced');
|
|
179
287
|
}
|
|
180
288
|
}
|
|
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
289
|
async findIntent(input) {
|
|
193
|
-
const
|
|
194
|
-
const intent = ledger.intents.find((candidate) => candidate.tenantId === input.tenantId && candidate.operationId === input.operationId);
|
|
290
|
+
const intent = await this.storeCall(() => this.store.read(intentKey(input)));
|
|
195
291
|
if (!intent)
|
|
196
292
|
return undefined;
|
|
293
|
+
validateStoredIntent(intent);
|
|
197
294
|
assertIntentScope(input, intent);
|
|
198
295
|
return clone(intent);
|
|
199
296
|
}
|
|
@@ -213,51 +310,44 @@ export class ExecutionAuthorityService {
|
|
|
213
310
|
const code = status === 'revoked' ? 'read_revoked' : status === 'expired' ? 'read_expired' : 'read_unknown';
|
|
214
311
|
throw new ExecutionAuthorityError(code, `Current execution intent read access is ${status}`);
|
|
215
312
|
}
|
|
216
|
-
async recordIntent(input, payloadDigest, checks) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
313
|
+
async recordIntent(input, payloadDigest, checks, attribution) {
|
|
314
|
+
const now = this.now().toISOString();
|
|
315
|
+
const intent = {
|
|
316
|
+
version: ledgerVersion,
|
|
317
|
+
operationId: input.operationId,
|
|
318
|
+
tenantId: input.tenantId,
|
|
319
|
+
principal: input.principal,
|
|
320
|
+
scopeId: input.scopeId,
|
|
321
|
+
runId: input.runId,
|
|
322
|
+
reservationId: input.reservationId,
|
|
323
|
+
approvalId: input.approvalId,
|
|
324
|
+
authorityRef: input.authorityRef,
|
|
325
|
+
constraintRefs: [...input.constraintRefs],
|
|
326
|
+
problem: { ...input.problem },
|
|
327
|
+
payloadDigest,
|
|
328
|
+
...(attribution ? { attribution: clone(attribution) } : {}),
|
|
329
|
+
// This is deliberately a non-replayable durable state. The current
|
|
330
|
+
// call may continue in memory; a later caller must reconcile it.
|
|
331
|
+
phase: 'recovery_required',
|
|
332
|
+
reservationMark: 'not_started',
|
|
333
|
+
effect: { operationId: input.operationId, status: 'not_started' },
|
|
334
|
+
verification: { initial: clone(checks) },
|
|
335
|
+
createdAt: now,
|
|
336
|
+
updatedAt: now,
|
|
337
|
+
};
|
|
338
|
+
const result = await this.storeCall(() => this.store.insert(intent));
|
|
339
|
+
validateStoredIntent(result.intent);
|
|
340
|
+
if (!result.created) {
|
|
341
|
+
assertIntentScope(input, result.intent);
|
|
342
|
+
if (result.intent.payloadDigest !== payloadDigest) {
|
|
343
|
+
throw new ExecutionAuthorityError('operation_conflict', `Operation ${input.operationId} was already used with a different payload`);
|
|
226
344
|
}
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
});
|
|
345
|
+
}
|
|
346
|
+
return { intent: clone(result.intent), created: result.created };
|
|
253
347
|
}
|
|
254
348
|
async updateIntent(input, payloadDigest, mutation) {
|
|
255
|
-
|
|
256
|
-
|
|
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];
|
|
349
|
+
const updated = await this.storeCall(() => this.store.update(intentKey(input), (existing) => {
|
|
350
|
+
validateStoredIntent(existing);
|
|
261
351
|
assertIntentScope(input, existing);
|
|
262
352
|
if (existing.payloadDigest !== payloadDigest) {
|
|
263
353
|
throw new ExecutionAuthorityError('operation_conflict', `Operation ${input.operationId} was already used with a different payload`);
|
|
@@ -265,9 +355,12 @@ export class ExecutionAuthorityService {
|
|
|
265
355
|
const nextIntent = clone(existing);
|
|
266
356
|
mutation(nextIntent);
|
|
267
357
|
nextIntent.updatedAt = this.now().toISOString();
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
358
|
+
return nextIntent;
|
|
359
|
+
}));
|
|
360
|
+
if (!updated)
|
|
361
|
+
throw new ExecutionAuthorityError('ledger_invalid', `Execution intent ${input.operationId} was not found`);
|
|
362
|
+
validateStoredIntent(updated);
|
|
363
|
+
return clone(updated);
|
|
271
364
|
}
|
|
272
365
|
async tryUpdateIntent(input, payloadDigest, mutation) {
|
|
273
366
|
try {
|
|
@@ -275,7 +368,7 @@ export class ExecutionAuthorityService {
|
|
|
275
368
|
}
|
|
276
369
|
catch {
|
|
277
370
|
// The durable recovery_required state is safer than retrying a failed
|
|
278
|
-
// cross-
|
|
371
|
+
// cross-store transition. Preserve the original denial to the caller.
|
|
279
372
|
}
|
|
280
373
|
}
|
|
281
374
|
async persistRecovery(input, payloadDigest, options) {
|
|
@@ -286,7 +379,7 @@ export class ExecutionAuthorityService {
|
|
|
286
379
|
intent.failure = { code: options.code, reason: options.reason };
|
|
287
380
|
});
|
|
288
381
|
}
|
|
289
|
-
async verifyAll(input, phase, expectedRevisions) {
|
|
382
|
+
async verifyAll(input, phase, expectedRevisions, attribution) {
|
|
290
383
|
const providers = [
|
|
291
384
|
['authority', this.authority],
|
|
292
385
|
['approval', this.approval],
|
|
@@ -298,6 +391,7 @@ export class ExecutionAuthorityService {
|
|
|
298
391
|
try {
|
|
299
392
|
result = await provider.verify({
|
|
300
393
|
...input,
|
|
394
|
+
...(attribution ? { attribution: clone(attribution) } : {}),
|
|
301
395
|
check,
|
|
302
396
|
phase,
|
|
303
397
|
expectedRevision: expectedRevisions?.[check],
|
|
@@ -345,6 +439,7 @@ export class ExecutionAuthorityService {
|
|
|
345
439
|
scopeId: input.scopeId,
|
|
346
440
|
issuedAt: now.toISOString(),
|
|
347
441
|
expiresAt: new Date(earliestExpiration).toISOString(),
|
|
442
|
+
...(attribution ? { attribution: clone(attribution) } : {}),
|
|
348
443
|
checks: Object.fromEntries(normalizedChecks.map(({ record, fencingToken, expiresAt }) => [record.check, {
|
|
349
444
|
revision: record.revision,
|
|
350
445
|
fencingToken: fencingToken,
|
|
@@ -353,13 +448,11 @@ export class ExecutionAuthorityService {
|
|
|
353
448
|
};
|
|
354
449
|
return { checks, capability };
|
|
355
450
|
}
|
|
356
|
-
handleExisting(intent, payloadDigest) {
|
|
357
|
-
|
|
358
|
-
throw new ExecutionAuthorityError('operation_conflict', `Operation ${intent.operationId} was already used with a different payload`);
|
|
359
|
-
}
|
|
451
|
+
handleExisting(intent, payloadDigest, attribution) {
|
|
452
|
+
assertSameRequest(intent, payloadDigest, attribution);
|
|
360
453
|
if (intent.phase === 'started' && intent.effect.status === 'started') {
|
|
361
454
|
return {
|
|
362
|
-
intent:
|
|
455
|
+
intent: publicIntent(intent),
|
|
363
456
|
effect: { operationId: intent.operationId, status: 'started' },
|
|
364
457
|
replayed: true,
|
|
365
458
|
};
|
|
@@ -371,6 +464,36 @@ export class ExecutionAuthorityService {
|
|
|
371
464
|
}
|
|
372
465
|
throw new ExecutionAuthorityError('recovery_required', `Operation ${intent.operationId} requires reconciliation before another attempt`);
|
|
373
466
|
}
|
|
467
|
+
replayRegistration(intent, payloadDigest, attribution) {
|
|
468
|
+
assertSameRequest(intent, payloadDigest, attribution);
|
|
469
|
+
const grant = intent.capability?.grant;
|
|
470
|
+
const replayable = intent.phase === 'registered' || intent.phase === 'started' || intent.phase === 'unknown';
|
|
471
|
+
if (replayable && grant && intent.capability) {
|
|
472
|
+
if (intent.phase === 'registered' && Date.parse(grant.expiresAt) <= this.now().getTime()) {
|
|
473
|
+
throw new ExecutionAuthorityError('capability_expired', `The permit for operation ${intent.operationId} has expired`);
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
intent: publicIntent(intent),
|
|
477
|
+
capability: clone(grant),
|
|
478
|
+
capabilityDigest: intent.capability.digest,
|
|
479
|
+
replayed: true,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
if (intent.phase === 'rejected' && intent.failure)
|
|
483
|
+
throw errorFromFailure(intent.failure);
|
|
484
|
+
throw new ExecutionAuthorityError('recovery_required', `Operation ${intent.operationId} requires reconciliation before another attempt`);
|
|
485
|
+
}
|
|
486
|
+
/** Store failures are never success and never switch to another ledger. */
|
|
487
|
+
async storeCall(call) {
|
|
488
|
+
try {
|
|
489
|
+
return await call();
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
if (error instanceof ExecutionAuthorityError)
|
|
493
|
+
throw error;
|
|
494
|
+
throw new ExecutionAuthorityError('store_unavailable', 'Execution intent store is unavailable');
|
|
495
|
+
}
|
|
496
|
+
}
|
|
374
497
|
now() {
|
|
375
498
|
const now = this.clock();
|
|
376
499
|
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
@@ -378,6 +501,47 @@ export class ExecutionAuthorityService {
|
|
|
378
501
|
return new Date(now);
|
|
379
502
|
}
|
|
380
503
|
}
|
|
504
|
+
/** The default store: a JSON sidecar under the local SSOT lock. */
|
|
505
|
+
export function createSidecarExecutionIntentStore(options) {
|
|
506
|
+
assertText(options?.dataDir, 'dataDir');
|
|
507
|
+
const dataDir = options.dataDir;
|
|
508
|
+
const sidecarPath = options.sidecarPath ?? defaultSidecarPath;
|
|
509
|
+
assertSidecarPath(sidecarPath);
|
|
510
|
+
const findIndex = (ledger, key) => ledger.intents
|
|
511
|
+
.findIndex((candidate) => candidate.tenantId === key.tenantId && candidate.operationId === key.operationId);
|
|
512
|
+
return {
|
|
513
|
+
async initialize() {
|
|
514
|
+
await initializePersonalOs(dataDir);
|
|
515
|
+
},
|
|
516
|
+
async read(key) {
|
|
517
|
+
const ledger = parseLedger(await readPersonalOsSidecar(dataDir, sidecarPath));
|
|
518
|
+
const index = findIndex(ledger, key);
|
|
519
|
+
return index < 0 ? undefined : clone(ledger.intents[index]);
|
|
520
|
+
},
|
|
521
|
+
async insert(intent) {
|
|
522
|
+
return mutatePersonalOsWithSidecar(dataDir, sidecarPath, async (current, sidecarContent) => {
|
|
523
|
+
const ledger = parseLedger(sidecarContent);
|
|
524
|
+
const index = findIndex(ledger, intent);
|
|
525
|
+
if (index >= 0) {
|
|
526
|
+
return { next: current, sidecarContent: serializeLedger(ledger), result: { intent: clone(ledger.intents[index]), created: false } };
|
|
527
|
+
}
|
|
528
|
+
ledger.intents.push(clone(intent));
|
|
529
|
+
return { next: current, sidecarContent: serializeLedger(ledger), result: { intent: clone(intent), created: true } };
|
|
530
|
+
});
|
|
531
|
+
},
|
|
532
|
+
async update(key, mutate) {
|
|
533
|
+
return mutatePersonalOsWithSidecar(dataDir, sidecarPath, async (current, sidecarContent) => {
|
|
534
|
+
const ledger = parseLedger(sidecarContent);
|
|
535
|
+
const index = findIndex(ledger, key);
|
|
536
|
+
if (index < 0)
|
|
537
|
+
return { next: current, sidecarContent: serializeLedger(ledger), result: undefined };
|
|
538
|
+
const next = mutate(clone(ledger.intents[index]));
|
|
539
|
+
ledger.intents[index] = clone(next);
|
|
540
|
+
return { next: current, sidecarContent: serializeLedger(ledger), result: clone(next) };
|
|
541
|
+
});
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
381
545
|
/**
|
|
382
546
|
* Binds the existing reservation store to the execution port without calling
|
|
383
547
|
* it from inside the execution sidecar transaction. The store remains the
|
|
@@ -478,6 +642,61 @@ function normalizeInput(input) {
|
|
|
478
642
|
problem,
|
|
479
643
|
};
|
|
480
644
|
}
|
|
645
|
+
const attributionModes = ['person', 'delegated_service', 'service'];
|
|
646
|
+
/** Enforces who may appear in which role; a service never passes as the person. */
|
|
647
|
+
function normalizeAttribution(value, principal) {
|
|
648
|
+
if (value === undefined)
|
|
649
|
+
return undefined;
|
|
650
|
+
if (!isRecord(value))
|
|
651
|
+
throw new ExecutionAuthorityError('validation_error', 'attribution must be an object');
|
|
652
|
+
const known = new Set(['mode', 'servicePrincipal', 'delegationRef', 'correlationId']);
|
|
653
|
+
if (Object.keys(value).some((key) => !known.has(key))) {
|
|
654
|
+
throw new ExecutionAuthorityError('validation_error', 'attribution contains an unknown field');
|
|
655
|
+
}
|
|
656
|
+
if (!attributionModes.includes(value.mode))
|
|
657
|
+
throw new ExecutionAuthorityError('validation_error', 'attribution.mode is invalid');
|
|
658
|
+
for (const field of ['servicePrincipal', 'delegationRef', 'correlationId']) {
|
|
659
|
+
if (value[field] !== undefined)
|
|
660
|
+
assertText(value[field], `attribution.${field}`);
|
|
661
|
+
}
|
|
662
|
+
if (value.mode === 'person') {
|
|
663
|
+
if (value.servicePrincipal !== undefined || value.delegationRef !== undefined) {
|
|
664
|
+
throw new ExecutionAuthorityError('validation_error', 'A person attribution cannot carry a service principal or delegation');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
if (value.servicePrincipal === undefined || value.delegationRef === undefined) {
|
|
669
|
+
throw new ExecutionAuthorityError('validation_error', `A ${value.mode} attribution requires servicePrincipal and delegationRef`);
|
|
670
|
+
}
|
|
671
|
+
if (value.mode === 'delegated_service' && value.servicePrincipal === principal) {
|
|
672
|
+
throw new ExecutionAuthorityError('validation_error', 'A delegated service must differ from the principal it acts for');
|
|
673
|
+
}
|
|
674
|
+
if (value.mode === 'service' && value.servicePrincipal !== principal) {
|
|
675
|
+
throw new ExecutionAuthorityError('validation_error', 'A service attribution must name the service as the principal');
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
mode: value.mode,
|
|
680
|
+
...(value.servicePrincipal !== undefined ? { servicePrincipal: value.servicePrincipal } : {}),
|
|
681
|
+
...(value.delegationRef !== undefined ? { delegationRef: value.delegationRef } : {}),
|
|
682
|
+
...(value.correlationId !== undefined ? { correlationId: value.correlationId } : {}),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function assertSameRequest(intent, payloadDigest, attribution) {
|
|
686
|
+
if (intent.payloadDigest !== payloadDigest || digest(intent.attribution ?? null) !== digest(attribution ?? null)) {
|
|
687
|
+
throw new ExecutionAuthorityError('operation_conflict', `Operation ${intent.operationId} was already used with a different payload or attribution`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/** Readers see the permit's expiry and digest, never its fencing tokens. */
|
|
691
|
+
function publicIntent(intent) {
|
|
692
|
+
const next = clone(intent);
|
|
693
|
+
if (next.capability)
|
|
694
|
+
next.capability = { expiresAt: next.capability.expiresAt, digest: next.capability.digest };
|
|
695
|
+
return next;
|
|
696
|
+
}
|
|
697
|
+
function intentKey(input) {
|
|
698
|
+
return { tenantId: input.tenantId, operationId: input.operationId };
|
|
699
|
+
}
|
|
481
700
|
function normalizeLookup(input) {
|
|
482
701
|
if (!input || typeof input !== 'object')
|
|
483
702
|
throw new ExecutionAuthorityError('validation_error', 'execution lookup is required');
|
|
@@ -587,7 +806,7 @@ function validateStoredIntent(value) {
|
|
|
587
806
|
|| !isText(value.principal)
|
|
588
807
|
|| !isText(value.scopeId)
|
|
589
808
|
|| !isText(value.payloadDigest)
|
|
590
|
-
|| !['recovery_required', 'reservation_marked', 'started', 'unknown', 'rejected'].includes(value.phase)
|
|
809
|
+
|| !['recovery_required', 'reservation_marked', 'registered', 'started', 'unknown', 'rejected'].includes(value.phase)
|
|
591
810
|
|| !['not_started', 'started', 'unknown'].includes(value.reservationMark)
|
|
592
811
|
|| !isRecord(value.effect)
|
|
593
812
|
|| value.effect.operationId !== value.operationId
|
|
@@ -600,6 +819,20 @@ function validateStoredIntent(value) {
|
|
|
600
819
|
|| !isText(value.updatedAt)) {
|
|
601
820
|
throw new ExecutionAuthorityError('ledger_invalid', 'Execution intent ledger contains an invalid record');
|
|
602
821
|
}
|
|
822
|
+
if (value.attribution !== undefined) {
|
|
823
|
+
try {
|
|
824
|
+
normalizeAttribution(value.attribution, value.principal);
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
throw new ExecutionAuthorityError('ledger_invalid', 'Execution intent ledger contains an invalid attribution');
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (value.capability !== undefined && (!isRecord(value.capability)
|
|
831
|
+
|| !isText(value.capability.expiresAt)
|
|
832
|
+
|| !isText(value.capability.digest)
|
|
833
|
+
|| (value.capability.grant !== undefined && (!isRecord(value.capability.grant) || value.capability.grant.kind !== 'execution-start')))) {
|
|
834
|
+
throw new ExecutionAuthorityError('ledger_invalid', 'Execution intent ledger contains an invalid capability');
|
|
835
|
+
}
|
|
603
836
|
}
|
|
604
837
|
function serializeLedger(ledger) {
|
|
605
838
|
return `${JSON.stringify(ledger, null, 2)}\n`;
|
|
@@ -14,12 +14,27 @@ export interface JudgmentValueProofArtifactRef {
|
|
|
14
14
|
}
|
|
15
15
|
/** Which part of the organization's judgment a basis came from. Absent means not recorded. */
|
|
16
16
|
export type JudgmentValueProofBasisLayer = 'philosophy' | 'objective' | 'world_model' | 'method' | 'constraint' | 'other';
|
|
17
|
+
/**
|
|
18
|
+
* A source that the local Web host can read and open. This is deliberately
|
|
19
|
+
* not a URL: the host owns the reader and keeps navigation same-origin.
|
|
20
|
+
*/
|
|
21
|
+
export type JudgmentValueProofBasisSourceKind = 'local_graph';
|
|
22
|
+
export type JudgmentValueProofBasisSourceEntityType = 'person' | 'org' | 'project' | 'decision';
|
|
23
|
+
export interface JudgmentValueProofBasisSource {
|
|
24
|
+
kind: JudgmentValueProofBasisSourceKind;
|
|
25
|
+
entity_id: string;
|
|
26
|
+
entity_type: JudgmentValueProofBasisSourceEntityType;
|
|
27
|
+
version?: string | null;
|
|
28
|
+
digest?: string | null;
|
|
29
|
+
}
|
|
17
30
|
export interface JudgmentValueProofBasis {
|
|
18
31
|
entity_id: string;
|
|
19
32
|
application: string;
|
|
20
33
|
layer?: JudgmentValueProofBasisLayer;
|
|
21
34
|
/** Version of the referenced entity used for this judgment. */
|
|
22
35
|
version?: string | null;
|
|
36
|
+
/** Optional, typed read contract for a source-bearing proof. */
|
|
37
|
+
source?: JudgmentValueProofBasisSource | null;
|
|
23
38
|
}
|
|
24
39
|
export interface JudgmentValueProofInheritanceSource {
|
|
25
40
|
kind: 'judgment' | 'method' | 'decision' | 'other';
|