@unson/brainbase-mcp 0.7.0 → 0.9.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 +1 -1
- package/contracts/foundation/graph-history.sql +634 -0
- package/contracts/judgment-dag/digest.json +2 -2
- package/contracts/judgment-value-proof/schema.json +43 -1
- package/dist/canonical-task-service.js +16 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/company-os-objectives.d.ts +11 -0
- package/dist/company-os-objectives.js +84 -47
- package/dist/foundation-authenticated-tools.d.ts +56 -0
- package/dist/foundation-authenticated-tools.js +347 -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-public-provider.d.ts +140 -0
- package/dist/foundation-public-provider.js +192 -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-problem-snapshot.d.ts +30 -2
- package/dist/judgment-problem-snapshot.js +302 -6
- 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/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/organization-graph.d.ts +15 -0
- package/dist/organization-graph.js +24 -4
- package/dist/personal-knowledge.d.ts +12 -0
- package/dist/personal-knowledge.js +29 -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/server.d.ts +7 -2
- package/dist/server.js +9 -6
- 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 +21 -3
- package/dist/world-model.js +94 -12
- package/package.json +94 -6
- 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 +119 -45
- 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 +180 -74
- package/ui/objective-editor.js +490 -43
- package/ui/outcome-knowledge.css +50 -14
- package/ui/outcome-knowledge.js +15 -1
- 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
- package/ui/icons/mana/LICENSE.tabler.txt +0 -21
- package/ui/icons/mana/affiliate.svg +0 -20
- package/ui/icons/mana/brand-github.svg +0 -15
- package/ui/icons/mana/brand-google-drive.svg +0 -17
- package/ui/icons/mana/brand-slack.svg +0 -18
- package/ui/icons/mana/cpu.svg +0 -24
- package/ui/outcome-mana.css +0 -255
- package/ui/outcome-mana.js +0 -2325
|
@@ -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,43 @@
|
|
|
1
|
+
import { type FoundationHttpCsrfVerifier, type FoundationHttpRouter } from './foundation-http.js';
|
|
2
|
+
/**
|
|
3
|
+
* Readiness probe for the Graph history contract used by the public adapter.
|
|
4
|
+
*
|
|
5
|
+
* The query is deliberately kept in the reusable adapter. Hosts provide the
|
|
6
|
+
* transaction-bound query port, while the migration remains the authority for
|
|
7
|
+
* RLS and immutable-history trigger state.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FOUNDATION_GRAPH_READY_SQL = "SELECT\n EXISTS (SELECT 1 FROM pg_class WHERE oid = to_regclass('public.graph_foundation_revisions') AND relrowsecurity)\n AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_entities')\n AND tgname = 'graph_entities_foundation_history_capture' AND tgenabled = 'O' AND tgtype = 21\n AND tgfoid = to_regprocedure('public.capture_graph_foundation_revision()'))\n AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_foundation_revisions')\n AND tgname = 'graph_foundation_revisions_guard' AND tgenabled = 'O' AND tgtype = 31\n AND tgfoid = to_regprocedure('public.guard_graph_foundation_revision_mutation()'))\n AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_foundation_revisions')\n AND tgname = 'graph_foundation_revisions_truncate_guard' AND tgenabled = 'O' AND tgtype = 34\n AND tgfoid = to_regprocedure('public.guard_graph_foundation_revision_mutation()')) AS ready";
|
|
10
|
+
export declare const FOUNDATION_GRAPH_CONTRACT_VERSION: "foundation-graph-http.v1";
|
|
11
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
12
|
+
export interface FoundationGraphTrustedIdentity {
|
|
13
|
+
/** Canonical principal supplied by the host authentication boundary. */
|
|
14
|
+
readonly principal: string;
|
|
15
|
+
/** Canonical organization scope supplied by the host authentication boundary. */
|
|
16
|
+
readonly organizationId: string;
|
|
17
|
+
/** Project scopes the authenticated principal may select. `*` is supported by hosts. */
|
|
18
|
+
readonly projectScopeIds: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
export interface FoundationGraphQueryResult {
|
|
21
|
+
readonly rows: readonly Record<string, unknown>[];
|
|
22
|
+
}
|
|
23
|
+
export interface FoundationGraphQueryClient {
|
|
24
|
+
query(text: string, values?: readonly unknown[]): FoundationGraphQueryResult | Promise<FoundationGraphQueryResult>;
|
|
25
|
+
}
|
|
26
|
+
export interface FoundationGraphHttpOptions {
|
|
27
|
+
/** Resolve only trusted identity; request body and query values are never used for identity. */
|
|
28
|
+
readonly resolveTrustedIdentity: (request: Request) => MaybePromise<FoundationGraphTrustedIdentity | null>;
|
|
29
|
+
/** Run the shared provider inside the host's authenticated transaction/context. */
|
|
30
|
+
readonly withAccessContext: <T>(identity: FoundationGraphTrustedIdentity, callback: (client: FoundationGraphQueryClient) => Promise<T>) => Promise<T>;
|
|
31
|
+
/** Host-owned CSRF verification port for mutation requests. */
|
|
32
|
+
readonly csrf?: FoundationHttpCsrfVerifier;
|
|
33
|
+
readonly bodyLimitBytes?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create the reusable Graph-backed public Foundation handler.
|
|
37
|
+
*
|
|
38
|
+
* This is a native Fetch-style adapter. It has no Express, authentication, or
|
|
39
|
+
* organization dependency: a host injects a trusted identity resolver, an
|
|
40
|
+
* already-authenticated transaction port, and its CSRF verifier.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createFoundationGraphHttpHandler(options: FoundationGraphHttpOptions): FoundationHttpRouter;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { FOUNDATION_HTTP_CONTRACT_VERSION, createFoundationHttpRouter } from './foundation-http.js';
|
|
2
|
+
import { createFoundationPublicProvider, createFoundationPublicRoute } from './foundation-public-provider.js';
|
|
3
|
+
import { createGraphFoundationReaders } from './graph-foundation-reader.js';
|
|
4
|
+
/**
|
|
5
|
+
* Readiness probe for the Graph history contract used by the public adapter.
|
|
6
|
+
*
|
|
7
|
+
* The query is deliberately kept in the reusable adapter. Hosts provide the
|
|
8
|
+
* transaction-bound query port, while the migration remains the authority for
|
|
9
|
+
* RLS and immutable-history trigger state.
|
|
10
|
+
*/
|
|
11
|
+
export const FOUNDATION_GRAPH_READY_SQL = `SELECT
|
|
12
|
+
EXISTS (SELECT 1 FROM pg_class WHERE oid = to_regclass('public.graph_foundation_revisions') AND relrowsecurity)
|
|
13
|
+
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_entities')
|
|
14
|
+
AND tgname = 'graph_entities_foundation_history_capture' AND tgenabled = 'O' AND tgtype = 21
|
|
15
|
+
AND tgfoid = to_regprocedure('public.capture_graph_foundation_revision()'))
|
|
16
|
+
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_foundation_revisions')
|
|
17
|
+
AND tgname = 'graph_foundation_revisions_guard' AND tgenabled = 'O' AND tgtype = 31
|
|
18
|
+
AND tgfoid = to_regprocedure('public.guard_graph_foundation_revision_mutation()'))
|
|
19
|
+
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('public.graph_foundation_revisions')
|
|
20
|
+
AND tgname = 'graph_foundation_revisions_truncate_guard' AND tgenabled = 'O' AND tgtype = 34
|
|
21
|
+
AND tgfoid = to_regprocedure('public.guard_graph_foundation_revision_mutation()')) AS ready`;
|
|
22
|
+
export const FOUNDATION_GRAPH_CONTRACT_VERSION = 'foundation-graph-http.v1';
|
|
23
|
+
const DEFAULT_BODY_LIMIT_BYTES = 65_536;
|
|
24
|
+
const DIRECT_FOUNDATION_REFERENCE_KINDS = new Set(['objective', 'variable', 'model', 'constraint', 'philosophy']);
|
|
25
|
+
const SNAPSHOT_REFERENCE_KINDS = new Set([
|
|
26
|
+
'entity', 'edge', 'objective', 'criterion', 'variable', 'observation', 'model', 'constraint',
|
|
27
|
+
'philosophy', 'authority', 'resource', 'deadline', 'dag', 'evidence'
|
|
28
|
+
]);
|
|
29
|
+
const FOUNDATION_SCOPE_TYPES = new Set(['personal', 'project', 'organization']);
|
|
30
|
+
function jsonResponse(status, value) {
|
|
31
|
+
return new Response(JSON.stringify(value), {
|
|
32
|
+
status,
|
|
33
|
+
headers: { 'content-type': 'application/json' }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Return true only for the canonical scope shape. Incomplete or extended
|
|
41
|
+
* shapes stay on the shared validator path so it remains the source of
|
|
42
|
+
* malformed-input errors.
|
|
43
|
+
*/
|
|
44
|
+
function isCanonicalFoundationScope(value) {
|
|
45
|
+
return isRecord(value)
|
|
46
|
+
&& Object.keys(value).length === 2
|
|
47
|
+
&& typeof value.type === 'string'
|
|
48
|
+
&& typeof value.id === 'string'
|
|
49
|
+
&& value.type.length > 0
|
|
50
|
+
&& value.id.length > 0
|
|
51
|
+
&& value.type === value.type.trim()
|
|
52
|
+
&& value.id === value.id.trim();
|
|
53
|
+
}
|
|
54
|
+
function isSelectedProjectScope(value, scopeId) {
|
|
55
|
+
return isCanonicalFoundationScope(value) && value.type === 'project' && value.id === scopeId;
|
|
56
|
+
}
|
|
57
|
+
function isAllowedFoundationReference(reference, scopeId, organizationId, knownKinds) {
|
|
58
|
+
if (!isRecord(reference) || !isCanonicalFoundationScope(reference.scope))
|
|
59
|
+
return true;
|
|
60
|
+
// Let the common validator report a missing/non-string kind as malformed;
|
|
61
|
+
// every present non-philosophy kind remains project-scoped.
|
|
62
|
+
if (typeof reference.kind !== 'string' || reference.kind.length === 0 || reference.kind !== reference.kind.trim())
|
|
63
|
+
return true;
|
|
64
|
+
// Unknown kinds and scope types remain the shared validator's responsibility.
|
|
65
|
+
// This boundary only narrows otherwise valid references.
|
|
66
|
+
if (!knownKinds.has(reference.kind) || !FOUNDATION_SCOPE_TYPES.has(reference.scope.type))
|
|
67
|
+
return true;
|
|
68
|
+
return isSelectedProjectScope(reference.scope, scopeId)
|
|
69
|
+
|| (reference.kind === 'philosophy'
|
|
70
|
+
&& reference.scope.type === 'organization'
|
|
71
|
+
&& reference.scope.id === organizationId);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Reject scope widening before the common provider sees a request. The
|
|
75
|
+
* provider receives both the selected project and trusted organization so an
|
|
76
|
+
* organization philosophy can resolve, but all other foundation references
|
|
77
|
+
* remain project-scoped and measurements remain project-only.
|
|
78
|
+
*/
|
|
79
|
+
function hasFoundationScopeOverride(request, body, scopeId, organizationId) {
|
|
80
|
+
const method = (request.method || 'GET').toUpperCase();
|
|
81
|
+
if (!['POST', 'PUT', 'PATCH'].includes(method) || !isRecord(body))
|
|
82
|
+
return false;
|
|
83
|
+
const pathname = new URL(request.url).pathname;
|
|
84
|
+
if (pathname.endsWith('/judgment-references/validate')) {
|
|
85
|
+
return isRecord(body.reference)
|
|
86
|
+
&& !isAllowedFoundationReference(body.reference, scopeId, organizationId, DIRECT_FOUNDATION_REFERENCE_KINDS);
|
|
87
|
+
}
|
|
88
|
+
if (!pathname.endsWith('/judgment-problems/validate'))
|
|
89
|
+
return false;
|
|
90
|
+
const snapshot = body.snapshot;
|
|
91
|
+
if (!isRecord(snapshot))
|
|
92
|
+
return false;
|
|
93
|
+
if (Object.hasOwn(snapshot, 'owner_scope')
|
|
94
|
+
&& isCanonicalFoundationScope(snapshot.owner_scope)
|
|
95
|
+
&& FOUNDATION_SCOPE_TYPES.has(snapshot.owner_scope.type)
|
|
96
|
+
&& !isSelectedProjectScope(snapshot.owner_scope, scopeId)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (!Array.isArray(snapshot.references))
|
|
100
|
+
return false;
|
|
101
|
+
return snapshot.references.some((reference) => {
|
|
102
|
+
if (!isRecord(reference))
|
|
103
|
+
return false;
|
|
104
|
+
if (Object.hasOwn(reference, 'scope')
|
|
105
|
+
&& !isAllowedFoundationReference(reference, scopeId, organizationId, SNAPSHOT_REFERENCE_KINDS)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
const measurement = reference.measurement;
|
|
109
|
+
const measurementScope = isRecord(measurement) ? measurement.scope : undefined;
|
|
110
|
+
const subjectIds = isRecord(measurementScope) ? measurementScope.subjectIds : undefined;
|
|
111
|
+
// Incomplete subjectIds remain the shared validator's responsibility.
|
|
112
|
+
return Array.isArray(subjectIds)
|
|
113
|
+
&& subjectIds.length > 0
|
|
114
|
+
&& subjectIds.every((subjectId) => typeof subjectId === 'string'
|
|
115
|
+
&& subjectId === subjectId.trim()
|
|
116
|
+
&& subjectId.length > 0)
|
|
117
|
+
&& subjectIds.some((subjectId) => subjectId !== scopeId);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function readJsonForScopeCheck(request, bodyLimitBytes) {
|
|
121
|
+
const method = (request.method || 'GET').toUpperCase();
|
|
122
|
+
const pathname = new URL(request.url).pathname;
|
|
123
|
+
if (!['POST', 'PUT', 'PATCH'].includes(method)
|
|
124
|
+
|| (!pathname.endsWith('/judgment-references/validate') && !pathname.endsWith('/judgment-problems/validate'))) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const body = await request.clone().text();
|
|
129
|
+
if (new TextEncoder().encode(body).byteLength > bodyLimitBytes)
|
|
130
|
+
return null;
|
|
131
|
+
return JSON.parse(body);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// The shared route owns malformed JSON errors. A failed preflight must not
|
|
135
|
+
// turn malformed input into an authorization decision.
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function identityIsValid(identity) {
|
|
140
|
+
if (!identity)
|
|
141
|
+
return false;
|
|
142
|
+
return typeof identity.principal === 'string'
|
|
143
|
+
&& identity.principal.trim().length > 0
|
|
144
|
+
&& typeof identity.organizationId === 'string'
|
|
145
|
+
&& identity.organizationId.trim().length > 0
|
|
146
|
+
&& Array.isArray(identity.projectScopeIds);
|
|
147
|
+
}
|
|
148
|
+
function selectedScopeIsAllowed(identity, scopeId) {
|
|
149
|
+
return identity.projectScopeIds.includes('*') || identity.projectScopeIds.includes(scopeId);
|
|
150
|
+
}
|
|
151
|
+
function readyResult(result) {
|
|
152
|
+
return result.rows[0]?.ready === true;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Create the reusable Graph-backed public Foundation handler.
|
|
156
|
+
*
|
|
157
|
+
* This is a native Fetch-style adapter. It has no Express, authentication, or
|
|
158
|
+
* organization dependency: a host injects a trusted identity resolver, an
|
|
159
|
+
* already-authenticated transaction port, and its CSRF verifier.
|
|
160
|
+
*/
|
|
161
|
+
export function createFoundationGraphHttpHandler(options) {
|
|
162
|
+
if (!options || typeof options.resolveTrustedIdentity !== 'function') {
|
|
163
|
+
throw new TypeError('resolveTrustedIdentity is required');
|
|
164
|
+
}
|
|
165
|
+
if (typeof options.withAccessContext !== 'function') {
|
|
166
|
+
throw new TypeError('withAccessContext is required');
|
|
167
|
+
}
|
|
168
|
+
const bodyLimitBytes = options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES;
|
|
169
|
+
if (!Number.isInteger(bodyLimitBytes) || bodyLimitBytes <= 0) {
|
|
170
|
+
throw new TypeError('bodyLimitBytes must be a positive integer');
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
contractVersion: FOUNDATION_HTTP_CONTRACT_VERSION,
|
|
174
|
+
routes: [],
|
|
175
|
+
async handle(request) {
|
|
176
|
+
let identity;
|
|
177
|
+
try {
|
|
178
|
+
identity = await options.resolveTrustedIdentity(request);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
identity = null;
|
|
182
|
+
}
|
|
183
|
+
if (!identityIsValid(identity)) {
|
|
184
|
+
return jsonResponse(403, { error: { code: 'foundation_trusted_context_required' } });
|
|
185
|
+
}
|
|
186
|
+
const url = new URL(request.url);
|
|
187
|
+
const scopeIds = url.searchParams.getAll('scope_id');
|
|
188
|
+
if (scopeIds.length !== 1 || !scopeIds[0]?.trim() || scopeIds[0] !== scopeIds[0].trim()) {
|
|
189
|
+
return jsonResponse(400, { error: { code: 'scope_id_required' } });
|
|
190
|
+
}
|
|
191
|
+
const scopeId = scopeIds[0];
|
|
192
|
+
if (!selectedScopeIsAllowed(identity, scopeId)) {
|
|
193
|
+
return jsonResponse(403, { error: { code: 'scope_not_allowed' } });
|
|
194
|
+
}
|
|
195
|
+
const body = await readJsonForScopeCheck(request, bodyLimitBytes);
|
|
196
|
+
if (hasFoundationScopeOverride(request, body, scopeId, identity.organizationId)) {
|
|
197
|
+
return jsonResponse(403, { error: { code: 'scope_not_allowed' } });
|
|
198
|
+
}
|
|
199
|
+
const context = {
|
|
200
|
+
principal: identity.principal,
|
|
201
|
+
scope: {
|
|
202
|
+
subjectIds: [scopeId, identity.organizationId],
|
|
203
|
+
validFrom: '1970-01-01T00:00:00Z'
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
try {
|
|
207
|
+
const response = await options.withAccessContext(identity, async (client) => {
|
|
208
|
+
let ready;
|
|
209
|
+
try {
|
|
210
|
+
ready = await client.query(FOUNDATION_GRAPH_READY_SQL);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return jsonResponse(503, { error: { code: 'foundation_provider_unavailable' } });
|
|
214
|
+
}
|
|
215
|
+
if (!readyResult(ready)) {
|
|
216
|
+
return jsonResponse(503, { error: { code: 'foundation_migration_required' } });
|
|
217
|
+
}
|
|
218
|
+
const readers = createGraphFoundationReaders({
|
|
219
|
+
context,
|
|
220
|
+
query: async (text, values) => ({
|
|
221
|
+
rows: (await client.query(text, values ?? [])).rows
|
|
222
|
+
})
|
|
223
|
+
});
|
|
224
|
+
const provider = createFoundationPublicProvider(readers);
|
|
225
|
+
const route = createFoundationPublicRoute(provider);
|
|
226
|
+
const handler = createFoundationHttpRouter({
|
|
227
|
+
resolveContext: () => context,
|
|
228
|
+
csrf: options.csrf,
|
|
229
|
+
routes: [route],
|
|
230
|
+
bodyLimitBytes
|
|
231
|
+
});
|
|
232
|
+
return handler.handle(request);
|
|
233
|
+
});
|
|
234
|
+
return response instanceof Response
|
|
235
|
+
? response
|
|
236
|
+
: jsonResponse(503, { error: { code: 'foundation_provider_unavailable' } });
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return jsonResponse(503, { error: { code: 'foundation_provider_unavailable' } });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|