brainclaw 1.14.0 → 1.16.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.
Files changed (63) hide show
  1. package/README.md +16 -263
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-capture.js +209 -0
  4. package/dist/cli/register-code-map.js +19 -0
  5. package/dist/cli/register-coordination.js +472 -0
  6. package/dist/cli/register-federation.js +258 -0
  7. package/dist/cli/register-lifecycle.js +436 -0
  8. package/dist/cli/register-memory-context.js +502 -0
  9. package/dist/cli/register-planning.js +167 -0
  10. package/dist/cli/register-review.js +149 -0
  11. package/dist/cli/shared.js +5 -0
  12. package/dist/cli.js +212 -2015
  13. package/dist/commands/dispatch-watch.js +25 -2
  14. package/dist/commands/harvest.js +31 -6
  15. package/dist/commands/mcp-catalog.js +1438 -0
  16. package/dist/commands/mcp-contract.js +33 -0
  17. package/dist/commands/mcp-presentation.js +27 -0
  18. package/dist/commands/mcp-read-handlers.js +72 -36
  19. package/dist/commands/mcp-write-admin.js +328 -0
  20. package/dist/commands/mcp-write-claims.js +864 -0
  21. package/dist/commands/mcp-write-coordination.js +1825 -0
  22. package/dist/commands/mcp-write-entities.js +620 -0
  23. package/dist/commands/mcp-write-memory.js +451 -0
  24. package/dist/commands/mcp-write-sequences.js +116 -0
  25. package/dist/commands/mcp-write-support.js +367 -0
  26. package/dist/commands/mcp.js +261 -5570
  27. package/dist/commands/update-handoff.js +28 -42
  28. package/dist/core/agent-capability.js +31 -14
  29. package/dist/core/agent-files.js +1 -1
  30. package/dist/core/agent-registry.js +51 -3
  31. package/dist/core/claims.js +18 -0
  32. package/dist/core/coordination.js +5 -2
  33. package/dist/core/cross-project.js +35 -1
  34. package/dist/core/dispatcher.js +34 -20
  35. package/dist/core/entity-operations.js +335 -12
  36. package/dist/core/entity-registry.js +72 -9
  37. package/dist/core/execution.js +28 -4
  38. package/dist/core/facade-schema.js +30 -4
  39. package/dist/core/federation-cloud.js +142 -11
  40. package/dist/core/federation-outbox.js +292 -0
  41. package/dist/core/federation-signing.js +115 -0
  42. package/dist/core/handoff-review.js +35 -0
  43. package/dist/core/io.js +6 -0
  44. package/dist/core/protocol-tool-policy.js +113 -0
  45. package/dist/core/review-loop-close.js +115 -0
  46. package/dist/core/schema.js +25 -2
  47. package/dist/core/security-detectors.js +35 -6
  48. package/dist/core/security.js +32 -12
  49. package/dist/core/worktree.js +98 -9
  50. package/dist/facts.js +13 -11
  51. package/dist/facts.json +12 -10
  52. package/docs/PROTOCOL.md +7 -3
  53. package/docs/concepts/coordinator-runbook.md +3 -0
  54. package/docs/concepts/dispatch-lifecycle.md +4 -4
  55. package/docs/concepts/loop-engine.md +3 -1
  56. package/docs/concepts/troubleshooting.md +1 -1
  57. package/docs/integrations/codex.md +3 -3
  58. package/docs/integrations/overview.md +1 -1
  59. package/docs/mcp-schema-changelog.md +153 -2
  60. package/docs/playbooks/orchestration.md +1 -1
  61. package/docs/product/entity-model-audit.md +3 -2
  62. package/docs/security.md +22 -1
  63. package/package.json +3 -1
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Shared MCP write-path support helpers.
3
+ *
4
+ * Extracted mechanically from mcp.ts (pln#622 PR3a): identity resolution and
5
+ * trust gating shared by every write-tool domain (coordination, sequences,
6
+ * steps, canonical grammar, …). mcp.ts re-exports the historical test surface
7
+ * (`__resetConnectionPrincipalForTests`, `PinnedConnectionPrincipal`).
8
+ *
9
+ * Import rule (pln#622 PR1 guard): this module must never import ./mcp.js.
10
+ *
11
+ * @module
12
+ */
13
+ import { AgentIdentityResolutionError, AgentTrustError, findAgentIdentityById, findAgentIdentityByName, normalizeAgentName, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveCurrentAgentIdentity, resolveOrAutoRegisterAgentIdentity, } from '../core/agent-registry.js';
14
+ import { loadClaim } from '../core/claims.js';
15
+ import { loadConfig } from '../core/config.js';
16
+ import { isObserverMode } from '../core/observer-mode.js';
17
+ import { buildOperationalIdentity, loadCurrentSession, loadSessionById, saveCurrentSession } from '../core/identity.js';
18
+ import { scanText } from '../core/security.js';
19
+ import { toolResponse } from './mcp-contract.js';
20
+ let principalCache;
21
+ /** Test hook — the principal is otherwise pinned for the process lifetime. */
22
+ export function __resetConnectionPrincipalForTests() {
23
+ principalCache = undefined;
24
+ }
25
+ /**
26
+ * Resolve the connection principal. The MCP server is one process per
27
+ * connection, so a process-level pin IS the per-connection pin; the cache key
28
+ * guards the identity-bearing env vars so in-process test harnesses that
29
+ * switch agents between calls re-resolve instead of leaking the first pin.
30
+ */
31
+ export function resolveConnectionPrincipal(cwd, sessionId) {
32
+ const env = process.env;
33
+ const key = [
34
+ env.BRAINCLAW_CLAIM_ID ?? '', env.BRAINCLAW_AGENT_ID ?? '',
35
+ env.BRAINCLAW_AGENT_NAME ?? '', env.BRAINCLAW_AGENT ?? '',
36
+ cwd ?? '', sessionId ?? '',
37
+ ].join('|');
38
+ if (principalCache && principalCache.key === key)
39
+ return principalCache.value;
40
+ let value;
41
+ // 1. Assignment binding: a dispatched worker carries BRAINCLAW_CLAIM_ID; the
42
+ // claim names the identity the coordinator dispatched — authoritative.
43
+ const claimId = env.BRAINCLAW_CLAIM_ID?.trim();
44
+ if (claimId) {
45
+ try {
46
+ const claim = loadClaim(claimId, cwd);
47
+ const identity = (claim.agent_id ? findAgentIdentityById(claim.agent_id, cwd) : undefined)
48
+ ?? findAgentIdentityByName(claim.agent, cwd);
49
+ if (identity) {
50
+ value = {
51
+ agent_name: identity.agent_name,
52
+ agent_id: identity.agent_id,
53
+ session_id: claim.session_id ?? sessionId,
54
+ pid: process.pid,
55
+ source: 'claim_binding',
56
+ };
57
+ }
58
+ }
59
+ catch { /* claim may not exist in this store — fall through */ }
60
+ }
61
+ // 2. Server-side detection (env-pinned or detected REGISTERED identity —
62
+ // read-only since pln#562 step 2, never mints).
63
+ if (!value) {
64
+ const identity = resolveCurrentAgentIdentity(cwd);
65
+ if (identity) {
66
+ value = {
67
+ agent_name: identity.agent_name,
68
+ agent_id: identity.agent_id,
69
+ session_id: sessionId,
70
+ pid: process.pid,
71
+ source: 'server_detection',
72
+ };
73
+ }
74
+ }
75
+ principalCache = { key, value };
76
+ return value;
77
+ }
78
+ export function resolveMutationIdentity(args, fields, cwd, sessionId) {
79
+ try {
80
+ const explicitName = typeof args[fields.nameField] === 'string' ? String(args[fields.nameField]) : undefined;
81
+ const explicitId = typeof args[fields.idField] === 'string' ? String(args[fields.idField]) : undefined;
82
+ // pln#562 step 3 — pinned connection principal. When the server resolved
83
+ // an authenticated principal, caller args are verified against it:
84
+ // matching/absent args → principal; mismatching args → curator-only
85
+ // explicit override, otherwise the mismatch is REJECTED loudly. Silently
86
+ // re-attributing a spoofed/mistaken identity to the principal would hide
87
+ // caller bugs — fail-loud is the contract (mcp-protocol.test 'rejects
88
+ // unregistered identities and mismatched id/name pairs').
89
+ const principal = resolveConnectionPrincipal(cwd, sessionId);
90
+ if (principal) {
91
+ // Re-load per call (cheap) so trust changes propagate mid-connection;
92
+ // the BINDING (who you are) stays pinned.
93
+ const principalDoc = findAgentIdentityById(principal.agent_id, cwd);
94
+ if (principalDoc) {
95
+ const mismatch = (explicitName !== undefined && normalizeAgentName(explicitName) !== normalizeAgentName(principal.agent_name))
96
+ || (explicitId !== undefined && explicitId !== principal.agent_id);
97
+ if (!mismatch) {
98
+ return { identity: principalDoc };
99
+ }
100
+ if ((principalDoc.trust_level ?? 'contributor') === 'curator') {
101
+ return {
102
+ identity: requireRegisteredAgentIdentity({
103
+ agentName: explicitName,
104
+ agentId: explicitId,
105
+ cwd,
106
+ allowCurrent: true,
107
+ allowEnv: true,
108
+ }),
109
+ };
110
+ }
111
+ return {
112
+ error: {
113
+ kind: 'identity_error',
114
+ message: `Caller-supplied identity (agent=${explicitName ?? '<none>'}, agentId=${explicitId ?? '<none>'}) does not match the pinned connection principal '${principal.agent_name}' (${principal.agent_id}). Omit the identity args, or have a curator perform the override.`,
115
+ },
116
+ };
117
+ }
118
+ }
119
+ // No pinned principal (unregistered connection): legacy chain.
120
+ // Session-pinned identity: if no explicit agent in args, use the session's pinned agent
121
+ let agentName = explicitName;
122
+ if (!agentName && sessionId) {
123
+ const session = loadSessionById(sessionId, cwd);
124
+ if (session?.agent) {
125
+ agentName = session.agent;
126
+ }
127
+ }
128
+ return {
129
+ identity: requireRegisteredAgentIdentity({
130
+ agentName,
131
+ agentId: explicitId,
132
+ cwd,
133
+ allowCurrent: true,
134
+ allowEnv: true,
135
+ }),
136
+ };
137
+ }
138
+ catch (error) {
139
+ if (error instanceof AgentIdentityResolutionError) {
140
+ return {
141
+ error: {
142
+ kind: error.kind,
143
+ message: error.message,
144
+ details: error.details,
145
+ },
146
+ };
147
+ }
148
+ return {
149
+ error: {
150
+ kind: 'identity_error',
151
+ message: error instanceof Error ? error.message : String(error),
152
+ },
153
+ };
154
+ }
155
+ }
156
+ export function ensureTrust(args, fields, level, cwd, sessionId) {
157
+ const resolved = resolveMutationIdentity(args, fields, cwd, sessionId);
158
+ if ('error' in resolved) {
159
+ return resolved;
160
+ }
161
+ try {
162
+ requireMinimumTrustLevel(resolved.identity, level);
163
+ return resolved;
164
+ }
165
+ catch (error) {
166
+ if (error instanceof AgentTrustError) {
167
+ return {
168
+ error: {
169
+ kind: error.kind,
170
+ message: error.message,
171
+ details: error.details,
172
+ },
173
+ };
174
+ }
175
+ return {
176
+ error: {
177
+ kind: 'trust_error',
178
+ message: error instanceof Error ? error.message : String(error),
179
+ },
180
+ };
181
+ }
182
+ }
183
+ export function explicitSessionIdFromEnv() {
184
+ return process.env.BRAINCLAW_SESSION_ID?.trim()
185
+ || process.env.OPENCLAW_SESSION_ID?.trim()
186
+ || process.env.CLAUDE_SESSION_ID?.trim()
187
+ || process.env.COPILOT_SESSION_ID?.trim();
188
+ }
189
+ export function projectInfoForCwd(cwd) {
190
+ try {
191
+ const config = loadConfig(cwd);
192
+ return { path: cwd, name: config.project_name };
193
+ }
194
+ catch {
195
+ return { path: cwd };
196
+ }
197
+ }
198
+ /**
199
+ * Resolve the agent identity for canonical-grammar mutation verbs
200
+ * (bclaw_create/update/remove/transition), so handlers can auto-fill required
201
+ * fields (e.g. plan.author) instead of letting the create land on disk with a
202
+ * missing field — which would then be silently GC'd by the state sync loop
203
+ * (see fix plan pln_5f44426c).
204
+ *
205
+ * pln#562 step 3 — a write that would create a record with a missing/'unknown'
206
+ * author must never be silent (that produced records that passed creation but
207
+ * were schema-invalid on read and silently GC'd from disk).
208
+ *
209
+ * pln#608 — extended with auto-repair: when the caller has no session but a
210
+ * derivable agent name (arg / $BRAINCLAW_AGENT_NAME / detected AI agent),
211
+ * fall through to `resolveOrAutoRegisterAgentIdentity` and materialize the
212
+ * session via `buildOperationalIdentity({ persistImplicitSession: true })`
213
+ * (same mechanic as switchProject:86-106 and session-start). The freshly-
214
+ * created session is tagged `auto_created` so aggressive harvesting can
215
+ * distinguish it from operator sessions (pln#602). The caller receives
216
+ * `auto_repair` and surfaces it as a warning — never silent.
217
+ *
218
+ * KEEP (still a hard error, doctrine boundary): the identity is ambiguous
219
+ * (no name in args, no env signal, no detectable agent). We do not invent
220
+ * an identity — invoke intent is unclear and the write would misattribute.
221
+ */
222
+ export function resolveCanonicalAuthor(args, cwd, connectionSessionId) {
223
+ const resolved = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
224
+ if ('identity' in resolved && resolved.identity) {
225
+ return {
226
+ agent_name: resolved.identity.agent_name,
227
+ agent_id: resolved.identity.agent_id,
228
+ };
229
+ }
230
+ const strictError = 'error' in resolved && resolved.error ? resolved.error : undefined;
231
+ // KEEP (doctrine boundary): a pinned principal that rejected the caller args
232
+ // is a SPOOF/MISMATCH, not an ambiguous first-write. Never auto-repair over
233
+ // it — silently re-attributing would defeat pln#562 step 3. The strict error
234
+ // already carries the pointer to a curator override.
235
+ if (resolveConnectionPrincipal(cwd, connectionSessionId)) {
236
+ throw new Error(`cannot resolve mutation author: ${strictError?.message ?? 'principal mismatch'}`);
237
+ }
238
+ // Observer processes are read-only dashboards/inspectors. Even when an env
239
+ // variable leaks an agent name into the observer process, canonical writes
240
+ // must not use the auto-repair path because it can mint identity/session
241
+ // state as a side effect.
242
+ if (isObserverMode()) {
243
+ throw new Error(`cannot resolve mutation author: ${strictError?.message ?? 'observer mode cannot auto-repair identity/session state'}`);
244
+ }
245
+ const explicitName = typeof args.agent === 'string' ? args.agent : undefined;
246
+ const explicitId = typeof args.agentId === 'string' ? args.agentId : undefined;
247
+ // resolveOrAutoRegisterAgentIdentity's fall-through helper only reads
248
+ // BRAINCLAW_AGENT / OPENCLAW_AGENT. resolveCurrentAgentIdentity also honors
249
+ // BRAINCLAW_AGENT_NAME, and dispatched workers set both. Normalize here so
250
+ // an env-declared name is a first-class signal to the auto-repair path.
251
+ const envAgentName = explicitName
252
+ ?? (process.env.BRAINCLAW_AGENT_NAME?.trim() || undefined)
253
+ ?? (process.env.BRAINCLAW_AGENT?.trim() || undefined);
254
+ let identity;
255
+ let autoRegistered;
256
+ try {
257
+ const outcome = resolveOrAutoRegisterAgentIdentity({
258
+ agentName: envAgentName,
259
+ agentId: explicitId,
260
+ cwd,
261
+ allowCurrent: true,
262
+ allowEnv: true,
263
+ });
264
+ identity = outcome.identity;
265
+ autoRegistered = outcome.auto_registered;
266
+ }
267
+ catch (err) {
268
+ // Genuine ambiguity — no derivable name. Stays a hard error (KEEP: doctrine
269
+ // boundary is "ambiguous intent → refuse with next_action", not silence).
270
+ const detail = err instanceof Error ? err.message : (strictError?.message ?? String(err));
271
+ throw new Error(`cannot resolve mutation author: ${detail} `
272
+ + 'Pass a registered agent, set $BRAINCLAW_AGENT_NAME, '
273
+ + 'or register with `brainclaw register-agent <name>` before writing.', { cause: err });
274
+ }
275
+ const explicitSessionId = connectionSessionId?.trim() || explicitSessionIdFromEnv();
276
+ const hadSessionBefore = explicitSessionId
277
+ ? Boolean(loadSessionById(explicitSessionId, cwd))
278
+ : Boolean(loadCurrentSession(cwd));
279
+ let sessionAutoCreated;
280
+ try {
281
+ const opIdentity = buildOperationalIdentity(identity.agent_name, cwd, {
282
+ agentId: identity.agent_id,
283
+ sessionId: explicitSessionId,
284
+ persistImplicitSession: true,
285
+ });
286
+ if (!hadSessionBefore && opIdentity.session_id) {
287
+ sessionAutoCreated = opIdentity.session_id;
288
+ const session = loadSessionById(opIdentity.session_id, cwd);
289
+ if (session && !session.auto_created) {
290
+ saveCurrentSession({ ...session, auto_created: true }, cwd);
291
+ }
292
+ }
293
+ }
294
+ catch { /* best-effort — write can still proceed without a persisted session */ }
295
+ const autoRepair = (autoRegistered || sessionAutoCreated)
296
+ ? {
297
+ ...(autoRegistered ? { agent_auto_registered: true } : {}),
298
+ ...(sessionAutoCreated ? { session_auto_created: sessionAutoCreated } : {}),
299
+ }
300
+ : undefined;
301
+ return {
302
+ agent_name: identity.agent_name,
303
+ agent_id: identity.agent_id,
304
+ ...(autoRepair ? { auto_repair: autoRepair } : {}),
305
+ };
306
+ }
307
+ export function renderAutoRepairWarning(auto_repair, agent_name) {
308
+ const parts = [];
309
+ if (auto_repair.agent_auto_registered) {
310
+ parts.push(`agent '${agent_name}' auto-registered (first use). Run \`brainclaw register-agent ${agent_name}\` to set capabilities and trust level.`);
311
+ }
312
+ if (auto_repair.session_auto_created) {
313
+ parts.push(`session ${auto_repair.session_auto_created} auto-created for this write.`);
314
+ }
315
+ return `⚠️ auto-repair: ${parts.join(' ')}`;
316
+ }
317
+ export function scopeMetadataForTarget(args, targetCwd, effectiveScope) {
318
+ const hasExplicitProject = typeof args.project === 'string' && args.project.trim().length > 0;
319
+ return {
320
+ resolved_project: projectInfoForCwd(targetCwd),
321
+ active_source: hasExplicitProject ? 'explicit' : effectiveScope.active_source,
322
+ };
323
+ }
324
+ export function scanMcpWriteText(text, cwd) {
325
+ if (!text)
326
+ return { warnings: [] };
327
+ let config;
328
+ try {
329
+ config = loadConfig(cwd);
330
+ }
331
+ catch {
332
+ return { warnings: [] }; // no store/config to scan against — nothing to enforce
333
+ }
334
+ const found = scanText(text, config);
335
+ if (found.length === 0)
336
+ return { warnings: [] };
337
+ const warnings = found.map((w) => w.message);
338
+ if (found.some((w) => w.level === 'block')) {
339
+ return {
340
+ warnings,
341
+ blockResponse: toolResponse({
342
+ content: [{
343
+ type: 'text',
344
+ text: 'Blocked: strict redaction is enabled and the text contains sensitive content. Nothing was written.\n'
345
+ + warnings.map((m) => `⚠ ${m}`).join('\n'),
346
+ }],
347
+ structuredContent: { error: 'security_block', blocked_by: 'strict_redaction', security_warnings: warnings },
348
+ }, true),
349
+ };
350
+ }
351
+ return { warnings };
352
+ }
353
+ /**
354
+ * Append warn-level security warnings to a successful write response so the MCP
355
+ * caller sees them (parity with the CLI's printed warnings). No-op when there
356
+ * are no warnings, so responses for clean text stay byte-identical.
357
+ */
358
+ export function appendSecurityWarnings(response, warnings) {
359
+ if (warnings.length === 0)
360
+ return response;
361
+ return {
362
+ ...response,
363
+ content: [...response.content, { type: 'text', text: warnings.map((m) => `⚠ security: ${m}`).join('\n') }],
364
+ structuredContent: { ...(response.structuredContent ?? {}), security_warnings: warnings },
365
+ };
366
+ }
367
+ //# sourceMappingURL=mcp-write-support.js.map