@unson/brainbase-mcp 0.6.0 → 0.8.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/foundation/graph-history.sql +634 -0
- package/contracts/judgment-value-proof/schema.json +43 -1
- 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 +820 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -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 +63 -0
- package/dist/company-os-objectives.js +234 -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-authenticated-tools.d.ts +56 -0
- package/dist/foundation-authenticated-tools.js +347 -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-graph-http.d.ts +43 -0
- package/dist/foundation-graph-http.js +243 -0
- package/dist/foundation-graph-write.d.ts +90 -0
- package/dist/foundation-graph-write.js +447 -0
- package/dist/foundation-http.d.ts +90 -0
- package/dist/foundation-http.js +439 -0
- package/dist/foundation-public-provider.d.ts +140 -0
- package/dist/foundation-public-provider.js +192 -0
- package/dist/foundation-store.d.ts +132 -0
- package/dist/foundation-store.js +654 -0
- package/dist/graph-corrections.d.ts +157 -0
- package/dist/graph-corrections.js +620 -0
- package/dist/graph-foundation-reader.d.ts +60 -0
- package/dist/graph-foundation-reader.js +494 -0
- package/dist/graph-retrieval.d.ts +6 -1
- package/dist/graph-revision-reader.d.ts +80 -0
- package/dist/graph-revision-reader.js +246 -0
- package/dist/graph-web-http.d.ts +26 -0
- package/dist/graph-web-http.js +215 -0
- package/dist/graph-web.d.ts +364 -0
- package/dist/graph-web.js +639 -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 +213 -0
- package/dist/judgment-problem-snapshot.js +1073 -0
- package/dist/judgment-value-proof-review.d.ts +166 -0
- package/dist/judgment-value-proof-review.js +437 -0
- package/dist/judgment-value-proof.d.ts +28 -1
- package/dist/judgment-value-proof.js +53 -8
- 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/knowledge-continuation.d.ts +127 -0
- package/dist/knowledge-continuation.js +446 -0
- package/dist/knowledge-lookup.d.ts +178 -0
- package/dist/knowledge-lookup.js +758 -0
- package/dist/local-web-fetch-bridge.d.ts +5 -0
- package/dist/local-web-fetch-bridge.js +37 -0
- package/dist/local-web-host.d.ts +109 -0
- package/dist/local-web-host.js +688 -0
- package/dist/local-web-security.d.ts +25 -0
- package/dist/local-web-security.js +95 -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/organization-graph.d.ts +15 -0
- package/dist/organization-graph.js +24 -4
- package/dist/personal-knowledge.d.ts +132 -0
- package/dist/personal-knowledge.js +779 -0
- package/dist/personal-memory-handover.d.ts +108 -0
- package/dist/personal-memory-handover.js +369 -0
- package/dist/philosophy-revision-reader.d.ts +85 -0
- package/dist/philosophy-revision-reader.js +241 -0
- package/dist/portable-graph.d.ts +24 -0
- package/dist/portable-graph.js +77 -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 +104 -2
- package/dist/server.js +186 -4
- 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/value-proof-review-http.d.ts +25 -0
- package/dist/value-proof-review-http.js +185 -0
- package/dist/world-model.d.ts +199 -0
- package/dist/world-model.js +955 -0
- package/package.json +230 -3
- package/ui/brainbase-tokens.css +56 -0
- package/ui/graph-own-share.js +367 -0
- package/ui/graph-projects-view.css +23 -0
- package/ui/graph-projects-view.js +730 -0
- package/ui/graph-registry-view.css +101 -0
- package/ui/graph-registry-view.js +804 -0
- package/ui/graph-view-shared.css +522 -0
- package/ui/graph-view-shared.js +1227 -0
- package/ui/judgment-view.css +312 -0
- package/ui/judgment-view.js +508 -0
- package/ui/local-web-shell.css +396 -0
- package/ui/local-web-shell.js +378 -0
- package/ui/objective-editor-http-port.js +154 -0
- package/ui/objective-editor.css +286 -0
- package/ui/objective-editor.js +1586 -0
- package/ui/outcome-knowledge.css +50 -14
- package/ui/outcome-knowledge.js +15 -1
- package/ui/outcome-mana.css +91 -14
- package/ui/outcome-mana.js +958 -61
- package/ui/value-proof-review.css +504 -0
- package/ui/value-proof-review.js +1104 -0
- package/ui/workspace-kit.css +485 -0
- package/ui/workspace-kit.js +229 -0
- package/ui/world-model-view.css +188 -0
- package/ui/world-model-view.js +486 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { foundationPublicToolDefinitions } from './foundation-public-provider.js';
|
|
3
|
+
import { philosophyRevisionDigest, } from './philosophy-revision-reader.js';
|
|
4
|
+
const FOUNDATION_READ_TYPES = new Set(['objective', 'variable', 'model', 'constraint', 'philosophy']);
|
|
5
|
+
const FOUNDATION_TOOL_NAMES = new Set([
|
|
6
|
+
'foundation_describe',
|
|
7
|
+
'foundation_read',
|
|
8
|
+
'foundation_validate_reference',
|
|
9
|
+
'foundation_validate_problem',
|
|
10
|
+
]);
|
|
11
|
+
const REVISION_PATTERN = /^[1-9]\d*$/u;
|
|
12
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
13
|
+
const MAX_SCOPE_ID_LENGTH = 256;
|
|
14
|
+
/**
|
|
15
|
+
* The public provider owns the contract and schemas. The Host adds the
|
|
16
|
+
* authenticated Graph scope to every public tool because a provider cannot
|
|
17
|
+
* select a tenant on behalf of the caller.
|
|
18
|
+
*/
|
|
19
|
+
function withScopeSchema(definition) {
|
|
20
|
+
const inputSchema = definition.inputSchema;
|
|
21
|
+
const properties = inputSchema.properties && typeof inputSchema.properties === 'object'
|
|
22
|
+
? inputSchema.properties
|
|
23
|
+
: {};
|
|
24
|
+
const required = Array.isArray(inputSchema.required)
|
|
25
|
+
? inputSchema.required.filter((value) => typeof value === 'string')
|
|
26
|
+
: [];
|
|
27
|
+
return {
|
|
28
|
+
...definition,
|
|
29
|
+
inputSchema: {
|
|
30
|
+
...inputSchema,
|
|
31
|
+
properties: {
|
|
32
|
+
...properties,
|
|
33
|
+
scope_id: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
minLength: 1,
|
|
36
|
+
maxLength: MAX_SCOPE_ID_LENGTH,
|
|
37
|
+
description: 'Authenticated project scope used for this read or validation.',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: [...new Set([...required, 'scope_id'])],
|
|
41
|
+
additionalProperties: inputSchema.additionalProperties ?? false,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export const foundationPublicTools = foundationPublicToolDefinitions.map(withScopeSchema);
|
|
46
|
+
export function isFoundationPublicToolName(name) {
|
|
47
|
+
return FOUNDATION_TOOL_NAMES.has(name);
|
|
48
|
+
}
|
|
49
|
+
function isRecord(value) {
|
|
50
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
function isNonEmptyString(value) {
|
|
53
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Scope IDs are copied into x-brainbase-projects. Reject delimiters and
|
|
57
|
+
* control characters before authentication so a caller cannot turn one
|
|
58
|
+
* selected project into an arbitrary project list.
|
|
59
|
+
*/
|
|
60
|
+
function readScopeId(args) {
|
|
61
|
+
if (typeof args.scope_id !== 'string')
|
|
62
|
+
return null;
|
|
63
|
+
const scopeId = args.scope_id.trim();
|
|
64
|
+
if (!scopeId || scopeId.length > MAX_SCOPE_ID_LENGTH)
|
|
65
|
+
return null;
|
|
66
|
+
if (/[\u0000-\u001f\u007f,]/u.test(scopeId))
|
|
67
|
+
return null;
|
|
68
|
+
return scopeId;
|
|
69
|
+
}
|
|
70
|
+
function invalidInput(message, dependencies) {
|
|
71
|
+
return dependencies.auth.toolError('error', 'foundation_request_invalid', message, []);
|
|
72
|
+
}
|
|
73
|
+
function rejectUnexpectedKeys(args, allowed, dependencies) {
|
|
74
|
+
const allowedKeys = new Set(allowed);
|
|
75
|
+
const unexpected = Object.keys(args).find((key) => !allowedKeys.has(key));
|
|
76
|
+
return unexpected ? invalidInput(`unexpected Foundation argument: ${unexpected}`, dependencies) : null;
|
|
77
|
+
}
|
|
78
|
+
function readRequiredString(args, key) {
|
|
79
|
+
return isNonEmptyString(args[key]) ? String(args[key]).trim() : null;
|
|
80
|
+
}
|
|
81
|
+
function validateReadArgs(args, dependencies) {
|
|
82
|
+
const type = readRequiredString(args, 'type');
|
|
83
|
+
const id = readRequiredString(args, 'id');
|
|
84
|
+
const revision = readRequiredString(args, 'revision');
|
|
85
|
+
if (!type || !FOUNDATION_READ_TYPES.has(type))
|
|
86
|
+
return invalidInput('foundation type is invalid', dependencies);
|
|
87
|
+
if (!id)
|
|
88
|
+
return invalidInput('foundation id is required', dependencies);
|
|
89
|
+
if (!revision || !REVISION_PATTERN.test(revision))
|
|
90
|
+
return invalidInput('foundation revision is invalid', dependencies);
|
|
91
|
+
if (args.digest !== undefined && (!isNonEmptyString(args.digest) || !DIGEST_PATTERN.test(String(args.digest)))) {
|
|
92
|
+
return invalidInput('foundation digest is invalid', dependencies);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function validateReferenceArgs(args, dependencies) {
|
|
97
|
+
if (!isRecord(args.reference))
|
|
98
|
+
return invalidInput('foundation reference is required', dependencies);
|
|
99
|
+
const reference = args.reference;
|
|
100
|
+
if (!isNonEmptyString(reference.kind) || !['objective', 'variable', 'model', 'constraint', 'philosophy'].includes(reference.kind)) {
|
|
101
|
+
return invalidInput('foundation reference kind is invalid', dependencies);
|
|
102
|
+
}
|
|
103
|
+
if (!isNonEmptyString(reference.id) || !isNonEmptyString(reference.revision) || !REVISION_PATTERN.test(String(reference.revision))) {
|
|
104
|
+
return invalidInput('foundation reference identity is invalid', dependencies);
|
|
105
|
+
}
|
|
106
|
+
if (!isNonEmptyString(reference.digest) || !DIGEST_PATTERN.test(String(reference.digest))) {
|
|
107
|
+
return invalidInput('foundation reference digest is invalid', dependencies);
|
|
108
|
+
}
|
|
109
|
+
if (!isRecord(reference.scope) || !isNonEmptyString(reference.scope.type) || !isNonEmptyString(reference.scope.id)) {
|
|
110
|
+
return invalidInput('foundation reference scope is invalid', dependencies);
|
|
111
|
+
}
|
|
112
|
+
if (!['personal', 'project', 'organization'].includes(reference.scope.type)) {
|
|
113
|
+
return invalidInput('foundation reference scope type is invalid', dependencies);
|
|
114
|
+
}
|
|
115
|
+
if (!isNonEmptyString(reference.valid_from))
|
|
116
|
+
return invalidInput('foundation reference valid_from is required', dependencies);
|
|
117
|
+
if (args.phase !== undefined && args.phase !== 'read' && args.phase !== 'historical_read') {
|
|
118
|
+
return invalidInput('foundation validation phase is invalid', dependencies);
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function validateProblemArgs(args, dependencies) {
|
|
123
|
+
if (!isRecord(args.snapshot))
|
|
124
|
+
return invalidInput('foundation snapshot is required', dependencies);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function selectedContext(context, scopeId) {
|
|
128
|
+
// authenticateProject proves membership against both the token and the
|
|
129
|
+
// configured allow-list. Restrict the outbound header to the selected
|
|
130
|
+
// project instead of forwarding every project in the token.
|
|
131
|
+
return { token: context.token, scope: [scopeId] };
|
|
132
|
+
}
|
|
133
|
+
function requestFor(name, args, scopeId) {
|
|
134
|
+
const query = new URLSearchParams({ scope_id: scopeId });
|
|
135
|
+
if (name === 'foundation_describe') {
|
|
136
|
+
return { path: `/api/foundation/contract?${query.toString()}`, method: 'GET' };
|
|
137
|
+
}
|
|
138
|
+
if (name === 'foundation_read') {
|
|
139
|
+
const type = String(args.type);
|
|
140
|
+
const id = String(args.id);
|
|
141
|
+
query.set('revision', String(args.revision));
|
|
142
|
+
if (args.digest !== undefined)
|
|
143
|
+
query.set('digest', String(args.digest));
|
|
144
|
+
return {
|
|
145
|
+
path: `/api/foundation/definitions/${encodeURIComponent(type)}/${encodeURIComponent(id)}?${query.toString()}`,
|
|
146
|
+
method: 'GET',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (name === 'foundation_validate_reference') {
|
|
150
|
+
const { scope_id: _scopeId, ...body } = args;
|
|
151
|
+
return {
|
|
152
|
+
path: `/api/foundation/judgment-references/validate?${query.toString()}`,
|
|
153
|
+
method: 'POST',
|
|
154
|
+
body,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const { scope_id: _scopeId, ...body } = args;
|
|
158
|
+
return {
|
|
159
|
+
path: `/api/foundation/judgment-problems/validate?${query.toString()}`,
|
|
160
|
+
method: 'POST',
|
|
161
|
+
body,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function fetchFoundationCsrfToken(dependencies, context) {
|
|
165
|
+
const sessionId = `brainbase-mcp-foundation-${randomUUID()}`;
|
|
166
|
+
let response;
|
|
167
|
+
try {
|
|
168
|
+
response = await (dependencies.fetch || globalThis.fetch)(`${dependencies.apiUrl.replace(/\/+$/, '')}/api/csrf-token`, {
|
|
169
|
+
method: 'GET',
|
|
170
|
+
headers: {
|
|
171
|
+
Authorization: `Bearer ${context.token}`,
|
|
172
|
+
'x-brainbase-projects': context.scope.join(','),
|
|
173
|
+
'x-session-id': sessionId,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
result: dependencies.auth.toolError('unavailable', 'foundation_csrf_unavailable', error instanceof Error ? error.message : String(error), context.scope),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
let payload = null;
|
|
184
|
+
let payloadParsed = false;
|
|
185
|
+
try {
|
|
186
|
+
payload = await response.json();
|
|
187
|
+
payloadParsed = true;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Keep the response fail-closed below; a 200 without a token is not a
|
|
191
|
+
// usable CSRF handshake.
|
|
192
|
+
}
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
return { ok: false, result: errorFromResponse(response, payload, context.scope, dependencies) };
|
|
195
|
+
}
|
|
196
|
+
const token = isRecord(payload) && typeof payload.token === 'string' ? payload.token.trim() : '';
|
|
197
|
+
if (!payloadParsed || !token) {
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
result: dependencies.auth.toolError('error', 'foundation_csrf_response_invalid', 'Brainbase API returned an invalid CSRF response', context.scope, response.status),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return { ok: true, sessionId, token };
|
|
204
|
+
}
|
|
205
|
+
async function fetchFoundationRequest(dependencies, context, request) {
|
|
206
|
+
if (request.method !== 'POST') {
|
|
207
|
+
return dependencies.auth.fetchAuthenticatedJson(context, request);
|
|
208
|
+
}
|
|
209
|
+
const csrf = await fetchFoundationCsrfToken(dependencies, context);
|
|
210
|
+
if (!csrf.ok)
|
|
211
|
+
return csrf;
|
|
212
|
+
return dependencies.auth.fetchAuthenticatedJson(context, {
|
|
213
|
+
...request,
|
|
214
|
+
headers: {
|
|
215
|
+
'x-session-id': csrf.sessionId,
|
|
216
|
+
'x-csrf-token': csrf.token,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function errorFromResponse(response, payload, scope, dependencies) {
|
|
221
|
+
const error = isRecord(payload) && isRecord(payload.error) ? payload.error : null;
|
|
222
|
+
const code = typeof error?.code === 'string' && error.code.trim()
|
|
223
|
+
? error.code
|
|
224
|
+
: response.status === 404
|
|
225
|
+
? 'foundation_api_unavailable'
|
|
226
|
+
: response.status >= 500
|
|
227
|
+
? 'foundation_api_unavailable'
|
|
228
|
+
: 'foundation_api_error';
|
|
229
|
+
const message = typeof error?.message === 'string' && error.message.trim()
|
|
230
|
+
? error.message
|
|
231
|
+
: `${response.status} ${response.statusText}`.trim();
|
|
232
|
+
const status = response.status === 404 || response.status >= 500 ? 'unavailable' : 'error';
|
|
233
|
+
return dependencies.auth.toolError(status, code, message, scope, response.status, error?.details);
|
|
234
|
+
}
|
|
235
|
+
function isFoundationScope(value) {
|
|
236
|
+
return isRecord(value)
|
|
237
|
+
&& (value.type === 'personal' || value.type === 'project' || value.type === 'organization')
|
|
238
|
+
&& isNonEmptyString(value.id);
|
|
239
|
+
}
|
|
240
|
+
function isFoundationAcl(value) {
|
|
241
|
+
return isRecord(value)
|
|
242
|
+
&& isNonEmptyString(value.ownerId)
|
|
243
|
+
&& (value.visibility === 'private'
|
|
244
|
+
|| value.visibility === 'project'
|
|
245
|
+
|| value.visibility === 'organization'
|
|
246
|
+
|| value.visibility === 'public')
|
|
247
|
+
&& Array.isArray(value.readerIds)
|
|
248
|
+
&& value.readerIds.every((readerId) => isNonEmptyString(readerId))
|
|
249
|
+
&& new Set(value.readerIds).size === value.readerIds.length
|
|
250
|
+
&& Array.isArray(value.writerIds)
|
|
251
|
+
&& value.writerIds.every((writerId) => isNonEmptyString(writerId))
|
|
252
|
+
&& new Set(value.writerIds).size === value.writerIds.length;
|
|
253
|
+
}
|
|
254
|
+
function isValidPhilosophyRevisionPayload(args, payload) {
|
|
255
|
+
if (payload.kind !== 'philosophy'
|
|
256
|
+
|| payload.id !== args.id
|
|
257
|
+
|| payload.revision !== args.revision
|
|
258
|
+
|| !isNonEmptyString(payload.digest)
|
|
259
|
+
|| !DIGEST_PATTERN.test(payload.digest)
|
|
260
|
+
|| !Object.prototype.hasOwnProperty.call(payload, 'payload')
|
|
261
|
+
|| !isRecord(payload.applicability)
|
|
262
|
+
|| !isFoundationScope(payload.applicability.scope)
|
|
263
|
+
|| !isFoundationAcl(payload.currentAcl)
|
|
264
|
+
|| !isFoundationScope(payload.currentScope)
|
|
265
|
+
|| (args.digest !== undefined && payload.digest !== args.digest)) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
// The canonical helper validates the JSON payload and applicability, and
|
|
270
|
+
// excludes read-time ACL/scope metadata from the immutable digest.
|
|
271
|
+
return philosophyRevisionDigest(payload) === payload.digest;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function isValidSuccessPayload(name, args, payload) {
|
|
278
|
+
if (!isRecord(payload))
|
|
279
|
+
return false;
|
|
280
|
+
if (name === 'foundation_describe') {
|
|
281
|
+
return payload.contractVersion === 'foundation-public.v1'
|
|
282
|
+
&& payload.connection === 'configured'
|
|
283
|
+
&& isRecord(payload.foundation)
|
|
284
|
+
&& payload.executionPermission === 'none';
|
|
285
|
+
}
|
|
286
|
+
if (name === 'foundation_read') {
|
|
287
|
+
if (args.type === 'philosophy') {
|
|
288
|
+
return isValidPhilosophyRevisionPayload(args, payload);
|
|
289
|
+
}
|
|
290
|
+
const definition = isRecord(payload.definition) ? payload.definition : null;
|
|
291
|
+
return definition !== null
|
|
292
|
+
&& isNonEmptyString(payload.digest)
|
|
293
|
+
&& DIGEST_PATTERN.test(payload.digest)
|
|
294
|
+
&& definition.id === args.id
|
|
295
|
+
&& definition.type === args.type
|
|
296
|
+
&& definition.revision === args.revision
|
|
297
|
+
&& (args.digest === undefined || payload.digest === args.digest);
|
|
298
|
+
}
|
|
299
|
+
// The canonical HTTP route returns 200 only for a resolved validation. A
|
|
300
|
+
// non-resolved result must remain an error/unavailable response even if an
|
|
301
|
+
// incorrectly connected upstream serializes it with HTTP 200.
|
|
302
|
+
return payload.status === 'resolved';
|
|
303
|
+
}
|
|
304
|
+
export async function handleFoundationPublicToolCall(name, args, dependencies) {
|
|
305
|
+
if (!isFoundationPublicToolName(name))
|
|
306
|
+
return null;
|
|
307
|
+
const scopeId = readScopeId(args);
|
|
308
|
+
if (!scopeId)
|
|
309
|
+
return invalidInput('scope_id is required and must be a safe project identifier', dependencies);
|
|
310
|
+
const unexpected = rejectUnexpectedKeys(args, name === 'foundation_describe'
|
|
311
|
+
? ['scope_id']
|
|
312
|
+
: name === 'foundation_read'
|
|
313
|
+
? ['scope_id', 'type', 'id', 'revision', 'digest']
|
|
314
|
+
: name === 'foundation_validate_reference'
|
|
315
|
+
? ['scope_id', 'reference', 'phase']
|
|
316
|
+
: ['scope_id', 'snapshot'], dependencies);
|
|
317
|
+
if (unexpected)
|
|
318
|
+
return unexpected;
|
|
319
|
+
if (name === 'foundation_read') {
|
|
320
|
+
const invalid = validateReadArgs(args, dependencies);
|
|
321
|
+
if (invalid)
|
|
322
|
+
return invalid;
|
|
323
|
+
}
|
|
324
|
+
else if (name === 'foundation_validate_reference') {
|
|
325
|
+
const invalid = validateReferenceArgs(args, dependencies);
|
|
326
|
+
if (invalid)
|
|
327
|
+
return invalid;
|
|
328
|
+
}
|
|
329
|
+
else if (name === 'foundation_validate_problem') {
|
|
330
|
+
const invalid = validateProblemArgs(args, dependencies);
|
|
331
|
+
if (invalid)
|
|
332
|
+
return invalid;
|
|
333
|
+
}
|
|
334
|
+
const authenticated = await dependencies.auth.authenticateProject({ project_code: scopeId }, { requireProject: true });
|
|
335
|
+
if ('status' in authenticated)
|
|
336
|
+
return authenticated;
|
|
337
|
+
const context = selectedContext(authenticated, scopeId);
|
|
338
|
+
const fetched = await fetchFoundationRequest(dependencies, context, requestFor(name, args, scopeId));
|
|
339
|
+
if (!fetched.ok)
|
|
340
|
+
return fetched.result;
|
|
341
|
+
if (!fetched.response.ok)
|
|
342
|
+
return errorFromResponse(fetched.response, fetched.payload, context.scope, dependencies);
|
|
343
|
+
if (!fetched.payloadParsed || !isValidSuccessPayload(name, args, fetched.payload)) {
|
|
344
|
+
return dependencies.auth.toolError('error', 'foundation_api_response_invalid', 'Brainbase API returned an invalid Foundation response', context.scope, fetched.response.status);
|
|
345
|
+
}
|
|
346
|
+
return { status: 'ok', scope: { project_codes: context.scope }, data: fetched.payload };
|
|
347
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FoundationDefinition, FoundationRevision, FoundationType } from './ontology-foundation.js';
|
|
2
|
+
import type { FoundationCatalog, FoundationCatalogRecord } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The key is an encoded tuple rather than `${type}:${id}` so IDs containing a
|
|
5
|
+
* colon cannot alias another logical resource.
|
|
6
|
+
*/
|
|
7
|
+
export declare function foundationKey(reference: Pick<FoundationRevision, 'type' | 'id'>): string;
|
|
8
|
+
export declare function createEmptyFoundationCatalog(): FoundationCatalog;
|
|
9
|
+
export declare function cloneFoundationCatalog(catalog: FoundationCatalog): FoundationCatalog;
|
|
10
|
+
export declare function cloneFoundationDefinition<T extends FoundationDefinition>(definition: T): T;
|
|
11
|
+
export interface FoundationRef extends FoundationRevision {
|
|
12
|
+
digest: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function foundationRef(record: FoundationCatalogRecord): FoundationRef;
|
|
15
|
+
/**
|
|
16
|
+
* Hashes the complete definition, with object keys sorted recursively. Array
|
|
17
|
+
* order remains significant because criteria, provenance, and authorized uses
|
|
18
|
+
* are ordered contracts in the shared ontology.
|
|
19
|
+
*/
|
|
20
|
+
export declare function digestFoundationDefinition(definition: FoundationDefinition): string;
|
|
21
|
+
export declare function nextFoundationRevision(revision: string): string;
|
|
22
|
+
export declare function assertFoundationRevision(value: unknown, label?: string): asserts value is FoundationRevision;
|
|
23
|
+
export declare function assertFoundationCatalog(value: unknown): asserts value is FoundationCatalog | undefined;
|
|
24
|
+
export declare function foundationCatalogOrEmpty(value: FoundationCatalog | undefined): FoundationCatalog;
|
|
25
|
+
export declare function findFoundationRecord(catalog: FoundationCatalog, reference: FoundationRevision): FoundationCatalogRecord | undefined;
|
|
26
|
+
export declare function findLatestFoundationRecord(catalog: FoundationCatalog, type: FoundationType, id: string): FoundationCatalogRecord | undefined;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { validateFoundationDefinition, validateFoundationRelation } from './ontology-foundation.js';
|
|
3
|
+
const revisionPattern = /^[1-9]\d*$/;
|
|
4
|
+
const digestPattern = /^sha256:[0-9a-f]{64}$/;
|
|
5
|
+
/**
|
|
6
|
+
* The key is an encoded tuple rather than `${type}:${id}` so IDs containing a
|
|
7
|
+
* colon cannot alias another logical resource.
|
|
8
|
+
*/
|
|
9
|
+
export function foundationKey(reference) {
|
|
10
|
+
return JSON.stringify([reference.type, reference.id]);
|
|
11
|
+
}
|
|
12
|
+
export function createEmptyFoundationCatalog() {
|
|
13
|
+
return {
|
|
14
|
+
version: 1,
|
|
15
|
+
records: [],
|
|
16
|
+
latest: {},
|
|
17
|
+
relations: []
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function cloneFoundationCatalog(catalog) {
|
|
21
|
+
return JSON.parse(JSON.stringify(catalog));
|
|
22
|
+
}
|
|
23
|
+
export function cloneFoundationDefinition(definition) {
|
|
24
|
+
return JSON.parse(JSON.stringify(definition));
|
|
25
|
+
}
|
|
26
|
+
export function foundationRef(record) {
|
|
27
|
+
return {
|
|
28
|
+
id: record.definition.id,
|
|
29
|
+
type: record.definition.type,
|
|
30
|
+
revision: record.definition.revision,
|
|
31
|
+
digest: record.digest
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Hashes the complete definition, with object keys sorted recursively. Array
|
|
36
|
+
* order remains significant because criteria, provenance, and authorized uses
|
|
37
|
+
* are ordered contracts in the shared ontology.
|
|
38
|
+
*/
|
|
39
|
+
export function digestFoundationDefinition(definition) {
|
|
40
|
+
const canonical = canonicalJson(definition);
|
|
41
|
+
return `sha256:${createHash('sha256').update(canonical, 'utf8').digest('hex')}`;
|
|
42
|
+
}
|
|
43
|
+
export function nextFoundationRevision(revision) {
|
|
44
|
+
assertRevision(revision, 'revision');
|
|
45
|
+
return (BigInt(revision) + 1n).toString();
|
|
46
|
+
}
|
|
47
|
+
export function assertFoundationRevision(value, label = 'revision') {
|
|
48
|
+
if (!isRecord(value)
|
|
49
|
+
|| typeof value.id !== 'string'
|
|
50
|
+
|| value.id.length === 0
|
|
51
|
+
|| !isFoundationType(value.type)
|
|
52
|
+
|| typeof value.revision !== 'string') {
|
|
53
|
+
throw new Error(`FOUNDATION-INVALID-REFERENCE: ${label} must contain id, type, and revision`);
|
|
54
|
+
}
|
|
55
|
+
assertRevision(value.revision, `${label}.revision`);
|
|
56
|
+
}
|
|
57
|
+
export function assertFoundationCatalog(value) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
return;
|
|
60
|
+
if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.records) || !Array.isArray(value.relations) || !isRecord(value.latest)) {
|
|
61
|
+
throw new Error('FOUNDATION-CATALOG-CORRUPT: version, records, latest, and relations are required');
|
|
62
|
+
}
|
|
63
|
+
const recordsByRevision = new Map();
|
|
64
|
+
const latestByLogical = new Map();
|
|
65
|
+
for (const [index, item] of value.records.entries()) {
|
|
66
|
+
if (!isRecord(item) || !isRecord(item.definition) || typeof item.digest !== 'string') {
|
|
67
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: records[${index}] must contain a definition and digest`);
|
|
68
|
+
}
|
|
69
|
+
const definition = item.definition;
|
|
70
|
+
assertDefinitionShape(definition, `records[${index}].definition`);
|
|
71
|
+
const revisionKey = `${foundationKey(definition)}@${definition.revision}`;
|
|
72
|
+
if (recordsByRevision.has(revisionKey)) {
|
|
73
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: duplicate revision ${revisionKey}`);
|
|
74
|
+
}
|
|
75
|
+
if (!digestPattern.test(item.digest) || item.digest !== digestFoundationDefinition(definition)) {
|
|
76
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: digest mismatch for ${revisionKey}`);
|
|
77
|
+
}
|
|
78
|
+
const record = item;
|
|
79
|
+
recordsByRevision.set(revisionKey, record);
|
|
80
|
+
const logicalKey = foundationKey(definition);
|
|
81
|
+
const previous = latestByLogical.get(logicalKey);
|
|
82
|
+
if (!previous || compareRevisions(definition.revision, previous.definition.revision) > 0) {
|
|
83
|
+
latestByLogical.set(logicalKey, record);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const latest = value.latest;
|
|
87
|
+
const latestKeys = Object.keys(latest);
|
|
88
|
+
if (latestKeys.length !== latestByLogical.size) {
|
|
89
|
+
throw new Error('FOUNDATION-CATALOG-CORRUPT: latest pointers do not match records');
|
|
90
|
+
}
|
|
91
|
+
for (const [logicalKey, record] of latestByLogical) {
|
|
92
|
+
const pointer = latest[logicalKey];
|
|
93
|
+
assertFoundationRevision(pointer, `latest[${logicalKey}]`);
|
|
94
|
+
if (foundationKey(pointer) !== logicalKey || pointer.revision !== record.definition.revision) {
|
|
95
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: latest pointer mismatch for ${logicalKey}`);
|
|
96
|
+
}
|
|
97
|
+
if (!recordsByRevision.has(`${logicalKey}@${pointer.revision}`)) {
|
|
98
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: latest pointer has no record for ${logicalKey}@${pointer.revision}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const [index, relation] of value.relations.entries()) {
|
|
102
|
+
if (!isRecord(relation))
|
|
103
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: relations[${index}] is not an object`);
|
|
104
|
+
const result = safeValidateRelation(relation);
|
|
105
|
+
if (!result.valid) {
|
|
106
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: relations[${index}] ${result.issues.map((issue) => issue.message).join('; ')}`);
|
|
107
|
+
}
|
|
108
|
+
for (const endpoint of [relation.source, relation.target]) {
|
|
109
|
+
if (!isRecord(endpoint) || typeof endpoint.id !== 'string' || !isFoundationType(endpoint.type) || typeof endpoint.revision !== 'string')
|
|
110
|
+
continue;
|
|
111
|
+
assertRevision(endpoint.revision, `relations[${index}] endpoint.revision`);
|
|
112
|
+
const endpointKey = `${foundationKey({ id: endpoint.id, type: endpoint.type })}@${endpoint.revision}`;
|
|
113
|
+
if (!recordsByRevision.has(endpointKey)) {
|
|
114
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: relation endpoint ${endpointKey} has no record`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function foundationCatalogOrEmpty(value) {
|
|
120
|
+
if (value === undefined)
|
|
121
|
+
return createEmptyFoundationCatalog();
|
|
122
|
+
assertFoundationCatalog(value);
|
|
123
|
+
return cloneFoundationCatalog(value);
|
|
124
|
+
}
|
|
125
|
+
export function findFoundationRecord(catalog, reference) {
|
|
126
|
+
assertFoundationRevision(reference);
|
|
127
|
+
return catalog.records.find((record) => (record.definition.id === reference.id
|
|
128
|
+
&& record.definition.type === reference.type
|
|
129
|
+
&& record.definition.revision === reference.revision));
|
|
130
|
+
}
|
|
131
|
+
export function findLatestFoundationRecord(catalog, type, id) {
|
|
132
|
+
const pointer = catalog.latest[foundationKey({ type, id })];
|
|
133
|
+
return pointer ? findFoundationRecord(catalog, pointer) : undefined;
|
|
134
|
+
}
|
|
135
|
+
function assertDefinitionShape(definition, label) {
|
|
136
|
+
if (!isRecord(definition)
|
|
137
|
+
|| typeof definition.id !== 'string'
|
|
138
|
+
|| definition.id.length === 0
|
|
139
|
+
|| !isFoundationType(definition.type)
|
|
140
|
+
|| typeof definition.revision !== 'string') {
|
|
141
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: ${label} has an invalid identity`);
|
|
142
|
+
}
|
|
143
|
+
assertRevision(definition.revision, `${label}.revision`);
|
|
144
|
+
if (!isRecord(definition.acl)
|
|
145
|
+
|| typeof definition.acl.ownerId !== 'string'
|
|
146
|
+
|| definition.acl.ownerId.length === 0
|
|
147
|
+
|| !Array.isArray(definition.acl.readerIds)
|
|
148
|
+
|| !Array.isArray(definition.acl.writerIds)
|
|
149
|
+
|| !['private', 'project', 'organization', 'public'].includes(definition.acl.visibility)) {
|
|
150
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: ${label}.acl is invalid`);
|
|
151
|
+
}
|
|
152
|
+
const result = safeValidateDefinition(definition);
|
|
153
|
+
if (!result.valid) {
|
|
154
|
+
throw new Error(`FOUNDATION-CATALOG-CORRUPT: ${label} ${result.issues.map((issue) => issue.message).join('; ')}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function safeValidateDefinition(definition) {
|
|
158
|
+
try {
|
|
159
|
+
return validateFoundationDefinition(definition, { use: 'draft' });
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
throw new Error(`invalid definition: ${formatError(error)}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function safeValidateRelation(relation) {
|
|
166
|
+
try {
|
|
167
|
+
return validateFoundationRelation(relation);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
throw new Error(`invalid relation: ${formatError(error)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function assertRevision(value, label) {
|
|
174
|
+
if (!revisionPattern.test(value)) {
|
|
175
|
+
throw new Error(`FOUNDATION-INVALID-REVISION: ${label} must be a positive canonical decimal`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function compareRevisions(left, right) {
|
|
179
|
+
const leftValue = BigInt(left);
|
|
180
|
+
const rightValue = BigInt(right);
|
|
181
|
+
return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
|
|
182
|
+
}
|
|
183
|
+
function isFoundationType(value) {
|
|
184
|
+
return value === 'objective' || value === 'variable' || value === 'model' || value === 'constraint';
|
|
185
|
+
}
|
|
186
|
+
function isRecord(value) {
|
|
187
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
188
|
+
}
|
|
189
|
+
function canonicalJson(value) {
|
|
190
|
+
const canonical = canonicalize(value);
|
|
191
|
+
const serialized = JSON.stringify(canonical);
|
|
192
|
+
if (serialized === undefined)
|
|
193
|
+
throw new Error('FOUNDATION-DIGEST-INVALID: definition is not JSON serializable');
|
|
194
|
+
return serialized;
|
|
195
|
+
}
|
|
196
|
+
function canonicalize(value) {
|
|
197
|
+
if (Array.isArray(value))
|
|
198
|
+
return value.map((item) => canonicalize(item));
|
|
199
|
+
if (isRecord(value)) {
|
|
200
|
+
return Object.fromEntries(Object.keys(value).sort().flatMap((key) => (value[key] === undefined ? [] : [[key, canonicalize(value[key])]])));
|
|
201
|
+
}
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
function formatError(error) {
|
|
205
|
+
return error instanceof Error ? error.message : String(error);
|
|
206
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FoundationRevisionStore } from './foundation-store.js';
|
|
2
|
+
import { type ConstraintAuthorizationContext, type ConstraintExceptionQuery, type ConstraintExceptionRecord, type ConstraintProjection, type ConstraintStore, type ConstraintStoreQuery } from './constraint-resolution.js';
|
|
3
|
+
/**
|
|
4
|
+
* Exception records are deliberately a host supplied boundary. The shared
|
|
5
|
+
* foundation catalog currently stores the ontology-level exception reference
|
|
6
|
+
* (Decision revision and scope), while Story04 also needs approval metadata,
|
|
7
|
+
* expiry, and rationale. A host must therefore provide the canonical
|
|
8
|
+
* evidence/execution adapter for that record until the shared contract grows
|
|
9
|
+
* those fields; this module never creates a private sidecar file.
|
|
10
|
+
*/
|
|
11
|
+
export interface ConstraintExceptionStore {
|
|
12
|
+
append(exception: ConstraintExceptionRecord, context: ConstraintAuthorizationContext): Promise<ConstraintExceptionRecord>;
|
|
13
|
+
list(query: ConstraintExceptionQuery, context: ConstraintAuthorizationContext): Promise<readonly ConstraintExceptionRecord[]>;
|
|
14
|
+
}
|
|
15
|
+
export interface FoundationConstraintStoreOptions {
|
|
16
|
+
foundationStore: FoundationRevisionStore;
|
|
17
|
+
exceptionStore: ConstraintExceptionStore;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Adapts the Objective Story's GraphFoundationRevisionStore to the resolver's
|
|
21
|
+
* ConstraintStore port. Definitions are persisted in the existing PersonalOs
|
|
22
|
+
* aggregate through FoundationRevisionStore; no second Graph or file is
|
|
23
|
+
* created here.
|
|
24
|
+
*/
|
|
25
|
+
export declare class FoundationConstraintStore<TConstraint extends ConstraintProjection = ConstraintProjection> implements ConstraintStore<TConstraint> {
|
|
26
|
+
private readonly foundationStore;
|
|
27
|
+
private readonly exceptionStore;
|
|
28
|
+
constructor(options: FoundationConstraintStoreOptions);
|
|
29
|
+
append(input: {
|
|
30
|
+
record: TConstraint;
|
|
31
|
+
expectedPreviousRevision?: string | null;
|
|
32
|
+
}, context?: ConstraintAuthorizationContext): Promise<TConstraint>;
|
|
33
|
+
get(ref: {
|
|
34
|
+
id: string;
|
|
35
|
+
revision: string;
|
|
36
|
+
}, context?: ConstraintAuthorizationContext): Promise<TConstraint | null>;
|
|
37
|
+
getLatest(id: string, context?: ConstraintAuthorizationContext): Promise<TConstraint | null>;
|
|
38
|
+
list(query: ConstraintStoreQuery, context?: ConstraintAuthorizationContext): Promise<readonly TConstraint[]>;
|
|
39
|
+
appendException(exception: ConstraintExceptionRecord, context?: ConstraintAuthorizationContext): Promise<ConstraintExceptionRecord>;
|
|
40
|
+
listExceptions(query: ConstraintExceptionQuery, context?: ConstraintAuthorizationContext): Promise<readonly ConstraintExceptionRecord[]>;
|
|
41
|
+
/**
|
|
42
|
+
* The injected exception boundary is intentionally persistence-only and may
|
|
43
|
+
* not enforce Foundation ACLs itself. Resolve the referenced Constraint at
|
|
44
|
+
* this adapter boundary before allowing exception evidence to cross it.
|
|
45
|
+
* Current ACL controls access even when an exception points at an older
|
|
46
|
+
* revision; the exact revision is still read to reject dangling references.
|
|
47
|
+
*/
|
|
48
|
+
private authorizeExceptionTarget;
|
|
49
|
+
}
|
|
50
|
+
export declare function createFoundationConstraintStore(options: FoundationConstraintStoreOptions): ConstraintStore;
|