@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,28 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { ExecutionAuthorityService, type ExecutionProblemReference } from './execution-authority.js';
|
|
3
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
/** Context resolved by the host after authentication and tenant selection. */
|
|
5
|
+
export interface TrustedExecutionAuthorityRequestContext {
|
|
6
|
+
tenantId: string;
|
|
7
|
+
principal: string;
|
|
8
|
+
scopeId: string;
|
|
9
|
+
/** A non-empty value means that the host already verified the mutation origin. */
|
|
10
|
+
verifiedMutationOrigin?: string;
|
|
11
|
+
}
|
|
12
|
+
/** The factory is the boundary where a host binds a service to its trusted context. */
|
|
13
|
+
export type ExecutionAuthorityServiceFactory = (context: TrustedExecutionAuthorityRequestContext) => MaybePromise<Pick<ExecutionAuthorityService, 'start' | 'readIntent'>>;
|
|
14
|
+
export interface ExecutionAuthorityMutationVerificationInput {
|
|
15
|
+
request: IncomingMessage;
|
|
16
|
+
context: TrustedExecutionAuthorityRequestContext;
|
|
17
|
+
}
|
|
18
|
+
export interface ExecutionAuthorityHttpOptions {
|
|
19
|
+
serviceFactory: ExecutionAuthorityServiceFactory;
|
|
20
|
+
basePath?: string;
|
|
21
|
+
bodyLimitBytes?: number;
|
|
22
|
+
/** Required for hosts that do not attach verifiedMutationOrigin to context. */
|
|
23
|
+
verifyMutationRequest?: (input: ExecutionAuthorityMutationVerificationInput) => MaybePromise<boolean>;
|
|
24
|
+
}
|
|
25
|
+
/** A composable route handler. `false` means that the request is outside this route. */
|
|
26
|
+
export type ExecutionAuthorityHttpHandler = (request: IncomingMessage, response: ServerResponse, context: TrustedExecutionAuthorityRequestContext | null) => Promise<boolean>;
|
|
27
|
+
export declare function createExecutionAuthorityHttpHandler(options: ExecutionAuthorityHttpOptions): ExecutionAuthorityHttpHandler;
|
|
28
|
+
export type { ExecutionProblemReference };
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ExecutionAuthorityError, } from './execution-authority.js';
|
|
3
|
+
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
4
|
+
const DEFAULT_BASE_PATH = '/execution-authority';
|
|
5
|
+
const snapshotIdPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
6
|
+
const boundedText = z.string().min(1).max(512).refine((value) => !/[\u0000-\u001f\u007f]/u.test(value), 'must be a bounded text value');
|
|
7
|
+
const problemSchema = z.object({
|
|
8
|
+
problem_snapshot_id: z.string().min(1).max(512).regex(snapshotIdPattern),
|
|
9
|
+
problem_id: boundedText,
|
|
10
|
+
revision: boundedText,
|
|
11
|
+
}).strict();
|
|
12
|
+
const startRequestSchema = z.object({
|
|
13
|
+
operationId: boundedText,
|
|
14
|
+
tenantId: boundedText,
|
|
15
|
+
principal: boundedText,
|
|
16
|
+
scopeId: boundedText,
|
|
17
|
+
runId: boundedText,
|
|
18
|
+
reservationId: boundedText,
|
|
19
|
+
approvalId: boundedText,
|
|
20
|
+
authorityRef: boundedText,
|
|
21
|
+
constraintRefs: z.array(boundedText).max(128),
|
|
22
|
+
problem: problemSchema,
|
|
23
|
+
}).strict();
|
|
24
|
+
class HttpInputError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'HttpInputError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
class BodyLimitError extends Error {
|
|
31
|
+
constructor() {
|
|
32
|
+
super('Request body exceeds the configured limit');
|
|
33
|
+
this.name = 'BodyLimitError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
class BodyReadError extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'BodyReadError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function requestPath(request) {
|
|
43
|
+
try {
|
|
44
|
+
return new URL(request.url ?? '/', 'http://localhost').pathname;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return '/';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function headerValue(request, name) {
|
|
51
|
+
const value = request.headers[name.toLowerCase()];
|
|
52
|
+
return Array.isArray(value) ? value[0] : value;
|
|
53
|
+
}
|
|
54
|
+
function writeJson(response, statusCode, body, headers = {}) {
|
|
55
|
+
if (response.headersSent)
|
|
56
|
+
return;
|
|
57
|
+
response.statusCode = statusCode;
|
|
58
|
+
response.setHeader('Content-Type', 'application/json');
|
|
59
|
+
for (const [name, value] of Object.entries(headers))
|
|
60
|
+
response.setHeader(name, value);
|
|
61
|
+
response.end(JSON.stringify(body));
|
|
62
|
+
}
|
|
63
|
+
async function readJsonBody(request, limitBytes) {
|
|
64
|
+
const contentLengthHeader = headerValue(request, 'content-length');
|
|
65
|
+
if (contentLengthHeader !== undefined) {
|
|
66
|
+
const contentLength = Number(contentLengthHeader);
|
|
67
|
+
if (Number.isFinite(contentLength) && contentLength > limitBytes) {
|
|
68
|
+
request.resume();
|
|
69
|
+
throw new BodyLimitError();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const chunks = [];
|
|
73
|
+
let totalBytes = 0;
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
let settled = false;
|
|
76
|
+
const cleanup = () => {
|
|
77
|
+
request.removeListener('data', onData);
|
|
78
|
+
request.removeListener('end', onEnd);
|
|
79
|
+
request.removeListener('error', onError);
|
|
80
|
+
request.removeListener('aborted', onAborted);
|
|
81
|
+
};
|
|
82
|
+
const finish = (error) => {
|
|
83
|
+
if (settled)
|
|
84
|
+
return;
|
|
85
|
+
settled = true;
|
|
86
|
+
cleanup();
|
|
87
|
+
if (error)
|
|
88
|
+
reject(error);
|
|
89
|
+
else
|
|
90
|
+
resolve();
|
|
91
|
+
};
|
|
92
|
+
const onData = (chunk) => {
|
|
93
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
94
|
+
totalBytes += buffer.byteLength;
|
|
95
|
+
if (totalBytes > limitBytes) {
|
|
96
|
+
request.resume();
|
|
97
|
+
finish(new BodyLimitError());
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
chunks.push(buffer);
|
|
101
|
+
};
|
|
102
|
+
const onEnd = () => finish();
|
|
103
|
+
const onError = () => finish(new BodyReadError('Request body could not be read'));
|
|
104
|
+
const onAborted = () => finish(new BodyReadError('Request was aborted'));
|
|
105
|
+
request.on('data', onData);
|
|
106
|
+
request.once('end', onEnd);
|
|
107
|
+
request.once('error', onError);
|
|
108
|
+
request.once('aborted', onAborted);
|
|
109
|
+
});
|
|
110
|
+
if (chunks.length === 0)
|
|
111
|
+
throw new HttpInputError('Request body must be a JSON object');
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new HttpInputError('Request body must contain valid JSON');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function normalizeBasePath(basePath) {
|
|
120
|
+
const value = basePath ?? DEFAULT_BASE_PATH;
|
|
121
|
+
if (typeof value !== 'string' || !value.startsWith('/') || value === '/' || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
122
|
+
throw new TypeError('basePath must be an absolute URL path');
|
|
123
|
+
}
|
|
124
|
+
const normalized = value.replace(/\/+$/u, '');
|
|
125
|
+
if (normalized.length === 0)
|
|
126
|
+
throw new TypeError('basePath must not be root');
|
|
127
|
+
return normalized;
|
|
128
|
+
}
|
|
129
|
+
function normalizePath(path) {
|
|
130
|
+
if (path.length > 1)
|
|
131
|
+
return path.replace(/\/+$/u, '');
|
|
132
|
+
return path;
|
|
133
|
+
}
|
|
134
|
+
function matchRoute(path, basePath) {
|
|
135
|
+
const normalized = normalizePath(path);
|
|
136
|
+
if (normalized === basePath)
|
|
137
|
+
return { operation: 'unknown' };
|
|
138
|
+
const prefix = `${basePath}/`;
|
|
139
|
+
if (!normalized.startsWith(prefix))
|
|
140
|
+
return null;
|
|
141
|
+
const suffix = normalized.slice(prefix.length);
|
|
142
|
+
if (suffix === 'start')
|
|
143
|
+
return { operation: 'start' };
|
|
144
|
+
const intentPrefix = 'intents/';
|
|
145
|
+
if (suffix.startsWith(intentPrefix)) {
|
|
146
|
+
const encodedOperationId = suffix.slice(intentPrefix.length);
|
|
147
|
+
if (encodedOperationId.length === 0 || encodedOperationId.includes('/'))
|
|
148
|
+
return { operation: 'unknown' };
|
|
149
|
+
try {
|
|
150
|
+
return { operation: 'read', operationId: decodeURIComponent(encodedOperationId) };
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return { operation: 'read', operationId: undefined };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { operation: 'unknown' };
|
|
157
|
+
}
|
|
158
|
+
function isRecord(value) {
|
|
159
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
160
|
+
}
|
|
161
|
+
function hasOwn(value, key) {
|
|
162
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
163
|
+
}
|
|
164
|
+
function assertContext(context) {
|
|
165
|
+
if (!context
|
|
166
|
+
|| !boundedText.safeParse(context.tenantId).success
|
|
167
|
+
|| !boundedText.safeParse(context.principal).success
|
|
168
|
+
|| !boundedText.safeParse(context.scopeId).success) {
|
|
169
|
+
throw new HttpInputError('Authenticated execution authority context is required');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function bindTrustedIdentity(body, context) {
|
|
173
|
+
if (!isRecord(body))
|
|
174
|
+
throw new HttpInputError('Request body must be a JSON object');
|
|
175
|
+
const input = { ...body };
|
|
176
|
+
if (hasOwn(input, 'authority') || hasOwn(input, 'authorities')) {
|
|
177
|
+
throw new HttpInputError('authority is not accepted in the request body');
|
|
178
|
+
}
|
|
179
|
+
const identityFields = [
|
|
180
|
+
['tenantId', ['tenantId', 'tenant_id', 'tenant'], context.tenantId],
|
|
181
|
+
['principal', ['principal', 'actorId', 'actor_id', 'actor'], context.principal],
|
|
182
|
+
['scopeId', ['scopeId', 'scope_id'], context.scopeId],
|
|
183
|
+
];
|
|
184
|
+
for (const [canonical, aliases, expected] of identityFields) {
|
|
185
|
+
for (const alias of aliases) {
|
|
186
|
+
if (!hasOwn(input, alias))
|
|
187
|
+
continue;
|
|
188
|
+
if (typeof input[alias] !== 'string' || input[alias] !== expected) {
|
|
189
|
+
throw new HttpInputError(`${canonical} does not match the trusted request context`);
|
|
190
|
+
}
|
|
191
|
+
delete input[alias];
|
|
192
|
+
}
|
|
193
|
+
input[canonical] = expected;
|
|
194
|
+
}
|
|
195
|
+
return input;
|
|
196
|
+
}
|
|
197
|
+
function parseStartBody(body, context) {
|
|
198
|
+
const bound = bindTrustedIdentity(body, context);
|
|
199
|
+
const parsed = startRequestSchema.safeParse(bound);
|
|
200
|
+
if (!parsed.success)
|
|
201
|
+
throw new HttpInputError(parsed.error.issues[0]?.message ?? 'Request body is invalid');
|
|
202
|
+
return parsed.data;
|
|
203
|
+
}
|
|
204
|
+
function serviceError(error) {
|
|
205
|
+
if (!(error instanceof ExecutionAuthorityError)) {
|
|
206
|
+
return { status: 500, code: 'internal_error', message: 'Execution authority operation failed' };
|
|
207
|
+
}
|
|
208
|
+
const status = (() => {
|
|
209
|
+
switch (error.code) {
|
|
210
|
+
case 'scope_mismatch':
|
|
211
|
+
case 'authority_revoked':
|
|
212
|
+
case 'authority_expired':
|
|
213
|
+
case 'approval_revoked':
|
|
214
|
+
case 'approval_expired':
|
|
215
|
+
case 'constraint_revoked':
|
|
216
|
+
case 'constraint_expired':
|
|
217
|
+
case 'reservation_revoked':
|
|
218
|
+
case 'reservation_expired':
|
|
219
|
+
case 'read_revoked':
|
|
220
|
+
case 'read_expired':
|
|
221
|
+
return 403;
|
|
222
|
+
case 'read_unknown':
|
|
223
|
+
case 'authority_unknown':
|
|
224
|
+
case 'approval_unknown':
|
|
225
|
+
case 'constraint_unknown':
|
|
226
|
+
case 'reservation_unknown':
|
|
227
|
+
return 503;
|
|
228
|
+
case 'operation_conflict':
|
|
229
|
+
case 'revision_conflict':
|
|
230
|
+
case 'fencing_unsupported':
|
|
231
|
+
case 'capability_expired':
|
|
232
|
+
case 'recovery_required':
|
|
233
|
+
case 'reservation_mark_unknown':
|
|
234
|
+
case 'effect_unknown':
|
|
235
|
+
return 409;
|
|
236
|
+
case 'validation_error':
|
|
237
|
+
return 400;
|
|
238
|
+
case 'ledger_invalid':
|
|
239
|
+
return 500;
|
|
240
|
+
default:
|
|
241
|
+
return 500;
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
return { status, code: error.code, message: error.message };
|
|
245
|
+
}
|
|
246
|
+
function writeError(response, status, code, message, headers = {}) {
|
|
247
|
+
writeJson(response, status, { error: { code, message } }, headers);
|
|
248
|
+
}
|
|
249
|
+
function methodAllowed(operation, method) {
|
|
250
|
+
return operation === 'read' ? method === 'GET' : operation === 'start' ? method === 'POST' : false;
|
|
251
|
+
}
|
|
252
|
+
async function verifyMutationRequest(options, request, context) {
|
|
253
|
+
if (typeof context.verifiedMutationOrigin === 'string' && context.verifiedMutationOrigin.trim().length > 0)
|
|
254
|
+
return true;
|
|
255
|
+
if (!options.verifyMutationRequest)
|
|
256
|
+
return false;
|
|
257
|
+
try {
|
|
258
|
+
return (await options.verifyMutationRequest({ request, context })) === true;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export function createExecutionAuthorityHttpHandler(options) {
|
|
265
|
+
if (!options || typeof options !== 'object' || typeof options.serviceFactory !== 'function') {
|
|
266
|
+
throw new TypeError('serviceFactory is required');
|
|
267
|
+
}
|
|
268
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
269
|
+
const bodyLimitBytes = options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES;
|
|
270
|
+
if (!Number.isInteger(bodyLimitBytes) || bodyLimitBytes <= 0) {
|
|
271
|
+
throw new TypeError('bodyLimitBytes must be a positive integer');
|
|
272
|
+
}
|
|
273
|
+
return async (request, response, context) => {
|
|
274
|
+
const route = matchRoute(requestPath(request), basePath);
|
|
275
|
+
if (!route)
|
|
276
|
+
return false;
|
|
277
|
+
if (!context) {
|
|
278
|
+
writeError(response, 401, 'unauthorized', 'Authenticated execution authority context is required');
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
assertContext(context);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
writeError(response, 401, 'unauthorized', error instanceof Error ? error.message : 'Authenticated execution authority context is required');
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (route.operation === 'unknown') {
|
|
289
|
+
writeError(response, 404, 'not_found', 'Execution authority route was not found');
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
const method = (request.method ?? 'GET').toUpperCase();
|
|
293
|
+
if (!methodAllowed(route.operation, method)) {
|
|
294
|
+
writeError(response, 405, 'method_not_allowed', 'Method is not allowed for this execution authority route', { Allow: route.operation === 'read' ? 'GET' : 'POST' });
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (route.operation === 'read') {
|
|
298
|
+
const parsedOperationId = boundedText.safeParse(route.operationId);
|
|
299
|
+
if (!parsedOperationId.success) {
|
|
300
|
+
writeError(response, 400, 'invalid_request', 'operationId is invalid');
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const service = await options.serviceFactory(context);
|
|
305
|
+
const result = await service.readIntent({
|
|
306
|
+
operationId: parsedOperationId.data,
|
|
307
|
+
tenantId: context.tenantId,
|
|
308
|
+
principal: context.principal,
|
|
309
|
+
scopeId: context.scopeId,
|
|
310
|
+
});
|
|
311
|
+
if (!result) {
|
|
312
|
+
writeError(response, 404, 'not_found', 'Execution intent was not found');
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
writeJson(response, 200, { action: 'read', operationId: parsedOperationId.data, result });
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
const failure = serviceError(error);
|
|
319
|
+
writeError(response, failure.status, failure.code, failure.message);
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
let body;
|
|
324
|
+
try {
|
|
325
|
+
body = await readJsonBody(request, bodyLimitBytes);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
if (error instanceof BodyLimitError) {
|
|
329
|
+
writeError(response, 413, 'body_too_large', error.message);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
writeError(response, 400, 'invalid_request', error instanceof Error ? error.message : 'Request body is invalid');
|
|
333
|
+
}
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
let parsed;
|
|
337
|
+
try {
|
|
338
|
+
parsed = parseStartBody(body, context);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
writeError(response, 400, 'invalid_request', error instanceof Error ? error.message : 'Request body is invalid');
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
if (!await verifyMutationRequest(options, request, context)) {
|
|
345
|
+
writeError(response, 403, 'mutation_origin_unverified', 'Mutation origin could not be verified');
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const service = await options.serviceFactory(context);
|
|
350
|
+
const result = await service.start(parsed);
|
|
351
|
+
writeJson(response, 200, { action: 'start', operationId: parsed.operationId, result });
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
const failure = serviceError(error);
|
|
355
|
+
writeError(response, failure.status, failure.code, failure.message);
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
};
|
|
359
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { ResourceReservationProblemReference, ResourceReservationService } from './resource-reservations.js';
|
|
2
|
+
declare const ledgerVersion: 1;
|
|
3
|
+
export type ExecutionProblemReference = ResourceReservationProblemReference;
|
|
4
|
+
export interface ExecutionStartInput {
|
|
5
|
+
operationId: string;
|
|
6
|
+
tenantId: string;
|
|
7
|
+
principal: string;
|
|
8
|
+
scopeId: string;
|
|
9
|
+
runId: string;
|
|
10
|
+
reservationId: string;
|
|
11
|
+
approvalId: string;
|
|
12
|
+
authorityRef: string;
|
|
13
|
+
constraintRefs: readonly string[];
|
|
14
|
+
/** Evidence reference only. It never grants execution permission. */
|
|
15
|
+
problem: ExecutionProblemReference;
|
|
16
|
+
}
|
|
17
|
+
export type ExecutionCheckKind = 'authority' | 'approval' | 'constraint' | 'reservation';
|
|
18
|
+
export type ExecutionVerificationPhase = 'initial' | 'final';
|
|
19
|
+
export type ExecutionCheckStatus = 'approved' | 'revoked' | 'expired' | 'unknown';
|
|
20
|
+
export type ExecutionReadAccessStatus = 'approved' | 'revoked' | 'expired' | 'unknown';
|
|
21
|
+
export interface ExecutionCheckRequest extends ExecutionStartInput {
|
|
22
|
+
check: ExecutionCheckKind;
|
|
23
|
+
phase: ExecutionVerificationPhase;
|
|
24
|
+
/** The revision returned by the first check. Providers may fence on it. */
|
|
25
|
+
expectedRevision?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ExecutionCheckResult {
|
|
28
|
+
status: ExecutionCheckStatus;
|
|
29
|
+
revision: string | null;
|
|
30
|
+
/** Opaque token that the effect owner must consume and validate at start. */
|
|
31
|
+
fencingToken?: string;
|
|
32
|
+
/** Short validity bound for the fencing token. */
|
|
33
|
+
expiresAt?: string;
|
|
34
|
+
evidence?: Readonly<Record<string, string>>;
|
|
35
|
+
}
|
|
36
|
+
export interface ExecutionReadAccessRequest {
|
|
37
|
+
operationId: string;
|
|
38
|
+
tenantId: string;
|
|
39
|
+
principal: string;
|
|
40
|
+
scopeId: string;
|
|
41
|
+
phase: 'read';
|
|
42
|
+
}
|
|
43
|
+
export interface ExecutionReadAccessResult {
|
|
44
|
+
status: ExecutionReadAccessStatus;
|
|
45
|
+
revision: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface ExecutionReadAccessPort {
|
|
48
|
+
verify(input: ExecutionReadAccessRequest): ExecutionReadAccessResult | Promise<ExecutionReadAccessResult>;
|
|
49
|
+
}
|
|
50
|
+
export interface ExecutionAuthorityPort {
|
|
51
|
+
verify(input: ExecutionCheckRequest): ExecutionCheckResult | Promise<ExecutionCheckResult>;
|
|
52
|
+
}
|
|
53
|
+
export interface ExecutionApprovalPort {
|
|
54
|
+
verify(input: ExecutionCheckRequest): ExecutionCheckResult | Promise<ExecutionCheckResult>;
|
|
55
|
+
}
|
|
56
|
+
export interface ExecutionConstraintPort {
|
|
57
|
+
verify(input: ExecutionCheckRequest): ExecutionCheckResult | Promise<ExecutionCheckResult>;
|
|
58
|
+
}
|
|
59
|
+
export interface ExecutionReservationPort {
|
|
60
|
+
verify(input: ExecutionCheckRequest): ExecutionCheckResult | Promise<ExecutionCheckResult>;
|
|
61
|
+
/** The returned revision is the read fence; the reservation store is the start linearization point. */
|
|
62
|
+
markStarted(input: ExecutionReservationMarkRequest): ExecutionReservationMarkResult | Promise<ExecutionReservationMarkResult>;
|
|
63
|
+
}
|
|
64
|
+
export interface ExecutionReservationMarkRequest extends ExecutionStartInput {
|
|
65
|
+
expectedRevision: string;
|
|
66
|
+
}
|
|
67
|
+
export interface ExecutionReservationMarkResult {
|
|
68
|
+
status: 'started' | 'unknown';
|
|
69
|
+
}
|
|
70
|
+
export interface ExecutionEffectPort {
|
|
71
|
+
/** The effect owner must consume and validate this capability at its own start boundary. */
|
|
72
|
+
start(input: ExecutionEffectRequest): ExecutionEffectResult | Promise<ExecutionEffectResult>;
|
|
73
|
+
}
|
|
74
|
+
export interface ExecutionEffectRequest extends ExecutionStartInput {
|
|
75
|
+
capability: ExecutionStartCapability;
|
|
76
|
+
}
|
|
77
|
+
export interface ExecutionStartCapability {
|
|
78
|
+
kind: 'execution-start';
|
|
79
|
+
operationId: string;
|
|
80
|
+
tenantId: string;
|
|
81
|
+
principal: string;
|
|
82
|
+
scopeId: string;
|
|
83
|
+
issuedAt: string;
|
|
84
|
+
expiresAt: string;
|
|
85
|
+
checks: Record<ExecutionCheckKind, {
|
|
86
|
+
revision: string;
|
|
87
|
+
fencingToken: string;
|
|
88
|
+
expiresAt: string;
|
|
89
|
+
}>;
|
|
90
|
+
}
|
|
91
|
+
export interface ExecutionEffectResult {
|
|
92
|
+
status: 'started' | 'unknown';
|
|
93
|
+
}
|
|
94
|
+
export type ExecutionIntentPhase = 'recovery_required' | 'reservation_marked' | 'started' | 'unknown' | 'rejected';
|
|
95
|
+
export type ExecutionIntentEffectStatus = 'not_started' | 'started' | 'unknown';
|
|
96
|
+
export type ExecutionIntentReservationMarkStatus = 'not_started' | 'started' | 'unknown';
|
|
97
|
+
export interface ExecutionCheckRecord {
|
|
98
|
+
check: ExecutionCheckKind;
|
|
99
|
+
status: ExecutionCheckStatus;
|
|
100
|
+
revision: string | null;
|
|
101
|
+
fencingTokenDigest?: string;
|
|
102
|
+
expiresAt?: string;
|
|
103
|
+
evidence: Record<string, string>;
|
|
104
|
+
}
|
|
105
|
+
export type ExecutionAuthorityErrorCode = 'validation_error' | 'authority_revoked' | 'authority_expired' | 'authority_unknown' | 'approval_revoked' | 'approval_expired' | 'approval_unknown' | 'constraint_revoked' | 'constraint_expired' | 'constraint_unknown' | 'reservation_revoked' | 'reservation_expired' | 'reservation_unknown' | 'read_revoked' | 'read_expired' | 'read_unknown' | 'revision_conflict' | 'fencing_unsupported' | 'capability_expired' | 'operation_conflict' | 'scope_mismatch' | 'recovery_required' | 'reservation_mark_unknown' | 'effect_unknown' | 'ledger_invalid';
|
|
106
|
+
export interface ExecutionIntentFailure {
|
|
107
|
+
code: ExecutionAuthorityErrorCode;
|
|
108
|
+
reason: string;
|
|
109
|
+
}
|
|
110
|
+
export interface ExecutionIntentRecord {
|
|
111
|
+
version: typeof ledgerVersion;
|
|
112
|
+
operationId: string;
|
|
113
|
+
tenantId: string;
|
|
114
|
+
principal: string;
|
|
115
|
+
scopeId: string;
|
|
116
|
+
runId: string;
|
|
117
|
+
reservationId: string;
|
|
118
|
+
approvalId: string;
|
|
119
|
+
authorityRef: string;
|
|
120
|
+
constraintRefs: string[];
|
|
121
|
+
problem: ExecutionProblemReference;
|
|
122
|
+
payloadDigest: string;
|
|
123
|
+
phase: ExecutionIntentPhase;
|
|
124
|
+
reservationMark: ExecutionIntentReservationMarkStatus;
|
|
125
|
+
effect: {
|
|
126
|
+
operationId: string;
|
|
127
|
+
status: ExecutionIntentEffectStatus;
|
|
128
|
+
};
|
|
129
|
+
verification: {
|
|
130
|
+
initial: ExecutionCheckRecord[];
|
|
131
|
+
final?: ExecutionCheckRecord[];
|
|
132
|
+
};
|
|
133
|
+
capability?: {
|
|
134
|
+
expiresAt: string;
|
|
135
|
+
digest: string;
|
|
136
|
+
};
|
|
137
|
+
failure?: ExecutionIntentFailure;
|
|
138
|
+
createdAt: string;
|
|
139
|
+
updatedAt: string;
|
|
140
|
+
}
|
|
141
|
+
export interface ExecutionStartResult {
|
|
142
|
+
intent: ExecutionIntentRecord;
|
|
143
|
+
effect: {
|
|
144
|
+
operationId: string;
|
|
145
|
+
status: 'started';
|
|
146
|
+
};
|
|
147
|
+
replayed: boolean;
|
|
148
|
+
}
|
|
149
|
+
export interface ExecutionAuthorityServiceOptions {
|
|
150
|
+
dataDir: string;
|
|
151
|
+
authority: ExecutionAuthorityPort;
|
|
152
|
+
approval: ExecutionApprovalPort;
|
|
153
|
+
constraint: ExecutionConstraintPort;
|
|
154
|
+
reservation: ExecutionReservationPort;
|
|
155
|
+
readAccess: ExecutionReadAccessPort;
|
|
156
|
+
effect: ExecutionEffectPort;
|
|
157
|
+
clock?: () => Date;
|
|
158
|
+
sidecarPath?: string;
|
|
159
|
+
}
|
|
160
|
+
export declare class ExecutionAuthorityError extends Error {
|
|
161
|
+
readonly code: ExecutionAuthorityErrorCode;
|
|
162
|
+
constructor(code: ExecutionAuthorityErrorCode, message: string);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Persists the execution intent before crossing either the reservation or
|
|
166
|
+
* external-effect boundary. The reservation and intent sidecars are separate
|
|
167
|
+
* transactions; callers must reconcile recovery_required/unknown states.
|
|
168
|
+
*/
|
|
169
|
+
export declare class ExecutionAuthorityService {
|
|
170
|
+
private readonly dataDir;
|
|
171
|
+
private readonly authority;
|
|
172
|
+
private readonly approval;
|
|
173
|
+
private readonly constraint;
|
|
174
|
+
private readonly reservation;
|
|
175
|
+
private readonly readAccess;
|
|
176
|
+
private readonly effect;
|
|
177
|
+
private readonly clock;
|
|
178
|
+
private readonly sidecarPath;
|
|
179
|
+
constructor(options: ExecutionAuthorityServiceOptions);
|
|
180
|
+
initialize(): Promise<void>;
|
|
181
|
+
start(input: ExecutionStartInput): Promise<ExecutionStartResult>;
|
|
182
|
+
readIntent(input: Pick<ExecutionStartInput, 'operationId' | 'tenantId' | 'principal' | 'scopeId'>): Promise<ExecutionIntentRecord | undefined>;
|
|
183
|
+
private findIntent;
|
|
184
|
+
private verifyReadAccess;
|
|
185
|
+
private recordIntent;
|
|
186
|
+
private updateIntent;
|
|
187
|
+
private tryUpdateIntent;
|
|
188
|
+
private persistRecovery;
|
|
189
|
+
private verifyAll;
|
|
190
|
+
private handleExisting;
|
|
191
|
+
private now;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Binds the existing reservation store to the execution port without calling
|
|
195
|
+
* it from inside the execution sidecar transaction. The store remains the
|
|
196
|
+
* owner of its current authorization and reservation-state checks.
|
|
197
|
+
*/
|
|
198
|
+
export declare function createResourceReservationExecutionPort(options: {
|
|
199
|
+
service: Pick<ResourceReservationService, 'readLedger' | 'startExternalAction'>;
|
|
200
|
+
}): ExecutionReservationPort;
|
|
201
|
+
export {};
|