@unson/brainbase-mcp 0.10.15 → 0.12.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.
@@ -154,7 +154,25 @@
154
154
  "entity_id": { "type": "string", "minLength": 1 },
155
155
  "application": { "type": "string", "minLength": 1 },
156
156
  "layer": { "enum": ["philosophy", "objective", "world_model", "method", "constraint", "other"] },
157
- "version": { "type": ["string", "null"] }
157
+ "version": { "type": ["string", "null"] },
158
+ "source": {
159
+ "oneOf": [
160
+ { "type": "null" },
161
+ { "$ref": "#/$defs/basis_source" }
162
+ ]
163
+ }
164
+ }
165
+ },
166
+ "basis_source": {
167
+ "type": "object",
168
+ "additionalProperties": false,
169
+ "required": ["kind", "entity_id", "entity_type"],
170
+ "properties": {
171
+ "kind": { "const": "local_graph" },
172
+ "entity_id": { "type": "string", "minLength": 1 },
173
+ "entity_type": { "enum": ["person", "org", "project", "decision"] },
174
+ "version": { "type": ["string", "null"] },
175
+ "digest": { "type": ["string", "null"] }
158
176
  }
159
177
  },
160
178
  "inheritance_source": {
@@ -1,5 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import { ExecutionAuthorityService, type ExecutionProblemReference } from './execution-authority.js';
2
+ import { ExecutionAuthorityService, type ExecutionAttribution, type ExecutionProblemReference } from './execution-authority.js';
3
3
  type MaybePromise<T> = T | Promise<T>;
4
4
  /** Context resolved by the host after authentication and tenant selection. */
5
5
  export interface TrustedExecutionAuthorityRequestContext {
@@ -8,9 +8,11 @@ export interface TrustedExecutionAuthorityRequestContext {
8
8
  scopeId: string;
9
9
  /** A non-empty value means that the host already verified the mutation origin. */
10
10
  verifiedMutationOrigin?: string;
11
+ /** Resolved by the host from authentication. Request bodies cannot supply it. */
12
+ attribution?: ExecutionAttribution;
11
13
  }
12
14
  /** The factory is the boundary where a host binds a service to its trusted context. */
13
- export type ExecutionAuthorityServiceFactory = (context: TrustedExecutionAuthorityRequestContext) => MaybePromise<Pick<ExecutionAuthorityService, 'start' | 'readIntent'>>;
15
+ export type ExecutionAuthorityServiceFactory = (context: TrustedExecutionAuthorityRequestContext) => MaybePromise<Pick<ExecutionAuthorityService, 'start' | 'readIntent'> & Partial<Pick<ExecutionAuthorityService, 'register' | 'reportEffect'>>>;
14
16
  export interface ExecutionAuthorityMutationVerificationInput {
15
17
  request: IncomingMessage;
16
18
  context: TrustedExecutionAuthorityRequestContext;
@@ -21,6 +21,19 @@ const startRequestSchema = z.object({
21
21
  constraintRefs: z.array(boundedText).max(128),
22
22
  problem: problemSchema,
23
23
  }).strict();
24
+ const attributionSchema = z.object({
25
+ mode: z.enum(['person', 'delegated_service', 'service']),
26
+ servicePrincipal: boundedText.optional(),
27
+ delegationRef: boundedText.optional(),
28
+ correlationId: boundedText.optional(),
29
+ }).strict();
30
+ const effectReportSchema = z.object({
31
+ tenantId: boundedText,
32
+ principal: boundedText,
33
+ scopeId: boundedText,
34
+ status: z.enum(['started', 'unknown']),
35
+ capabilityDigest: boundedText,
36
+ }).strict();
24
37
  class HttpInputError extends Error {
25
38
  constructor(message) {
26
39
  super(message);
@@ -141,16 +154,21 @@ function matchRoute(path, basePath) {
141
154
  const suffix = normalized.slice(prefix.length);
142
155
  if (suffix === 'start')
143
156
  return { operation: 'start' };
157
+ if (suffix === 'register')
158
+ return { operation: 'register' };
144
159
  const intentPrefix = 'intents/';
145
160
  if (suffix.startsWith(intentPrefix)) {
146
- const encodedOperationId = suffix.slice(intentPrefix.length);
161
+ const rest = suffix.slice(intentPrefix.length);
162
+ const reportSuffix = '/effect';
163
+ const operation = rest.endsWith(reportSuffix) ? 'report_effect' : 'read';
164
+ const encodedOperationId = operation === 'report_effect' ? rest.slice(0, -reportSuffix.length) : rest;
147
165
  if (encodedOperationId.length === 0 || encodedOperationId.includes('/'))
148
166
  return { operation: 'unknown' };
149
167
  try {
150
- return { operation: 'read', operationId: decodeURIComponent(encodedOperationId) };
168
+ return { operation, operationId: decodeURIComponent(encodedOperationId) };
151
169
  }
152
170
  catch {
153
- return { operation: 'read', operationId: undefined };
171
+ return { operation, operationId: undefined };
154
172
  }
155
173
  }
156
174
  return { operation: 'unknown' };
@@ -168,6 +186,21 @@ function assertContext(context) {
168
186
  || !boundedText.safeParse(context.scopeId).success) {
169
187
  throw new HttpInputError('Authenticated execution authority context is required');
170
188
  }
189
+ if (context.attribution !== undefined && !attributionIsConsistent(context.attribution, context.principal)) {
190
+ throw new HttpInputError('Authenticated execution attribution is inconsistent');
191
+ }
192
+ }
193
+ /** Mirrors the service rules so an inconsistent host context is an authentication failure. */
194
+ function attributionIsConsistent(value, principal) {
195
+ const parsed = attributionSchema.safeParse(value);
196
+ if (!parsed.success)
197
+ return false;
198
+ const { mode, servicePrincipal, delegationRef } = parsed.data;
199
+ if (mode === 'person')
200
+ return servicePrincipal === undefined && delegationRef === undefined;
201
+ if (servicePrincipal === undefined || delegationRef === undefined)
202
+ return false;
203
+ return mode === 'delegated_service' ? servicePrincipal !== principal : servicePrincipal === principal;
171
204
  }
172
205
  function bindTrustedIdentity(body, context) {
173
206
  if (!isRecord(body))
@@ -201,6 +234,18 @@ function parseStartBody(body, context) {
201
234
  throw new HttpInputError(parsed.error.issues[0]?.message ?? 'Request body is invalid');
202
235
  return parsed.data;
203
236
  }
237
+ function parseEffectReportBody(body, context, operationId) {
238
+ const bound = bindTrustedIdentity(body, context);
239
+ if (hasOwn(bound, 'operationId')) {
240
+ if (bound.operationId !== operationId)
241
+ throw new HttpInputError('operationId does not match the request path');
242
+ delete bound.operationId;
243
+ }
244
+ const parsed = effectReportSchema.safeParse(bound);
245
+ if (!parsed.success)
246
+ throw new HttpInputError(parsed.error.issues[0]?.message ?? 'Request body is invalid');
247
+ return { ...parsed.data, operationId };
248
+ }
204
249
  function serviceError(error) {
205
250
  if (!(error instanceof ExecutionAuthorityError)) {
206
251
  return { status: 500, code: 'internal_error', message: 'Execution authority operation failed' };
@@ -208,6 +253,7 @@ function serviceError(error) {
208
253
  const status = (() => {
209
254
  switch (error.code) {
210
255
  case 'scope_mismatch':
256
+ case 'capability_mismatch':
211
257
  case 'authority_revoked':
212
258
  case 'authority_expired':
213
259
  case 'approval_revoked':
@@ -224,7 +270,12 @@ function serviceError(error) {
224
270
  case 'approval_unknown':
225
271
  case 'constraint_unknown':
226
272
  case 'reservation_unknown':
273
+ case 'store_unavailable':
227
274
  return 503;
275
+ case 'intent_not_found':
276
+ return 404;
277
+ case 'effect_unconfigured':
278
+ return 501;
228
279
  case 'operation_conflict':
229
280
  case 'revision_conflict':
230
281
  case 'fencing_unsupported':
@@ -247,7 +298,9 @@ function writeError(response, status, code, message, headers = {}) {
247
298
  writeJson(response, status, { error: { code, message } }, headers);
248
299
  }
249
300
  function methodAllowed(operation, method) {
250
- return operation === 'read' ? method === 'GET' : operation === 'start' ? method === 'POST' : false;
301
+ if (operation === 'read')
302
+ return method === 'GET';
303
+ return operation === 'unknown' ? false : method === 'POST';
251
304
  }
252
305
  async function verifyMutationRequest(options, request, context) {
253
306
  if (typeof context.verifiedMutationOrigin === 'string' && context.verifiedMutationOrigin.trim().length > 0)
@@ -333,6 +386,38 @@ export function createExecutionAuthorityHttpHandler(options) {
333
386
  }
334
387
  return true;
335
388
  }
389
+ const requestOptions = context.attribution ? { attribution: context.attribution } : {};
390
+ if (route.operation === 'report_effect') {
391
+ const parsedOperationId = boundedText.safeParse(route.operationId);
392
+ let report;
393
+ try {
394
+ if (!parsedOperationId.success)
395
+ throw new HttpInputError('operationId is invalid');
396
+ report = parseEffectReportBody(body, context, parsedOperationId.data);
397
+ }
398
+ catch (error) {
399
+ writeError(response, 400, 'invalid_request', error instanceof Error ? error.message : 'Request body is invalid');
400
+ return true;
401
+ }
402
+ if (!await verifyMutationRequest(options, request, context)) {
403
+ writeError(response, 403, 'mutation_origin_unverified', 'Mutation origin could not be verified');
404
+ return true;
405
+ }
406
+ try {
407
+ const service = await options.serviceFactory(context);
408
+ if (typeof service.reportEffect !== 'function') {
409
+ writeError(response, 501, 'operation_unconfigured', 'This host does not accept effect reports');
410
+ return true;
411
+ }
412
+ const intent = await service.reportEffect(report);
413
+ writeJson(response, 200, { action: 'report_effect', operationId: report.operationId, result: { intent } });
414
+ }
415
+ catch (error) {
416
+ const failure = serviceError(error);
417
+ writeError(response, failure.status, failure.code, failure.message);
418
+ }
419
+ return true;
420
+ }
336
421
  let parsed;
337
422
  try {
338
423
  parsed = parseStartBody(body, context);
@@ -347,8 +432,18 @@ export function createExecutionAuthorityHttpHandler(options) {
347
432
  }
348
433
  try {
349
434
  const service = await options.serviceFactory(context);
350
- const result = await service.start(parsed);
351
- writeJson(response, 200, { action: 'start', operationId: parsed.operationId, result });
435
+ if (route.operation === 'register') {
436
+ if (typeof service.register !== 'function') {
437
+ writeError(response, 501, 'operation_unconfigured', 'This host does not register executions');
438
+ return true;
439
+ }
440
+ const result = await service.register(parsed, requestOptions);
441
+ writeJson(response, 200, { action: 'register', operationId: parsed.operationId, result });
442
+ }
443
+ else {
444
+ const result = await service.start(parsed, requestOptions);
445
+ writeJson(response, 200, { action: 'start', operationId: parsed.operationId, result });
446
+ }
352
447
  }
353
448
  catch (error) {
354
449
  const failure = serviceError(error);
@@ -14,6 +14,23 @@ export interface ExecutionStartInput {
14
14
  /** Evidence reference only. It never grants execution permission. */
15
15
  problem: ExecutionProblemReference;
16
16
  }
17
+ /**
18
+ * Who started the execution, as resolved by the host from authenticated
19
+ * context. Request bodies never supply it.
20
+ */
21
+ export type ExecutionAttributionMode = 'person' | 'delegated_service' | 'service';
22
+ export interface ExecutionAttribution {
23
+ /** person: the principal acted directly. delegated_service: a service acted for the principal. service: an automated job acted on its own authority. */
24
+ mode: ExecutionAttributionMode;
25
+ /** Authenticated service principal. Required unless mode is person. */
26
+ servicePrincipal?: string;
27
+ /** Verified delegation, or for mode service the authority grant that the job relies on. */
28
+ delegationRef?: string;
29
+ correlationId?: string;
30
+ }
31
+ export interface ExecutionRequestOptions {
32
+ attribution?: ExecutionAttribution;
33
+ }
17
34
  export type ExecutionCheckKind = 'authority' | 'approval' | 'constraint' | 'reservation';
18
35
  export type ExecutionVerificationPhase = 'initial' | 'final';
19
36
  export type ExecutionCheckStatus = 'approved' | 'revoked' | 'expired' | 'unknown';
@@ -23,6 +40,8 @@ export interface ExecutionCheckRequest extends ExecutionStartInput {
23
40
  phase: ExecutionVerificationPhase;
24
41
  /** The revision returned by the first check. Providers may fence on it. */
25
42
  expectedRevision?: string;
43
+ /** Present when the host resolved an attribution; providers verify delegation with it. */
44
+ attribution?: ExecutionAttribution;
26
45
  }
27
46
  export interface ExecutionCheckResult {
28
47
  status: ExecutionCheckStatus;
@@ -82,6 +101,7 @@ export interface ExecutionStartCapability {
82
101
  scopeId: string;
83
102
  issuedAt: string;
84
103
  expiresAt: string;
104
+ attribution?: ExecutionAttribution;
85
105
  checks: Record<ExecutionCheckKind, {
86
106
  revision: string;
87
107
  fencingToken: string;
@@ -91,7 +111,8 @@ export interface ExecutionStartCapability {
91
111
  export interface ExecutionEffectResult {
92
112
  status: 'started' | 'unknown';
93
113
  }
94
- export type ExecutionIntentPhase = 'recovery_required' | 'reservation_marked' | 'started' | 'unknown' | 'rejected';
114
+ /** registered: checks passed and a permit was issued; the effect owner has not reported yet. */
115
+ export type ExecutionIntentPhase = 'recovery_required' | 'reservation_marked' | 'registered' | 'started' | 'unknown' | 'rejected';
95
116
  export type ExecutionIntentEffectStatus = 'not_started' | 'started' | 'unknown';
96
117
  export type ExecutionIntentReservationMarkStatus = 'not_started' | 'started' | 'unknown';
97
118
  export interface ExecutionCheckRecord {
@@ -102,7 +123,7 @@ export interface ExecutionCheckRecord {
102
123
  expiresAt?: string;
103
124
  evidence: Record<string, string>;
104
125
  }
105
- export type ExecutionAuthorityErrorCode = 'validation_error' | 'authority_revoked' | 'authority_expired' | 'authority_unknown' | 'approval_revoked' | 'approval_expired' | 'approval_unknown' | 'constraint_revoked' | 'constraint_expired' | 'constraint_unknown' | 'reservation_revoked' | 'reservation_expired' | 'reservation_unknown' | 'read_revoked' | 'read_expired' | 'read_unknown' | 'revision_conflict' | 'fencing_unsupported' | 'capability_expired' | 'operation_conflict' | 'scope_mismatch' | 'recovery_required' | 'reservation_mark_unknown' | 'effect_unknown' | 'ledger_invalid';
126
+ export type ExecutionAuthorityErrorCode = 'validation_error' | 'authority_revoked' | 'authority_expired' | 'authority_unknown' | 'approval_revoked' | 'approval_expired' | 'approval_unknown' | 'constraint_revoked' | 'constraint_expired' | 'constraint_unknown' | 'reservation_revoked' | 'reservation_expired' | 'reservation_unknown' | 'read_revoked' | 'read_expired' | 'read_unknown' | 'revision_conflict' | 'fencing_unsupported' | 'capability_expired' | 'operation_conflict' | 'scope_mismatch' | 'recovery_required' | 'reservation_mark_unknown' | 'effect_unknown' | 'effect_unconfigured' | 'capability_mismatch' | 'intent_not_found' | 'store_unavailable' | 'ledger_invalid';
106
127
  export interface ExecutionIntentFailure {
107
128
  code: ExecutionAuthorityErrorCode;
108
129
  reason: string;
@@ -120,6 +141,7 @@ export interface ExecutionIntentRecord {
120
141
  constraintRefs: string[];
121
142
  problem: ExecutionProblemReference;
122
143
  payloadDigest: string;
144
+ attribution?: ExecutionAttribution;
123
145
  phase: ExecutionIntentPhase;
124
146
  reservationMark: ExecutionIntentReservationMarkStatus;
125
147
  effect: {
@@ -133,6 +155,8 @@ export interface ExecutionIntentRecord {
133
155
  capability?: {
134
156
  expiresAt: string;
135
157
  digest: string;
158
+ /** Stored only for registrations so a lost response can be replayed. Never returned to readers. */
159
+ grant?: ExecutionStartCapability;
136
160
  };
137
161
  failure?: ExecutionIntentFailure;
138
162
  createdAt: string;
@@ -146,14 +170,51 @@ export interface ExecutionStartResult {
146
170
  };
147
171
  replayed: boolean;
148
172
  }
173
+ export interface ExecutionRegistrationResult {
174
+ intent: ExecutionIntentRecord;
175
+ capability: ExecutionStartCapability;
176
+ capabilityDigest: string;
177
+ replayed: boolean;
178
+ }
179
+ export interface ExecutionEffectReport {
180
+ operationId: string;
181
+ tenantId: string;
182
+ principal: string;
183
+ scopeId: string;
184
+ /** Binds the report to the issued permit. It is not a secret. */
185
+ capabilityDigest: string;
186
+ status: 'started' | 'unknown';
187
+ }
188
+ export interface ExecutionIntentKey {
189
+ tenantId: string;
190
+ operationId: string;
191
+ }
192
+ /**
193
+ * Durable intent storage. A host injects a database adapter here; the default
194
+ * is the local sidecar. insert and update must each be atomic per key.
195
+ */
196
+ export interface ExecutionIntentStore {
197
+ initialize?(): Promise<void>;
198
+ read(key: ExecutionIntentKey): Promise<ExecutionIntentRecord | undefined>;
199
+ /** Stores the intent when the key is absent; otherwise returns the existing record with created false. */
200
+ insert(intent: ExecutionIntentRecord): Promise<{
201
+ intent: ExecutionIntentRecord;
202
+ created: boolean;
203
+ }>;
204
+ /** Writes mutate(current) atomically. Returns undefined when absent. A throwing mutate writes nothing and rethrows. */
205
+ update(key: ExecutionIntentKey, mutate: (current: ExecutionIntentRecord) => ExecutionIntentRecord): Promise<ExecutionIntentRecord | undefined>;
206
+ }
149
207
  export interface ExecutionAuthorityServiceOptions {
150
- dataDir: string;
208
+ /** Used for the default sidecar store when store is omitted. */
209
+ dataDir?: string;
210
+ store?: ExecutionIntentStore;
151
211
  authority: ExecutionAuthorityPort;
152
212
  approval: ExecutionApprovalPort;
153
213
  constraint: ExecutionConstraintPort;
154
214
  reservation: ExecutionReservationPort;
155
215
  readAccess: ExecutionReadAccessPort;
156
- effect: ExecutionEffectPort;
216
+ /** Required for start. Registration-only hosts leave the effect to its owner. */
217
+ effect?: ExecutionEffectPort;
157
218
  clock?: () => Date;
158
219
  sidecarPath?: string;
159
220
  }
@@ -163,11 +224,11 @@ export declare class ExecutionAuthorityError extends Error {
163
224
  }
164
225
  /**
165
226
  * Persists the execution intent before crossing either the reservation or
166
- * external-effect boundary. The reservation and intent sidecars are separate
227
+ * external-effect boundary. The reservation and intent stores are separate
167
228
  * transactions; callers must reconcile recovery_required/unknown states.
168
229
  */
169
230
  export declare class ExecutionAuthorityService {
170
- private readonly dataDir;
231
+ private readonly store;
171
232
  private readonly authority;
172
233
  private readonly approval;
173
234
  private readonly constraint;
@@ -175,11 +236,23 @@ export declare class ExecutionAuthorityService {
175
236
  private readonly readAccess;
176
237
  private readonly effect;
177
238
  private readonly clock;
178
- private readonly sidecarPath;
179
239
  constructor(options: ExecutionAuthorityServiceOptions);
180
240
  initialize(): Promise<void>;
181
- start(input: ExecutionStartInput): Promise<ExecutionStartResult>;
241
+ /** Runs the checks, marks the reservation, and calls the in-process effect port. */
242
+ start(input: ExecutionStartInput, options?: ExecutionRequestOptions): Promise<ExecutionStartResult>;
243
+ /**
244
+ * Registers the execution: runs the checks, marks the reservation, and
245
+ * issues the permit. The effect owner reports the outcome with reportEffect.
246
+ */
247
+ register(input: ExecutionStartInput, options?: ExecutionRequestOptions): Promise<ExecutionRegistrationResult>;
248
+ /** Records the effect owner's outcome for a registered execution. */
249
+ reportEffect(report: ExecutionEffectReport): Promise<ExecutionIntentRecord>;
182
250
  readIntent(input: Pick<ExecutionStartInput, 'operationId' | 'tenantId' | 'principal' | 'scopeId'>): Promise<ExecutionIntentRecord | undefined>;
251
+ /**
252
+ * Shared path for start and register: initial checks, durable intent,
253
+ * fenced final checks, permit, and the reservation mark.
254
+ */
255
+ private prepare;
183
256
  private findIntent;
184
257
  private verifyReadAccess;
185
258
  private recordIntent;
@@ -188,8 +261,16 @@ export declare class ExecutionAuthorityService {
188
261
  private persistRecovery;
189
262
  private verifyAll;
190
263
  private handleExisting;
264
+ private replayRegistration;
265
+ /** Store failures are never success and never switch to another ledger. */
266
+ private storeCall;
191
267
  private now;
192
268
  }
269
+ /** The default store: a JSON sidecar under the local SSOT lock. */
270
+ export declare function createSidecarExecutionIntentStore(options: {
271
+ dataDir: string;
272
+ sidecarPath?: string;
273
+ }): ExecutionIntentStore;
193
274
  /**
194
275
  * Binds the existing reservation store to the execution port without calling
195
276
  * it from inside the execution sidecar transaction. The store remains the