@unson/brainbase-mcp 0.6.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/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 +139 -3
- 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
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { initializePersonalOs, mutatePersonalOsWithSidecar, readPersonalOsSidecar } from './ssot.js';
|
|
3
|
+
const ledgerVersion = 1;
|
|
4
|
+
const defaultSidecarPath = 'resource-reservations/ledger.json';
|
|
5
|
+
const snapshotIdPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
6
|
+
/**
|
|
7
|
+
* Adapt the real immutable ProblemSnapshot store to the reservation port.
|
|
8
|
+
*
|
|
9
|
+
* Story05 owns loading, integrity checks, and current read ACL checks. This
|
|
10
|
+
* adapter only binds the caller's principal, loads the current snapshot, and
|
|
11
|
+
* verifies the fixed problem/revision and scope reference. The loader must
|
|
12
|
+
* connect Story05's canonical reference provider. The tenant boundary and
|
|
13
|
+
* execution authorization remain the reservation/auth ports.
|
|
14
|
+
*/
|
|
15
|
+
export function createResourceReservationProblemSnapshotPort(options) {
|
|
16
|
+
if (!options || typeof options !== 'object' || typeof options.root !== 'string' || options.root.length === 0) {
|
|
17
|
+
throw new TypeError('problem snapshot root must be a non-empty path');
|
|
18
|
+
}
|
|
19
|
+
if (typeof options.load !== 'function') {
|
|
20
|
+
throw new TypeError('problem snapshot loader is required');
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
async verify({ principal, scopeId, reference }) {
|
|
24
|
+
try {
|
|
25
|
+
const snapshot = await options.load({
|
|
26
|
+
root: options.root,
|
|
27
|
+
snapshot_id: reference.problem_snapshot_id,
|
|
28
|
+
access: { principal },
|
|
29
|
+
reference_resolution: 'current',
|
|
30
|
+
});
|
|
31
|
+
return snapshot.snapshot_version === 'judgment-problem-snapshot.v1'
|
|
32
|
+
&& snapshot.problem_id === reference.problem_id
|
|
33
|
+
&& snapshot.revision === reference.revision
|
|
34
|
+
&& snapshot.owner_scope.id === scopeId
|
|
35
|
+
&& snapshot.execution_permission === 'none';
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export class ResourceReservationError extends Error {
|
|
44
|
+
code;
|
|
45
|
+
constructor(code, message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = 'ResourceReservationError';
|
|
48
|
+
this.code = code;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export class ResourceReservationService {
|
|
52
|
+
dataDir;
|
|
53
|
+
authorization;
|
|
54
|
+
problemSnapshot;
|
|
55
|
+
capacity;
|
|
56
|
+
clock;
|
|
57
|
+
sidecarPath;
|
|
58
|
+
constructor(options) {
|
|
59
|
+
assertText(options.dataDir, 'dataDir');
|
|
60
|
+
this.dataDir = options.dataDir;
|
|
61
|
+
this.authorization = options.authorization;
|
|
62
|
+
this.problemSnapshot = options.problemSnapshot;
|
|
63
|
+
this.capacity = options.capacity;
|
|
64
|
+
this.clock = options.clock ?? (() => new Date());
|
|
65
|
+
this.sidecarPath = options.sidecarPath ?? defaultSidecarPath;
|
|
66
|
+
assertSidecarPath(this.sidecarPath);
|
|
67
|
+
}
|
|
68
|
+
async initialize() {
|
|
69
|
+
await initializePersonalOs(this.dataDir);
|
|
70
|
+
}
|
|
71
|
+
async estimate(input) {
|
|
72
|
+
const request = normalizeRequest(input);
|
|
73
|
+
await this.verifyProblem(request);
|
|
74
|
+
await this.authorize({ action: 'estimate', request });
|
|
75
|
+
await this.initialize();
|
|
76
|
+
const ledger = parseLedger(await readPersonalOsSidecar(this.dataDir, this.sidecarPath));
|
|
77
|
+
const capacity = await this.readCapacity(request);
|
|
78
|
+
const committedAmount = committedAmountFor(ledger, request);
|
|
79
|
+
return {
|
|
80
|
+
tenantId: request.tenantId,
|
|
81
|
+
scopeId: request.scopeId,
|
|
82
|
+
resourceId: request.resourceId,
|
|
83
|
+
period: request.period,
|
|
84
|
+
unit: request.unit,
|
|
85
|
+
requestedAmount: request.amount,
|
|
86
|
+
capacity,
|
|
87
|
+
committedAmount,
|
|
88
|
+
availableAmount: Math.max(0, capacity - committedAmount),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async approve(input) {
|
|
92
|
+
const request = normalizeRequest(input);
|
|
93
|
+
return this.mutate('approve', request.operationId, request, async (ledger) => {
|
|
94
|
+
await this.verifyProblem(request);
|
|
95
|
+
const decision = await this.authorize({ action: 'approve', request });
|
|
96
|
+
const approval = approvalFromDecision(decision, this.now());
|
|
97
|
+
const now = this.now().toISOString();
|
|
98
|
+
const reservation = {
|
|
99
|
+
reservationId: `reservation-${randomUUID()}`,
|
|
100
|
+
state: 'approved',
|
|
101
|
+
...reservationFields(request),
|
|
102
|
+
approval,
|
|
103
|
+
externalAction: { status: 'not_started' },
|
|
104
|
+
createdAt: now,
|
|
105
|
+
updatedAt: now,
|
|
106
|
+
history: [{
|
|
107
|
+
action: 'approve',
|
|
108
|
+
operationId: request.operationId,
|
|
109
|
+
principal: request.principal,
|
|
110
|
+
occurredAt: now,
|
|
111
|
+
toState: 'approved',
|
|
112
|
+
amount: request.amount,
|
|
113
|
+
externalActionStatus: 'not_started',
|
|
114
|
+
}],
|
|
115
|
+
};
|
|
116
|
+
ledger.reservations.push(reservation);
|
|
117
|
+
return { reservation: clone(reservation) };
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async reserve(input) {
|
|
121
|
+
const request = normalizeRequest(input);
|
|
122
|
+
const approvalId = input.approvalId;
|
|
123
|
+
if (approvalId !== undefined)
|
|
124
|
+
assertText(approvalId, 'approvalId');
|
|
125
|
+
if (approvalId === undefined) {
|
|
126
|
+
throw new ResourceReservationError('approval_required', 'reserve requires a prior approvalId');
|
|
127
|
+
}
|
|
128
|
+
const payload = { ...request, approvalId };
|
|
129
|
+
return this.mutate('reserve', request.operationId, payload, async (ledger) => {
|
|
130
|
+
await this.verifyProblem(request);
|
|
131
|
+
const reservation = findReservationByApprovalId(ledger, approvalId, request);
|
|
132
|
+
assertRequestMatchesReservation(request, reservation);
|
|
133
|
+
assertApprovalUsable(reservation, this.now());
|
|
134
|
+
if (reservation.state !== 'approved') {
|
|
135
|
+
throw new ResourceReservationError('reservation_state_conflict', `Reservation ${reservation.reservationId} is ${reservation.state}`);
|
|
136
|
+
}
|
|
137
|
+
const approval = reservation.approval;
|
|
138
|
+
await this.authorize({ action: 'reserve', request, approvalId: approval.approvalId, reservation });
|
|
139
|
+
const capacity = await this.readCapacity(request);
|
|
140
|
+
const committedAmount = committedAmountFor(ledger, request, reservation.reservationId);
|
|
141
|
+
if (committedAmount + request.amount > capacity) {
|
|
142
|
+
throw new ResourceReservationError('capacity_exceeded', `Capacity ${capacity} is insufficient for ${request.amount}`);
|
|
143
|
+
}
|
|
144
|
+
const now = this.now().toISOString();
|
|
145
|
+
const previousState = reservation.state;
|
|
146
|
+
reservation.state = 'reserved';
|
|
147
|
+
reservation.updatedAt = now;
|
|
148
|
+
reservation.history.push({
|
|
149
|
+
action: 'reserve',
|
|
150
|
+
operationId: request.operationId,
|
|
151
|
+
principal: request.principal,
|
|
152
|
+
occurredAt: now,
|
|
153
|
+
fromState: previousState,
|
|
154
|
+
toState: 'reserved',
|
|
155
|
+
amount: reservation.amount,
|
|
156
|
+
externalActionStatus: reservation.externalAction.status,
|
|
157
|
+
});
|
|
158
|
+
return { reservation: clone(reservation) };
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async release(input) {
|
|
162
|
+
return this.transition('release', input, 'released');
|
|
163
|
+
}
|
|
164
|
+
async cancel(input) {
|
|
165
|
+
return this.transition('cancel', input, 'cancelled');
|
|
166
|
+
}
|
|
167
|
+
async consume(input) {
|
|
168
|
+
return this.transition('consume', input, 'consumed');
|
|
169
|
+
}
|
|
170
|
+
async startExternalAction(input) {
|
|
171
|
+
const normalized = normalizeMutationInput(input);
|
|
172
|
+
return this.mutate('start_external', normalized.operationId, normalized, async (ledger) => {
|
|
173
|
+
const reservation = findReservationById(ledger, normalized.reservationId, normalized);
|
|
174
|
+
await this.authorize({ action: 'start_external', input: normalized, reservation });
|
|
175
|
+
if (reservation.state !== 'reserved') {
|
|
176
|
+
throw new ResourceReservationError('reservation_state_conflict', `Reservation ${reservation.reservationId} is ${reservation.state}`);
|
|
177
|
+
}
|
|
178
|
+
if (reservation.externalAction.status !== 'not_started') {
|
|
179
|
+
throw new ResourceReservationError('reservation_state_conflict', `External action is already ${reservation.externalAction.status}`);
|
|
180
|
+
}
|
|
181
|
+
const now = this.now().toISOString();
|
|
182
|
+
reservation.externalAction = { status: 'started', startedAt: now };
|
|
183
|
+
reservation.updatedAt = now;
|
|
184
|
+
reservation.history.push({
|
|
185
|
+
action: 'start_external',
|
|
186
|
+
operationId: normalized.operationId,
|
|
187
|
+
principal: normalized.principal,
|
|
188
|
+
occurredAt: now,
|
|
189
|
+
fromState: reservation.state,
|
|
190
|
+
toState: reservation.state,
|
|
191
|
+
amount: reservation.amount,
|
|
192
|
+
externalActionStatus: 'started',
|
|
193
|
+
});
|
|
194
|
+
return { reservation: clone(reservation) };
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async readLedger(input) {
|
|
198
|
+
const normalized = normalizeScopeInput(input);
|
|
199
|
+
await this.authorize({ action: 'read', input: normalized });
|
|
200
|
+
await this.initialize();
|
|
201
|
+
const ledger = parseLedger(await readPersonalOsSidecar(this.dataDir, this.sidecarPath));
|
|
202
|
+
return {
|
|
203
|
+
version: ledgerVersion,
|
|
204
|
+
reservations: ledger.reservations
|
|
205
|
+
.filter((reservation) => reservation.tenantId === normalized.tenantId
|
|
206
|
+
&& reservation.scopeId === normalized.scopeId
|
|
207
|
+
&& reservation.principal === normalized.principal)
|
|
208
|
+
.map((reservation) => clone(reservation)),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async transition(action, input, targetState) {
|
|
212
|
+
const normalized = normalizeMutationInput(input);
|
|
213
|
+
return this.mutate(action, normalized.operationId, normalized, async (ledger) => {
|
|
214
|
+
const reservation = findReservationById(ledger, normalized.reservationId, normalized);
|
|
215
|
+
await this.authorize({ action, input: normalized, reservation });
|
|
216
|
+
if (reservation.externalAction.status === 'started' || reservation.externalAction.status === 'unknown') {
|
|
217
|
+
if (action === 'release' || action === 'cancel') {
|
|
218
|
+
throw new ResourceReservationError('external_action_reconciliation_required', `Reservation ${reservation.reservationId} has an external action in ${reservation.externalAction.status} state`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const allowedStates = action === 'consume' ? ['reserved'] : ['approved', 'reserved'];
|
|
222
|
+
if (!allowedStates.includes(reservation.state)) {
|
|
223
|
+
throw new ResourceReservationError('reservation_state_conflict', `Reservation ${reservation.reservationId} is ${reservation.state}`);
|
|
224
|
+
}
|
|
225
|
+
const now = this.now().toISOString();
|
|
226
|
+
const previousState = reservation.state;
|
|
227
|
+
reservation.state = targetState;
|
|
228
|
+
reservation.updatedAt = now;
|
|
229
|
+
if (action === 'consume')
|
|
230
|
+
reservation.externalAction = { status: 'completed', startedAt: reservation.externalAction.startedAt };
|
|
231
|
+
reservation.history.push({
|
|
232
|
+
action,
|
|
233
|
+
operationId: normalized.operationId,
|
|
234
|
+
principal: normalized.principal,
|
|
235
|
+
occurredAt: now,
|
|
236
|
+
fromState: previousState,
|
|
237
|
+
toState: targetState,
|
|
238
|
+
amount: reservation.amount,
|
|
239
|
+
externalActionStatus: reservation.externalAction.status,
|
|
240
|
+
});
|
|
241
|
+
return { reservation: clone(reservation) };
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async mutate(action, operationId, payload, handler) {
|
|
245
|
+
await this.initialize();
|
|
246
|
+
const payloadDigest = digest({ action, payload });
|
|
247
|
+
return mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, async (current, sidecarContent) => {
|
|
248
|
+
const ledger = parseLedger(sidecarContent);
|
|
249
|
+
const tenantId = extractTenantId(payload);
|
|
250
|
+
const existing = ledger.operations.find((operation) => (operation.tenantId === tenantId && operation.operationId === operationId));
|
|
251
|
+
if (existing) {
|
|
252
|
+
// Keep the scope boundary before comparing the payload so a caller in
|
|
253
|
+
// another principal or scope cannot learn whether an operation exists.
|
|
254
|
+
assertReplayScope(payload, existing.result.reservation);
|
|
255
|
+
if (existing.action !== action || existing.payloadDigest !== payloadDigest) {
|
|
256
|
+
throw new ResourceReservationError('operation_conflict', `Operation ${operationId} was already used with a different payload`);
|
|
257
|
+
}
|
|
258
|
+
// Replaying an idempotent operation still reads its stored result. The
|
|
259
|
+
// caller must pass the current authorization check before that result
|
|
260
|
+
// is returned, even though no new write is performed.
|
|
261
|
+
await this.reauthorizeReplay(action, existing);
|
|
262
|
+
return { next: current, sidecarContent: serializeLedger(ledger), result: clone(existing.result) };
|
|
263
|
+
}
|
|
264
|
+
const result = await handler(ledger);
|
|
265
|
+
ledger.operations.push({
|
|
266
|
+
operationId,
|
|
267
|
+
tenantId,
|
|
268
|
+
action,
|
|
269
|
+
payloadDigest,
|
|
270
|
+
result: clone(result),
|
|
271
|
+
recordedAt: this.now().toISOString(),
|
|
272
|
+
});
|
|
273
|
+
return { next: current, sidecarContent: serializeLedger(ledger), result };
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async reauthorizeReplay(action, existing) {
|
|
277
|
+
const storedReservation = existing.result.reservation;
|
|
278
|
+
if (action === 'approve') {
|
|
279
|
+
const request = requestFromReservation(storedReservation, existing.operationId);
|
|
280
|
+
await this.authorize({ action, request, reservation: storedReservation });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (action === 'reserve') {
|
|
284
|
+
const request = requestFromReservation(storedReservation, existing.operationId);
|
|
285
|
+
await this.authorize({
|
|
286
|
+
action,
|
|
287
|
+
request,
|
|
288
|
+
approvalId: storedReservation.approval.approvalId,
|
|
289
|
+
reservation: storedReservation,
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const input = {
|
|
294
|
+
operationId: existing.operationId,
|
|
295
|
+
tenantId: storedReservation.tenantId,
|
|
296
|
+
principal: storedReservation.principal,
|
|
297
|
+
scopeId: storedReservation.scopeId,
|
|
298
|
+
reservationId: storedReservation.reservationId,
|
|
299
|
+
};
|
|
300
|
+
await this.authorize({ action, input, reservation: storedReservation });
|
|
301
|
+
}
|
|
302
|
+
async authorize(input) {
|
|
303
|
+
const request = input.request;
|
|
304
|
+
const scopeInput = input.input;
|
|
305
|
+
const mutationInput = scopeInput !== undefined && 'reservationId' in scopeInput ? scopeInput : undefined;
|
|
306
|
+
const decision = await this.authorization.authorize({
|
|
307
|
+
action: input.action,
|
|
308
|
+
operationId: request?.operationId ?? mutationInput?.operationId ?? `read-${randomUUID()}`,
|
|
309
|
+
tenantId: request?.tenantId ?? scopeInput?.tenantId ?? input.reservation?.tenantId ?? '',
|
|
310
|
+
principal: request?.principal ?? scopeInput?.principal ?? input.reservation?.principal ?? '',
|
|
311
|
+
scopeId: request?.scopeId ?? scopeInput?.scopeId ?? input.reservation?.scopeId ?? '',
|
|
312
|
+
resourceId: request?.resourceId ?? input.reservation?.resourceId,
|
|
313
|
+
runId: request?.runId ?? input.reservation?.runId,
|
|
314
|
+
problem: request?.problem ?? input.reservation?.problem,
|
|
315
|
+
approvalId: input.approvalId,
|
|
316
|
+
reservationId: mutationInput?.reservationId ?? input.reservation?.reservationId,
|
|
317
|
+
reservation: input.reservation,
|
|
318
|
+
});
|
|
319
|
+
if (decision.status === 'expired')
|
|
320
|
+
throw new ResourceReservationError('approval_expired', 'Authorization approval is expired');
|
|
321
|
+
if (decision.status === 'revoked')
|
|
322
|
+
throw new ResourceReservationError('approval_revoked', 'Authorization approval is revoked');
|
|
323
|
+
if (decision.status !== 'approved')
|
|
324
|
+
throw new ResourceReservationError('authorization_denied', 'Authorization denied the resource operation');
|
|
325
|
+
if (decision.expiresAt !== undefined && decision.expiresAt !== null) {
|
|
326
|
+
assertTimestamp(decision.expiresAt, 'authorization.expiresAt');
|
|
327
|
+
if (Date.parse(decision.expiresAt) <= this.now().getTime()) {
|
|
328
|
+
throw new ResourceReservationError('approval_expired', 'Authorization approval is expired');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return decision;
|
|
332
|
+
}
|
|
333
|
+
async verifyProblem(request) {
|
|
334
|
+
try {
|
|
335
|
+
const verified = await this.problemSnapshot.verify({
|
|
336
|
+
tenantId: request.tenantId,
|
|
337
|
+
principal: request.principal,
|
|
338
|
+
scopeId: request.scopeId,
|
|
339
|
+
reference: request.problem,
|
|
340
|
+
});
|
|
341
|
+
if (verified === false)
|
|
342
|
+
throw new Error('snapshot verifier rejected reference');
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
throw new ResourceReservationError('problem_snapshot_invalid', 'Problem snapshot reference could not be verified');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async readCapacity(request) {
|
|
349
|
+
const capacity = await this.capacity.read({
|
|
350
|
+
tenantId: request.tenantId,
|
|
351
|
+
principal: request.principal,
|
|
352
|
+
scopeId: request.scopeId,
|
|
353
|
+
resourceId: request.resourceId,
|
|
354
|
+
period: request.period,
|
|
355
|
+
unit: request.unit,
|
|
356
|
+
});
|
|
357
|
+
if (capacity === null || !Number.isFinite(capacity) || capacity < 0) {
|
|
358
|
+
throw new ResourceReservationError('capacity_unknown', 'Current resource capacity is unknown');
|
|
359
|
+
}
|
|
360
|
+
return capacity;
|
|
361
|
+
}
|
|
362
|
+
now() {
|
|
363
|
+
const now = this.clock();
|
|
364
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
365
|
+
throw new ResourceReservationError('validation_error', 'Clock returned an invalid date');
|
|
366
|
+
return new Date(now);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function normalizeRequest(input) {
|
|
370
|
+
assertText(input.operationId, 'operationId');
|
|
371
|
+
assertText(input.tenantId, 'tenantId');
|
|
372
|
+
assertText(input.principal, 'principal');
|
|
373
|
+
assertText(input.scopeId, 'scopeId');
|
|
374
|
+
assertText(input.resourceId, 'resourceId');
|
|
375
|
+
assertText(input.unit, 'unit');
|
|
376
|
+
assertText(input.runId, 'runId');
|
|
377
|
+
if (!Number.isFinite(input.amount) || input.amount <= 0)
|
|
378
|
+
throw new ResourceReservationError('validation_error', 'amount must be a positive finite number');
|
|
379
|
+
const period = normalizePeriod(input.period);
|
|
380
|
+
const problem = normalizeProblemReference(input.problem);
|
|
381
|
+
return {
|
|
382
|
+
operationId: input.operationId,
|
|
383
|
+
tenantId: input.tenantId,
|
|
384
|
+
principal: input.principal,
|
|
385
|
+
scopeId: input.scopeId,
|
|
386
|
+
resourceId: input.resourceId,
|
|
387
|
+
period,
|
|
388
|
+
amount: input.amount,
|
|
389
|
+
unit: input.unit,
|
|
390
|
+
runId: input.runId,
|
|
391
|
+
problem,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function normalizeMutationInput(input) {
|
|
395
|
+
assertText(input.operationId, 'operationId');
|
|
396
|
+
assertText(input.tenantId, 'tenantId');
|
|
397
|
+
assertText(input.principal, 'principal');
|
|
398
|
+
assertText(input.scopeId, 'scopeId');
|
|
399
|
+
assertText(input.reservationId, 'reservationId');
|
|
400
|
+
return { ...input };
|
|
401
|
+
}
|
|
402
|
+
function normalizeScopeInput(input) {
|
|
403
|
+
assertText(input.tenantId, 'tenantId');
|
|
404
|
+
assertText(input.principal, 'principal');
|
|
405
|
+
assertText(input.scopeId, 'scopeId');
|
|
406
|
+
return { ...input };
|
|
407
|
+
}
|
|
408
|
+
function normalizePeriod(period) {
|
|
409
|
+
assertText(period?.startsAt, 'period.startsAt');
|
|
410
|
+
assertText(period?.endsAt, 'period.endsAt');
|
|
411
|
+
assertTimestamp(period.startsAt, 'period.startsAt');
|
|
412
|
+
assertTimestamp(period.endsAt, 'period.endsAt');
|
|
413
|
+
if (Date.parse(period.endsAt) <= Date.parse(period.startsAt)) {
|
|
414
|
+
throw new ResourceReservationError('validation_error', 'period.endsAt must be after period.startsAt');
|
|
415
|
+
}
|
|
416
|
+
return { startsAt: period.startsAt, endsAt: period.endsAt };
|
|
417
|
+
}
|
|
418
|
+
function normalizeProblemReference(reference) {
|
|
419
|
+
assertText(reference?.problem_snapshot_id, 'problem.problem_snapshot_id');
|
|
420
|
+
assertText(reference?.problem_id, 'problem.problem_id');
|
|
421
|
+
assertText(reference?.revision, 'problem.revision');
|
|
422
|
+
if (!snapshotIdPattern.test(reference.problem_snapshot_id)) {
|
|
423
|
+
throw new ResourceReservationError('validation_error', 'problem.problem_snapshot_id must be a sha256 snapshot ID');
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
problem_snapshot_id: reference.problem_snapshot_id,
|
|
427
|
+
problem_id: reference.problem_id,
|
|
428
|
+
revision: reference.revision,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function reservationFields(request) {
|
|
432
|
+
const { operationId: _operationId, ...fields } = request;
|
|
433
|
+
return fields;
|
|
434
|
+
}
|
|
435
|
+
function requestFromReservation(reservation, operationId) {
|
|
436
|
+
return {
|
|
437
|
+
operationId,
|
|
438
|
+
tenantId: reservation.tenantId,
|
|
439
|
+
principal: reservation.principal,
|
|
440
|
+
scopeId: reservation.scopeId,
|
|
441
|
+
resourceId: reservation.resourceId,
|
|
442
|
+
period: { ...reservation.period },
|
|
443
|
+
amount: reservation.amount,
|
|
444
|
+
unit: reservation.unit,
|
|
445
|
+
runId: reservation.runId,
|
|
446
|
+
problem: { ...reservation.problem },
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function assertText(value, label) {
|
|
450
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 512 || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
451
|
+
throw new ResourceReservationError('validation_error', `${label} must be a bounded text value`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function assertTimestamp(value, label) {
|
|
455
|
+
if (!Number.isFinite(Date.parse(value)))
|
|
456
|
+
throw new ResourceReservationError('validation_error', `${label} must be an ISO timestamp`);
|
|
457
|
+
}
|
|
458
|
+
function assertSidecarPath(path) {
|
|
459
|
+
if (!path || path.startsWith('/') || path.includes('\\') || path.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
460
|
+
throw new ResourceReservationError('validation_error', 'sidecarPath must remain relative to the SSOT root');
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function approvalFromDecision(decision, now) {
|
|
464
|
+
const approvalId = decision.approvalId ?? `approval-${randomUUID()}`;
|
|
465
|
+
assertText(approvalId, 'authorization.approvalId');
|
|
466
|
+
const expiresAt = decision.expiresAt ?? undefined;
|
|
467
|
+
if (expiresAt !== undefined)
|
|
468
|
+
assertTimestamp(expiresAt, 'authorization.expiresAt');
|
|
469
|
+
const evidence = decision.evidence ?? {};
|
|
470
|
+
for (const [key, value] of Object.entries(evidence)) {
|
|
471
|
+
assertText(key, 'authorization.evidence key');
|
|
472
|
+
assertText(value, 'authorization.evidence value');
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
approvalId,
|
|
476
|
+
approvedAt: now.toISOString(),
|
|
477
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
478
|
+
evidence: { ...evidence },
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function assertApprovalUsable(reservation, now = new Date()) {
|
|
482
|
+
if (reservation.approval.expiresAt !== undefined && Date.parse(reservation.approval.expiresAt) <= now.getTime()) {
|
|
483
|
+
throw new ResourceReservationError('approval_expired', `Approval ${reservation.approval.approvalId} is expired`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function assertRequestMatchesReservation(request, reservation) {
|
|
487
|
+
if (request.tenantId !== reservation.tenantId || request.scopeId !== reservation.scopeId) {
|
|
488
|
+
throw new ResourceReservationError('scope_mismatch', `Reservation ${reservation.reservationId} is outside the requested scope`);
|
|
489
|
+
}
|
|
490
|
+
const same = request.resourceId === reservation.resourceId
|
|
491
|
+
&& request.unit === reservation.unit
|
|
492
|
+
&& request.runId === reservation.runId
|
|
493
|
+
&& request.amount === reservation.amount
|
|
494
|
+
&& request.period.startsAt === reservation.period.startsAt
|
|
495
|
+
&& request.period.endsAt === reservation.period.endsAt
|
|
496
|
+
&& digest(request.problem) === digest(reservation.problem);
|
|
497
|
+
if (!same)
|
|
498
|
+
throw new ResourceReservationError('approval_mismatch', `Reservation ${reservation.reservationId} does not match the requested payload`);
|
|
499
|
+
}
|
|
500
|
+
function assertReplayScope(payload, reservation) {
|
|
501
|
+
if (!isRecord(payload)
|
|
502
|
+
|| payload.tenantId !== reservation.tenantId
|
|
503
|
+
|| payload.principal !== reservation.principal
|
|
504
|
+
|| payload.scopeId !== reservation.scopeId) {
|
|
505
|
+
throw new ResourceReservationError('scope_mismatch', `Stored operation ${reservation.reservationId} is outside the requested scope`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function findReservationById(ledger, reservationId, scope) {
|
|
509
|
+
const reservation = ledger.reservations.find((item) => item.reservationId === reservationId);
|
|
510
|
+
if (!reservation)
|
|
511
|
+
throw new ResourceReservationError('reservation_not_found', `Reservation ${reservationId} was not found`);
|
|
512
|
+
if (reservation.tenantId !== scope.tenantId || reservation.principal !== scope.principal || reservation.scopeId !== scope.scopeId) {
|
|
513
|
+
throw new ResourceReservationError('scope_mismatch', `Reservation ${reservationId} is outside the requested scope`);
|
|
514
|
+
}
|
|
515
|
+
return reservation;
|
|
516
|
+
}
|
|
517
|
+
function findReservationByApprovalId(ledger, approvalId, scope) {
|
|
518
|
+
const reservation = ledger.reservations.find((item) => item.approval.approvalId === approvalId);
|
|
519
|
+
if (!reservation)
|
|
520
|
+
throw new ResourceReservationError('approval_required', `Approval ${approvalId} was not found`);
|
|
521
|
+
if (reservation.tenantId !== scope.tenantId || reservation.principal !== scope.principal || reservation.scopeId !== scope.scopeId) {
|
|
522
|
+
throw new ResourceReservationError('scope_mismatch', `Approval ${approvalId} is outside the requested scope`);
|
|
523
|
+
}
|
|
524
|
+
return reservation;
|
|
525
|
+
}
|
|
526
|
+
function committedAmountFor(ledger, request, excludingReservationId) {
|
|
527
|
+
return ledger.reservations
|
|
528
|
+
.filter((reservation) => reservation.reservationId !== excludingReservationId)
|
|
529
|
+
.filter((reservation) => reservation.state === 'reserved')
|
|
530
|
+
.filter((reservation) => reservation.tenantId === request.tenantId && reservation.scopeId === request.scopeId)
|
|
531
|
+
.filter((reservation) => reservation.resourceId === request.resourceId && reservation.unit === request.unit)
|
|
532
|
+
.filter((reservation) => overlaps(reservation.period, request.period))
|
|
533
|
+
.reduce((sum, reservation) => sum + reservation.amount, 0);
|
|
534
|
+
}
|
|
535
|
+
function overlaps(left, right) {
|
|
536
|
+
return Date.parse(left.startsAt) < Date.parse(right.endsAt) && Date.parse(right.startsAt) < Date.parse(left.endsAt);
|
|
537
|
+
}
|
|
538
|
+
function parseLedger(content) {
|
|
539
|
+
if (!content || content.trim() === '')
|
|
540
|
+
return { version: ledgerVersion, reservations: [], operations: [] };
|
|
541
|
+
let value;
|
|
542
|
+
try {
|
|
543
|
+
value = JSON.parse(content);
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
throw new ResourceReservationError('ledger_invalid', 'Resource reservation ledger is not valid JSON');
|
|
547
|
+
}
|
|
548
|
+
if (!isRecord(value) || value.version !== ledgerVersion || !Array.isArray(value.reservations) || !Array.isArray(value.operations)) {
|
|
549
|
+
throw new ResourceReservationError('ledger_invalid', 'Resource reservation ledger has an unsupported shape');
|
|
550
|
+
}
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
function serializeLedger(ledger) {
|
|
554
|
+
return `${JSON.stringify(ledger, null, 2)}\n`;
|
|
555
|
+
}
|
|
556
|
+
function extractTenantId(payload) {
|
|
557
|
+
if (!isRecord(payload) || typeof payload.tenantId !== 'string') {
|
|
558
|
+
throw new ResourceReservationError('validation_error', 'operation payload must include tenantId');
|
|
559
|
+
}
|
|
560
|
+
return payload.tenantId;
|
|
561
|
+
}
|
|
562
|
+
function digest(value) {
|
|
563
|
+
return createHash('sha256').update(JSON.stringify(sortJson(value))).digest('hex');
|
|
564
|
+
}
|
|
565
|
+
function sortJson(value) {
|
|
566
|
+
if (Array.isArray(value))
|
|
567
|
+
return value.map((item) => sortJson(item));
|
|
568
|
+
if (!isRecord(value))
|
|
569
|
+
return value;
|
|
570
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, sortJson(item)]));
|
|
571
|
+
}
|
|
572
|
+
function isRecord(value) {
|
|
573
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
574
|
+
}
|
|
575
|
+
function clone(value) {
|
|
576
|
+
return structuredClone(value);
|
|
577
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -117,6 +117,103 @@ export declare const toolDefinitions: readonly [{
|
|
|
117
117
|
};
|
|
118
118
|
};
|
|
119
119
|
};
|
|
120
|
+
}, {
|
|
121
|
+
readonly name: "personal_knowledge_context";
|
|
122
|
+
readonly description: "Return the authenticated v1 personal knowledge context. Identity and storage selection come from trusted configuration, not tool arguments.";
|
|
123
|
+
readonly inputSchema: {
|
|
124
|
+
readonly type: "object";
|
|
125
|
+
readonly additionalProperties: false;
|
|
126
|
+
readonly properties: {};
|
|
127
|
+
};
|
|
128
|
+
}, {
|
|
129
|
+
readonly name: "personal_knowledge_register";
|
|
130
|
+
readonly description: "Register one v1 personal knowledge event in the explicitly configured local or managed context. Caller identity, organization, source, and path are never accepted as event fields.";
|
|
131
|
+
readonly inputSchema: {
|
|
132
|
+
readonly type: "object";
|
|
133
|
+
readonly required: readonly ["event"];
|
|
134
|
+
readonly additionalProperties: false;
|
|
135
|
+
readonly properties: {
|
|
136
|
+
readonly event: {
|
|
137
|
+
readonly type: "object";
|
|
138
|
+
readonly required: readonly ["event_id"];
|
|
139
|
+
readonly additionalProperties: false;
|
|
140
|
+
readonly properties: {
|
|
141
|
+
readonly event_id: {
|
|
142
|
+
readonly type: "string";
|
|
143
|
+
readonly minLength: 1;
|
|
144
|
+
};
|
|
145
|
+
readonly body: {
|
|
146
|
+
readonly type: "string";
|
|
147
|
+
};
|
|
148
|
+
readonly content: {
|
|
149
|
+
readonly type: "string";
|
|
150
|
+
};
|
|
151
|
+
readonly body_hash: {
|
|
152
|
+
readonly type: "string";
|
|
153
|
+
readonly minLength: 1;
|
|
154
|
+
};
|
|
155
|
+
readonly occurred_at: {
|
|
156
|
+
readonly type: "string";
|
|
157
|
+
readonly minLength: 1;
|
|
158
|
+
};
|
|
159
|
+
readonly captured_at: {
|
|
160
|
+
readonly type: "string";
|
|
161
|
+
readonly minLength: 1;
|
|
162
|
+
};
|
|
163
|
+
readonly source: {};
|
|
164
|
+
readonly source_pointer: {};
|
|
165
|
+
readonly parent_episode_id: {
|
|
166
|
+
readonly type: readonly ["string", "null"];
|
|
167
|
+
};
|
|
168
|
+
readonly permission_snapshot: {
|
|
169
|
+
readonly type: "object";
|
|
170
|
+
};
|
|
171
|
+
readonly sensitivity: {
|
|
172
|
+
readonly type: "string";
|
|
173
|
+
readonly minLength: 1;
|
|
174
|
+
};
|
|
175
|
+
readonly kind: {
|
|
176
|
+
readonly type: "string";
|
|
177
|
+
readonly minLength: 1;
|
|
178
|
+
};
|
|
179
|
+
readonly type: {
|
|
180
|
+
readonly type: "string";
|
|
181
|
+
readonly minLength: 1;
|
|
182
|
+
};
|
|
183
|
+
readonly tags: {
|
|
184
|
+
readonly type: "array";
|
|
185
|
+
readonly items: {
|
|
186
|
+
readonly type: "string";
|
|
187
|
+
readonly minLength: 1;
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
readonly metadata: {
|
|
191
|
+
readonly type: "object";
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
}, {
|
|
198
|
+
readonly name: "personal_knowledge_search";
|
|
199
|
+
readonly description: "Search the explicitly configured v1 personal knowledge context. The result includes the verified context and never falls back to another store.";
|
|
200
|
+
readonly inputSchema: {
|
|
201
|
+
readonly type: "object";
|
|
202
|
+
readonly required: readonly ["query"];
|
|
203
|
+
readonly additionalProperties: false;
|
|
204
|
+
readonly properties: {
|
|
205
|
+
readonly query: {
|
|
206
|
+
readonly type: "string";
|
|
207
|
+
readonly minLength: 1;
|
|
208
|
+
readonly maxLength: 4000;
|
|
209
|
+
};
|
|
210
|
+
readonly limit: {
|
|
211
|
+
readonly type: "integer";
|
|
212
|
+
readonly minimum: 1;
|
|
213
|
+
readonly maximum: 50;
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
};
|
|
120
217
|
}, {
|
|
121
218
|
readonly name: "onboarding_status";
|
|
122
219
|
readonly description: "Report seeded areas, first value demo readiness, missing setup, and local connection status.";
|