@unson/brainbase-mcp 0.13.0 → 0.15.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.
@@ -53,4 +53,14 @@ export type FoundationPublicToolDependencies = FoundationAuthenticatedToolDepend
53
53
  export declare const foundationPublicTools: Tool[];
54
54
  export declare function isFoundationPublicToolName(name: string): name is FoundationPublicToolName;
55
55
  export declare function handleFoundationPublicToolCall(name: string, args: Record<string, unknown>, dependencies: FoundationPublicToolDependencies): Promise<FoundationAuthenticatedToolResult | null>;
56
+ /**
57
+ * brainbase-project ADR-015: adopt a Foundation draft for judgment and
58
+ * evaluation through the canonical API. The caller passes the revision and
59
+ * digest they reviewed (for example with foundation_read); the API refuses a
60
+ * draft that changed since, the draft's own owner, a caller without a gm or
61
+ * above grant for the project, and a definition not ready for judgment.
62
+ */
63
+ export declare const foundationAdoptionTools: Tool[];
64
+ export declare function isFoundationAdoptionToolName(name: string): boolean;
65
+ export declare function handleFoundationAdoptionToolCall(name: string, args: Record<string, unknown>, dependencies: FoundationAuthenticatedToolDependencies): Promise<FoundationAuthenticatedToolResult | null>;
56
66
  export {};
@@ -345,3 +345,89 @@ export async function handleFoundationPublicToolCall(name, args, dependencies) {
345
345
  }
346
346
  return { status: 'ok', scope: { project_codes: context.scope }, data: fetched.payload };
347
347
  }
348
+ const FOUNDATION_ADOPTION_TYPES = new Set(['objective', 'variable', 'model', 'constraint']);
349
+ const FOUNDATION_ADOPTION_PATH = '/api/company-os/foundation-adoptions';
350
+ /**
351
+ * brainbase-project ADR-015: adopt a Foundation draft for judgment and
352
+ * evaluation through the canonical API. The caller passes the revision and
353
+ * digest they reviewed (for example with foundation_read); the API refuses a
354
+ * draft that changed since, the draft's own owner, a caller without a gm or
355
+ * above grant for the project, and a definition not ready for judgment.
356
+ */
357
+ export const foundationAdoptionTools = [{
358
+ name: 'foundation_adopt',
359
+ description: [
360
+ 'Adopt a Foundation draft (objective, variable, model or constraint) so it can be used for judgment and evaluation, never execution.',
361
+ 'Read the draft first with foundation_read and pass the exact revision and digest you reviewed; the content is adopted unchanged as the next revision.',
362
+ 'Only a gm or above of the project who is not the owner of the draft can adopt. This is a write: confirm with the person before calling it.',
363
+ ].join(' '),
364
+ inputSchema: {
365
+ type: 'object',
366
+ properties: {
367
+ scope_id: { type: 'string', minLength: 1, maxLength: MAX_SCOPE_ID_LENGTH, description: 'Project code the draft belongs to.' },
368
+ type: { type: 'string', enum: ['objective', 'variable', 'model', 'constraint'] },
369
+ id: { type: 'string', minLength: 1, maxLength: 256 },
370
+ revision: { type: 'string', pattern: '^[1-9][0-9]*$', description: 'The draft revision you reviewed.' },
371
+ digest: { type: 'string', pattern: '^sha256:[0-9a-f]{64}$', description: 'The digest of the reviewed revision, as foundation_read returned it.' },
372
+ },
373
+ required: ['scope_id', 'type', 'id', 'revision', 'digest'],
374
+ additionalProperties: false,
375
+ },
376
+ }];
377
+ export function isFoundationAdoptionToolName(name) {
378
+ return name === 'foundation_adopt';
379
+ }
380
+ function isValidAdoptionPayload(args, payload) {
381
+ if (!isRecord(payload) || payload.action !== 'adopt' || !isRecord(payload.result))
382
+ return false;
383
+ const result = payload.result;
384
+ return result.id === args.id
385
+ && result.type === args.type
386
+ && result.revision === String(Number(args.revision) + 1)
387
+ && isNonEmptyString(result.digest)
388
+ && DIGEST_PATTERN.test(result.digest);
389
+ }
390
+ export async function handleFoundationAdoptionToolCall(name, args, dependencies) {
391
+ if (!isFoundationAdoptionToolName(name))
392
+ return null;
393
+ const scopeId = readScopeId(args);
394
+ if (!scopeId)
395
+ return invalidInput('scope_id is required and must be a safe project identifier', dependencies);
396
+ const unexpected = rejectUnexpectedKeys(args, ['scope_id', 'type', 'id', 'revision', 'digest'], dependencies);
397
+ if (unexpected)
398
+ return unexpected;
399
+ const type = readRequiredString(args, 'type');
400
+ const id = readRequiredString(args, 'id');
401
+ const revision = readRequiredString(args, 'revision');
402
+ const digest = readRequiredString(args, 'digest');
403
+ if (!type || !FOUNDATION_ADOPTION_TYPES.has(type))
404
+ return invalidInput('foundation type must be objective, variable, model or constraint', dependencies);
405
+ if (!id)
406
+ return invalidInput('foundation id is required', dependencies);
407
+ if (!revision || !REVISION_PATTERN.test(revision))
408
+ return invalidInput('foundation revision is invalid', dependencies);
409
+ if (!digest || !DIGEST_PATTERN.test(digest))
410
+ return invalidInput('foundation digest is invalid', dependencies);
411
+ const authenticated = await dependencies.auth.authenticateProject({ project_code: scopeId }, { requireProject: true });
412
+ if ('status' in authenticated)
413
+ return authenticated;
414
+ const context = selectedContext(authenticated, scopeId);
415
+ const csrf = await fetchFoundationCsrfToken(dependencies, context);
416
+ if (!csrf.ok)
417
+ return csrf.result;
418
+ const fetched = await dependencies.auth.fetchAuthenticatedJson(context, {
419
+ path: FOUNDATION_ADOPTION_PATH,
420
+ method: 'POST',
421
+ body: { id, type, revision, digest },
422
+ headers: { 'x-brainbase-scope': scopeId, 'x-session-id': csrf.sessionId, 'x-csrf-token': csrf.token },
423
+ });
424
+ if (!fetched.ok)
425
+ return fetched.result;
426
+ if (!fetched.response.ok)
427
+ return errorFromResponse(fetched.response, fetched.payload, context.scope, dependencies);
428
+ const normalizedArgs = { id, type, revision };
429
+ if (!fetched.payloadParsed || !isValidAdoptionPayload(normalizedArgs, fetched.payload)) {
430
+ return dependencies.auth.toolError('error', 'foundation_api_response_invalid', 'Brainbase API returned an invalid adoption response', context.scope, fetched.response.status);
431
+ }
432
+ return { status: 'ok', scope: { project_codes: context.scope }, data: fetched.payload };
433
+ }
@@ -8,8 +8,9 @@ import type { FoundationDefinition, FoundationScope, FoundationType } from './on
8
8
  * principal, selected project, and current revision.
9
9
  */
10
10
  export declare const FOUNDATION_GRAPH_WRITE_CONTRACT_VERSION: "foundation-graph-draft-write.v1";
11
+ export declare const FOUNDATION_GRAPH_ADOPTION_CONTRACT_VERSION: "foundation-graph-adoption.v1";
11
12
  export type FoundationGraphWriteOperation = 'create' | 'update';
12
- export type FoundationGraphWriteIssueCode = 'INVALID_INPUT' | 'INVALID_IDENTITY' | 'ROW_DEFINITION_MISMATCH' | 'REVISION_CONFLICT' | 'CURRENT_DEFINITION_REQUIRED' | 'DEFINITION_INVALID' | 'DRAFT_ADOPTION_REQUIRED' | 'CANDIDATE_STORAGE_REQUIRED' | 'DRAFT_AUTHORIZATION_REQUIRED' | 'PROVENANCE_REQUIRED' | 'OWNER_MISMATCH' | 'ACL_CHANGE_FORBIDDEN' | 'WRITER_NOT_AUTHORIZED' | 'SCOPE_VIOLATION' | 'PROJECT_MISMATCH';
13
+ export type FoundationGraphWriteIssueCode = 'INVALID_INPUT' | 'INVALID_IDENTITY' | 'ROW_DEFINITION_MISMATCH' | 'REVISION_CONFLICT' | 'CURRENT_DEFINITION_REQUIRED' | 'DEFINITION_INVALID' | 'DRAFT_ADOPTION_REQUIRED' | 'CANDIDATE_STORAGE_REQUIRED' | 'DRAFT_AUTHORIZATION_REQUIRED' | 'PROVENANCE_REQUIRED' | 'OWNER_MISMATCH' | 'ACL_CHANGE_FORBIDDEN' | 'WRITER_NOT_AUTHORIZED' | 'SCOPE_VIOLATION' | 'PROJECT_MISMATCH' | 'ADOPTION_TARGET_CHANGED' | 'ADOPTER_IS_OWNER' | 'ALREADY_ADOPTED' | 'DEFINITION_NOT_READY' | 'AUTHORITY_REQUIRED';
13
14
  export interface FoundationGraphWriteIssue {
14
15
  readonly code: FoundationGraphWriteIssueCode;
15
16
  readonly path: string;
@@ -88,3 +89,62 @@ export declare function validateFoundationGraphWrite(input: unknown): Foundation
88
89
  export declare function normalizeFoundationGraphWrite(input: unknown): FoundationGraphWriteResult;
89
90
  /** Descriptive alias for callers that use the validation/normalization pair. */
90
91
  export declare const validateAndNormalizeFoundationGraphWrite: typeof normalizeFoundationGraphWrite;
92
+ /**
93
+ * ADR-015: adopt the current Foundation draft for judgment and evaluation.
94
+ *
95
+ * The adopter names the revision and digest they reviewed. The contract only
96
+ * succeeds when that is the current Graph revision, the adopter is not the
97
+ * draft's owner, and the unchanged definition passes the judgment and
98
+ * evaluation checks. The adopted definition is the next revision with the same
99
+ * content; only the adoption state, authorized uses, storage class and an
100
+ * appended decision provenance entry change. Whether the adopter holds the
101
+ * required organization authority is decided by the host before this call and
102
+ * passed in as `adoption.authorityRef`.
103
+ */
104
+ export interface FoundationGraphAdoptionInput {
105
+ readonly context: FoundationGraphWriteContext;
106
+ /** The current Graph row, as read under the adopter's access in the same transaction. */
107
+ readonly row: FoundationGraphWriteRow;
108
+ /** What the adopter reviewed. */
109
+ readonly expected: {
110
+ readonly revision: string;
111
+ readonly digest: string;
112
+ };
113
+ readonly adoption: {
114
+ readonly authorityRef: string;
115
+ readonly adoptedAt: string;
116
+ };
117
+ }
118
+ export interface FoundationGraphAdoptionRecord {
119
+ readonly adoptedBy: string;
120
+ readonly authorityRef: string;
121
+ readonly adoptedRevision: string;
122
+ readonly adoptedDigest: string;
123
+ readonly adoptedAt: string;
124
+ }
125
+ export interface FoundationGraphAdoptionResult {
126
+ readonly contractVersion: typeof FOUNDATION_GRAPH_ADOPTION_CONTRACT_VERSION;
127
+ readonly operation: 'adopt';
128
+ readonly projectCode: string;
129
+ readonly row: {
130
+ readonly id: string;
131
+ readonly type: FoundationType;
132
+ readonly revision: string;
133
+ readonly projectCode: string;
134
+ readonly payload: {
135
+ readonly foundation: FoundationDefinition;
136
+ readonly adoption: FoundationGraphAdoptionRecord;
137
+ };
138
+ };
139
+ readonly definition: FoundationDefinition;
140
+ readonly adoption: FoundationGraphAdoptionRecord;
141
+ }
142
+ export interface FoundationGraphAdoptionValidationResult {
143
+ readonly valid: boolean;
144
+ readonly status: 'valid' | 'invalid';
145
+ readonly issues: readonly FoundationGraphWriteIssue[];
146
+ readonly normalized?: FoundationGraphAdoptionResult;
147
+ }
148
+ export declare function validateFoundationGraphAdoption(input: unknown): FoundationGraphAdoptionValidationResult;
149
+ /** Normalize a valid adoption or throw with the complete validation issues. */
150
+ export declare function normalizeFoundationGraphAdoption(input: unknown): FoundationGraphAdoptionResult;
@@ -1,4 +1,4 @@
1
- import { cloneFoundationDefinition, nextFoundationRevision } from './foundation-catalog.js';
1
+ import { cloneFoundationDefinition, digestFoundationDefinition, nextFoundationRevision } from './foundation-catalog.js';
2
2
  import { validateFoundationDefinition } from './ontology-foundation.js';
3
3
  /**
4
4
  * Contract for preparing a Foundation draft for the canonical Graph writer.
@@ -12,6 +12,10 @@ export const FOUNDATION_GRAPH_WRITE_CONTRACT_VERSION = 'foundation-graph-draft-w
12
12
  const FOUNDATION_TYPES = ['objective', 'variable', 'model', 'constraint'];
13
13
  const REVISION_PATTERN = /^[1-9]\d*$/u;
14
14
  const FOUNDATION_USES_DRAFT = ['draft'];
15
+ /** ADR-015 F2: an adopted definition may be used for judgment and evaluation, never execution. */
16
+ const FOUNDATION_USES_ADOPTED = ['draft', 'judgment', 'evaluation'];
17
+ const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
18
+ export const FOUNDATION_GRAPH_ADOPTION_CONTRACT_VERSION = 'foundation-graph-adoption.v1';
15
19
  /**
16
20
  * Error thrown by the throwing normalizer. The non-throwing validator should
17
21
  * be used at HTTP/MCP boundaries when callers need to render all issues.
@@ -207,6 +211,117 @@ export function normalizeFoundationGraphWrite(input) {
207
211
  }
208
212
  /** Descriptive alias for callers that use the validation/normalization pair. */
209
213
  export const validateAndNormalizeFoundationGraphWrite = normalizeFoundationGraphWrite;
214
+ export function validateFoundationGraphAdoption(input) {
215
+ const issues = [];
216
+ const fail = () => ({ valid: false, status: 'invalid', issues });
217
+ if (!isRecord(input) || !isRecord(input.context) || !isRecord(input.row) || !isRecord(input.expected) || !isRecord(input.adoption)) {
218
+ issues.push({ code: 'INVALID_INPUT', path: 'input', message: 'Adoption needs context, row, expected and adoption objects.' });
219
+ return fail();
220
+ }
221
+ const context = input.context;
222
+ const row = input.row;
223
+ const expected = input.expected;
224
+ const adoption = input.adoption;
225
+ validateContext(context, issues);
226
+ validateGraphRow(row, issues);
227
+ if (isNonEmptyString(row.projectCode) && isNonEmptyString(context.projectCode) && row.projectCode !== context.projectCode) {
228
+ issues.push({ code: 'PROJECT_MISMATCH', path: 'row.projectCode', message: 'Graph row projectCode must match the trusted selected project.' });
229
+ }
230
+ if (!isCanonicalRevision(expected.revision) || typeof expected.digest !== 'string' || !DIGEST_PATTERN.test(expected.digest)) {
231
+ issues.push({ code: 'INVALID_INPUT', path: 'expected', message: 'expected needs the reviewed revision and its sha256 digest.' });
232
+ }
233
+ if (!isNonEmptyString(adoption.authorityRef)) {
234
+ issues.push({ code: 'AUTHORITY_REQUIRED', path: 'adoption.authorityRef', message: 'Adoption requires the evidence of the adopter\'s organization authority.' });
235
+ }
236
+ if (typeof adoption.adoptedAt !== 'string' || !isRfc3339(adoption.adoptedAt)) {
237
+ issues.push({ code: 'INVALID_INPUT', path: 'adoption.adoptedAt', message: 'adoption.adoptedAt must be an RFC 3339 timestamp.' });
238
+ }
239
+ if (issues.length > 0)
240
+ return fail();
241
+ const rowType = resolveRowType(row);
242
+ const current = unwrapFoundation(row.payload.foundation);
243
+ const currentResult = validateFoundationDefinition(current, { use: 'draft' });
244
+ appendDefinitionIssues(issues, 'row.payload.foundation', currentResult.issues);
245
+ if (!currentResult.valid || !isFoundationDefinitionRecord(current) || !rowType)
246
+ return fail();
247
+ if (current.id !== row.id || current.type !== rowType || current.revision !== row.revision) {
248
+ issues.push({ code: 'ROW_DEFINITION_MISMATCH', path: 'row.payload.foundation', message: 'The current definition must match the Graph row identity and revision.' });
249
+ return fail();
250
+ }
251
+ const currentDefinition = current;
252
+ const currentDigest = digestFoundationDefinition(currentDefinition);
253
+ if (expected.revision !== current.revision || expected.digest !== currentDigest) {
254
+ issues.push({
255
+ code: 'ADOPTION_TARGET_CHANGED',
256
+ path: 'expected',
257
+ message: `The reviewed revision ${String(expected.revision)} is not the current revision ${current.revision} with the same content.`
258
+ });
259
+ }
260
+ if (current.adoptionState !== 'draft') {
261
+ issues.push({ code: 'ALREADY_ADOPTED', path: 'row.payload.foundation.adoptionState', message: `Only a draft can be adopted; the current revision is ${String(current.adoptionState)}.` });
262
+ }
263
+ if (current.acl.ownerId === context.principal) {
264
+ issues.push({ code: 'ADOPTER_IS_OWNER', path: 'context.principal', message: 'The owner of a draft cannot adopt it.' });
265
+ }
266
+ validateCandidateScope(current, context, issues);
267
+ if (issues.length > 0)
268
+ return fail();
269
+ let revision;
270
+ try {
271
+ revision = nextFoundationRevision(current.revision);
272
+ }
273
+ catch {
274
+ issues.push({ code: 'REVISION_CONFLICT', path: 'row.revision', message: 'The next revision cannot be derived from the current revision.' });
275
+ return fail();
276
+ }
277
+ const authorityRef = String(adoption.authorityRef);
278
+ const adopted = cloneFoundationDefinition(currentDefinition);
279
+ adopted.revision = revision;
280
+ adopted.adoptionState = 'approved';
281
+ adopted.storage = 'ontology';
282
+ adopted.authorizedUses = [...FOUNDATION_USES_ADOPTED];
283
+ adopted.provenance = [
284
+ ...adopted.provenance,
285
+ { sourceId: `foundation-adoption:${current.id}@${current.revision}`, sourceKind: 'decision', evidenceIds: [authorityRef] }
286
+ ];
287
+ for (const use of ['judgment', 'evaluation']) {
288
+ const ready = validateFoundationDefinition(adopted, { use });
289
+ if (!ready.valid) {
290
+ for (const issue of ready.issues) {
291
+ issues.push({ code: 'DEFINITION_NOT_READY', path: `row.payload.foundation.${issue.path}`, message: `Not ready for ${use}: ${issue.message}` });
292
+ }
293
+ }
294
+ }
295
+ if (issues.length > 0)
296
+ return fail();
297
+ const record = {
298
+ adoptedBy: context.principal,
299
+ authorityRef,
300
+ adoptedRevision: current.revision,
301
+ adoptedDigest: currentDigest,
302
+ adoptedAt: String(adoption.adoptedAt)
303
+ };
304
+ return {
305
+ valid: true,
306
+ status: 'valid',
307
+ issues: [],
308
+ normalized: {
309
+ contractVersion: FOUNDATION_GRAPH_ADOPTION_CONTRACT_VERSION,
310
+ operation: 'adopt',
311
+ projectCode: context.projectCode,
312
+ row: { id: current.id, type: rowType, revision, projectCode: context.projectCode, payload: { foundation: adopted, adoption: record } },
313
+ definition: adopted,
314
+ adoption: record
315
+ }
316
+ };
317
+ }
318
+ /** Normalize a valid adoption or throw with the complete validation issues. */
319
+ export function normalizeFoundationGraphAdoption(input) {
320
+ const result = validateFoundationGraphAdoption(input);
321
+ if (!result.valid || !result.normalized)
322
+ throw new FoundationGraphWriteError(result.issues);
323
+ return result.normalized;
324
+ }
210
325
  function validateContext(context, issues) {
211
326
  if (!isNonEmptyString(context.principal)) {
212
327
  issues.push({ code: 'INVALID_INPUT', path: 'context.principal', message: 'context.principal must be a non-empty trusted identifier.' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -352,5 +352,5 @@
352
352
  "engines": {
353
353
  "node": ">=20"
354
354
  },
355
- "gitHead": "c9b9adbff2dd409f77ed868d9c3939b3269f2d4f"
355
+ "gitHead": "2d242e478ef24937acfaf02363995d445974ed8e"
356
356
  }