@animalabs/connectome-host 0.7.4 → 0.8.1

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 (67) hide show
  1. package/.env.example +12 -5
  2. package/.github/PULL_REQUEST_TEMPLATE.md +3 -2
  3. package/.github/workflows/changelog.yml +9 -4
  4. package/.github/workflows/ci.yml +5 -3
  5. package/.github/workflows/publish.yml +12 -6
  6. package/CHANGELOG.md +320 -0
  7. package/CONTRIBUTING.md +47 -19
  8. package/README.md +27 -0
  9. package/bun.lock +26 -32
  10. package/changelog.d/README.md +28 -0
  11. package/package.json +6 -6
  12. package/recipes/SETUP.md +11 -5
  13. package/recipes/TRIUMVIRATE-SETUP.md +68 -14
  14. package/recipes/knowledge-miner.json +0 -30
  15. package/recipes/mock-test.json +19 -0
  16. package/recipes/triumvirate.json +6 -1
  17. package/scripts/release-changelog.ts +210 -21
  18. package/src/cache-keepalive-log.ts +41 -0
  19. package/src/commands.ts +221 -32
  20. package/src/framework-agent-config.ts +3 -0
  21. package/src/framework-strategy.ts +42 -0
  22. package/src/gate-telemetry.ts +134 -0
  23. package/src/headless.ts +10 -0
  24. package/src/index.ts +194 -55
  25. package/src/mcpl-config.ts +99 -1
  26. package/src/modules/identity-module.ts +310 -2
  27. package/src/modules/instructions-module.ts +265 -0
  28. package/src/modules/mcpl-admin-module.ts +58 -11
  29. package/src/modules/subagent-module.ts +18 -0
  30. package/src/modules/web-ui-module.ts +32 -4
  31. package/src/recipe.ts +821 -25
  32. package/src/web/panel-data.ts +44 -1
  33. package/src/workspace-mounts.ts +73 -0
  34. package/test/audit-module-optins.test.ts +10 -3
  35. package/test/cache-keepalive-log.test.ts +83 -0
  36. package/test/commands-qa-family.test.ts +239 -0
  37. package/test/conversations-recipe.test.ts +142 -0
  38. package/test/count-tokens-model.test.ts +31 -0
  39. package/test/framework-fkm-composition.test.ts +35 -3
  40. package/test/framework-strategy-defaults.test.ts +60 -0
  41. package/test/gate-telemetry-adapter.test.ts +84 -0
  42. package/test/gate-telemetry.test.ts +124 -0
  43. package/test/identity-and-surfaces.test.ts +212 -1
  44. package/test/instructions-module.test.ts +258 -0
  45. package/test/mcpl-admin-module.test.ts +41 -0
  46. package/test/mcpl-agent-overlay.test.ts +51 -3
  47. package/test/mcpl-child-env.test.ts +64 -0
  48. package/test/nudge-command.test.ts +47 -0
  49. package/test/recipe-cache-keepalive.test.ts +59 -0
  50. package/test/recipe-compression-fallback.test.ts +19 -0
  51. package/test/recipe-hybrid-prose-routing.test.ts +12 -0
  52. package/test/recipe-instructions.test.ts +176 -0
  53. package/test/recipe-kv-unified.test.ts +87 -0
  54. package/test/recipe-mcp-source.test.ts +54 -0
  55. package/test/recipe-openai-compatible.test.ts +54 -0
  56. package/test/recipe-path-resolution.test.ts +19 -8
  57. package/test/recipe-provider.test.ts +14 -0
  58. package/test/recipe-save-unresolved.test.ts +244 -0
  59. package/test/recipe-source-only.test.ts +38 -0
  60. package/test/release-changelog.test.ts +202 -0
  61. package/test/subagent-prose-routing.test.ts +109 -0
  62. package/test/subconscious-recipe.test.ts +86 -0
  63. package/test/tool-wrapper-prose-guard-recipe.test.ts +37 -0
  64. package/test/web-ui-module.test.ts +41 -0
  65. package/test/workspace-mounts.test.ts +68 -0
  66. package/web/src/App.tsx +10 -0
  67. package/web/src/Health.tsx +61 -1
@@ -120,7 +120,8 @@ export class McplAdminModule implements Module {
120
120
  description:
121
121
  'List all MCPL servers: connection/retry state, whether policy was established, ' +
122
122
  'the effective grant, masked/denied capability paths, host-command authority, ' +
123
- 'tool count, target, and config source.',
123
+ 'validated manifest revision/fetch/negotiation freshness, tool count, target, ' +
124
+ 'and config source.',
124
125
  inputSchema: { type: 'object', properties: {} },
125
126
  },
126
127
  {
@@ -130,7 +131,14 @@ export class McplAdminModule implements Module {
130
131
  'restarts) and hot-connect it now — its tools become available immediately. ' +
131
132
  'If a server with this id is already running it is restarted with the new ' +
132
133
  'config. Provide either `command` (stdio, spawned as the host user) or `url` ' +
133
- '(WebSocket). Relative ./ args resolve against the host working directory.',
134
+ '(WebSocket). Relative ./ args resolve against the host working directory. ' +
135
+ 'Sensible defaults: omit (or pass empty) the list fields and every feature ' +
136
+ 'set and tool the server offers is available; an empty array means ' +
137
+ '"unspecified", never deny-all (deny-all is disabledTools/' +
138
+ 'disabledFeatureSets: ["*"]). Self-deployed servers get channels + tools ' +
139
+ 'only — consequential capabilities (context hooks around your inference, ' +
140
+ 'server-initiated inference, inference lifecycle) are host-masked; a server ' +
141
+ 'that genuinely needs one is an operator conversation, not a deploy flag.',
134
142
  inputSchema: {
135
143
  type: 'object',
136
144
  properties: {
@@ -142,10 +150,10 @@ export class McplAdminModule implements Module {
142
150
  token: { type: 'string', description: 'Bearer token for WebSocket auth (only when the operator hands you one — prefer `access`).' },
143
151
  access: { type: 'string', description: 'Name of a host-managed access grant (e.g. "eidoverse"): the host attaches your standing credentials to the connection automatically. Nothing for you to obtain or handle.' },
144
152
  toolPrefix: { type: 'string', description: 'Tool namespace prefix. Default: mcpl--<id>.' },
145
- reconnect: { type: 'boolean', description: 'Auto-reconnect on transport failure (default false). Note: does NOT respawn a crashed child — use mcpl_restart for that.' },
146
- enabledFeatureSets: { type: 'array', items: { type: 'string' } },
147
- disabledFeatureSets: { type: 'array', items: { type: 'string' } },
148
- enabledTools: { type: 'array', items: { type: 'string' }, description: 'Tool allow-list (bare names, * wildcard).' },
153
+ reconnect: { type: 'boolean', description: 'Auto-reconnect on transport failure. Default: true for websocket URLs (a bounced server comes back on its own), false for stdio. Note: does NOT respawn a crashed child — use mcpl_restart for that.' },
154
+ enabledFeatureSets: { type: 'array', items: { type: 'string' }, description: 'Feature-set allowlist (* wildcard). Omit or pass [] for all offered.' },
155
+ disabledFeatureSets: { type: 'array', items: { type: 'string' }, description: 'Feature-set deny-list; wins over enabled.' },
156
+ enabledTools: { type: 'array', items: { type: 'string' }, description: 'Tool allow-list (bare names, * wildcard). Omit or pass [] for all offered.' },
149
157
  disabledTools: { type: 'array', items: { type: 'string' }, description: 'Tool deny-list; wins over enabledTools.' },
150
158
  },
151
159
  required: ['id'],
@@ -239,6 +247,11 @@ export class McplAdminModule implements Module {
239
247
  maskedCapabilities?: string[];
240
248
  deniedCapabilities?: string[];
241
249
  allowHostCommands?: boolean;
250
+ manifestState?: {
251
+ lastValidatedRevision: string | null;
252
+ lastFetchedAt: number | null;
253
+ lastNegotiatedAt: number | null;
254
+ };
242
255
  }
243
256
  >;
244
257
  const overlay = readAgentOverlay(this.overlayPath);
@@ -262,7 +275,8 @@ export class McplAdminModule implements Module {
262
275
  `grant=${formatCapabilityList(s.effectiveGrant)}, ` +
263
276
  `masked=${formatCapabilityList(s.maskedCapabilities)}, ` +
264
277
  `denied=${formatCapabilityList(s.deniedCapabilities)}, ` +
265
- `hostCommands=${hostCommands}; ${s.toolCount} tools, ` +
278
+ `hostCommands=${hostCommands}, ` +
279
+ `manifest=${formatManifestState(s.manifestState)}; ${s.toolCount} tools, ` +
266
280
  `prefix=${s.toolPrefix}, source=${source}, ${target}`,
267
281
  );
268
282
  }
@@ -314,10 +328,17 @@ export class McplAdminModule implements Module {
314
328
  }
315
329
  if (typeof input.toolPrefix === 'string') entry.toolPrefix = input.toolPrefix;
316
330
  if (typeof input.reconnect === 'boolean') entry.reconnect = input.reconnect;
317
- if (Array.isArray(input.enabledFeatureSets)) entry.enabledFeatureSets = input.enabledFeatureSets.map(String);
318
- if (Array.isArray(input.disabledFeatureSets)) entry.disabledFeatureSets = input.disabledFeatureSets.map(String);
319
- if (Array.isArray(input.enabledTools)) entry.enabledTools = input.enabledTools.map(String);
320
- if (Array.isArray(input.disabledTools)) entry.disabledTools = input.disabledTools.map(String);
331
+ // Empty arrays are NOT persisted: OpenAI-style strict function calling
332
+ // forces every schema property, so callers emit `[]` meaning
333
+ // "unspecified" and a persisted empty ALLOWLIST is deny-all under the
334
+ // §5.3 pin (Mica's silently eventless eidoverse, 2026-08-04).
335
+ // resolveOverlayEntry drops them at read time too; this keeps the file
336
+ // itself from carrying the trap. Deny-all is spelled ["*"] on the
337
+ // deny-lists.
338
+ if (Array.isArray(input.enabledFeatureSets) && input.enabledFeatureSets.length) entry.enabledFeatureSets = input.enabledFeatureSets.map(String);
339
+ if (Array.isArray(input.disabledFeatureSets) && input.disabledFeatureSets.length) entry.disabledFeatureSets = input.disabledFeatureSets.map(String);
340
+ if (Array.isArray(input.enabledTools) && input.enabledTools.length) entry.enabledTools = input.enabledTools.map(String);
341
+ if (Array.isArray(input.disabledTools) && input.disabledTools.length) entry.disabledTools = input.disabledTools.map(String);
321
342
 
322
343
  // Persist to the overlay first — a connect failure still leaves the entry
323
344
  // in place so the agent can fix the server and mcpl_restart it.
@@ -407,3 +428,29 @@ function formatCapabilityList(paths: string[] | undefined): string {
407
428
  if (paths === undefined) return 'unknown';
408
429
  return `[${paths.join(',')}]`;
409
430
  }
431
+
432
+ function formatManifestState(state: {
433
+ lastValidatedRevision: string | null;
434
+ lastFetchedAt: number | null;
435
+ lastNegotiatedAt: number | null;
436
+ } | undefined): string {
437
+ if (state === undefined) return 'unknown';
438
+ return `{revision=${formatManifestRevision(state.lastValidatedRevision)},` +
439
+ `fetchedAt=${formatManifestTimestamp(state.lastFetchedAt)},` +
440
+ `negotiatedAt=${formatManifestTimestamp(state.lastNegotiatedAt)}}`;
441
+ }
442
+
443
+ /** Bound and quote the server-authored, equality-only revision before putting
444
+ * it on a model-facing text surface. Conforming revisions fit well below the
445
+ * limit; malformed peers cannot inject control lines or unbounded text. */
446
+ function formatManifestRevision(revision: string | null): string {
447
+ if (revision === null) return 'none';
448
+ const bounded = revision.length <= 64 ? revision : `${revision.slice(0, 61)}...`;
449
+ return JSON.stringify(bounded);
450
+ }
451
+
452
+ function formatManifestTimestamp(timestamp: number | null): string {
453
+ if (timestamp === null) return 'none';
454
+ const date = new Date(timestamp);
455
+ return Number.isNaN(date.getTime()) ? 'invalid' : date.toISOString();
456
+ }
@@ -1376,6 +1376,18 @@ export class SubagentModule implements Module {
1376
1376
  * 3. parent agent's `maxTokens` (by default, subagents inherit their caller's budget)
1377
1377
  * 4. last-resort framework fallback (4096) — only reached if there's no parent at all
1378
1378
  */
1379
+ /** Ephemeral agents inherit the caller's prose-routing mode. AF's Agent
1380
+ * defaults to 'locus' (ambient locus capture publishes plain prose to the
1381
+ * open channel), so a resident running proseRouting 'disabled' would still
1382
+ * spawn divers whose between-tool-calls prose leaks into its live channel
1383
+ * as parent speech — field-confirmed 2026-08-26. Falls back to the
1384
+ * configured parent agent, then to AF's own default. */
1385
+ private resolveProseRouting(callerAgentName?: string): 'locus' | 'explicit' | 'hybrid' | 'disabled' | undefined {
1386
+ const parentName = callerAgentName ?? this.config.parentAgentName;
1387
+ if (!parentName) return undefined;
1388
+ return this.framework?.getAgent(parentName)?.proseRouting;
1389
+ }
1390
+
1379
1391
  private resolveMaxTokens(callMaxTokens: number | undefined, parentAgentName?: string): number {
1380
1392
  if (callMaxTokens !== undefined) return callMaxTokens;
1381
1393
  if (this.config.defaultMaxTokens !== undefined) return this.config.defaultMaxTokens;
@@ -1703,6 +1715,9 @@ export class SubagentModule implements Module {
1703
1715
  model,
1704
1716
  systemPrompt: input.systemPrompt,
1705
1717
  maxTokens: this.resolveMaxTokens(input.maxTokens, _callerAgentName),
1718
+ ...(this.resolveProseRouting(_callerAgentName) !== undefined
1719
+ ? { proseRouting: this.resolveProseRouting(_callerAgentName) }
1720
+ : {}),
1706
1721
  maxStreamTokens: 500_000,
1707
1722
  strategy: new KnowledgeStrategy({
1708
1723
  headWindowTokens: 2_000,
@@ -1855,6 +1870,9 @@ export class SubagentModule implements Module {
1855
1870
  model,
1856
1871
  systemPrompt,
1857
1872
  maxTokens: this.resolveMaxTokens(input.maxTokens, callerAgentName),
1873
+ ...(this.resolveProseRouting(callerAgentName) !== undefined
1874
+ ? { proseRouting: this.resolveProseRouting(callerAgentName) }
1875
+ : {}),
1858
1876
  maxStreamTokens: 500_000,
1859
1877
  strategy: new KnowledgeStrategy({
1860
1878
  headWindowTokens: 2_000,
@@ -902,6 +902,15 @@ export class WebUiModule implements Module {
902
902
 
903
903
  private async handleHttp(req: Request, server: ReturnType<typeof Bun.serve>): Promise<Response> {
904
904
  const url = new URL(req.url);
905
+
906
+ // Every HTTP route here is read-only — mutation happens over the WS.
907
+ // Wrong methods used to fall through to the same handlers (a POST
908
+ // /debug/context behaved exactly like the GET), which lies to API
909
+ // consumers probing the surface.
910
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
911
+ return new Response('Method Not Allowed', { status: 405, headers: { allow: 'GET, HEAD' } });
912
+ }
913
+
905
914
  const isRetrievalTraceRoute = url.pathname === '/debug/retrieval'
906
915
  || url.pathname === '/debug/retrieval/view';
907
916
 
@@ -1073,9 +1082,20 @@ export class WebUiModule implements Module {
1073
1082
  return this.serveWorkspaceFile(url.pathname.slice('/files/'.length));
1074
1083
  }
1075
1084
 
1076
- // Static SPA
1085
+ // API namespace exhausted: anything still unmatched under /debug/ is a
1086
+ // typo, wrong casing, or trailing slash — an honest 404 beats the SPA
1087
+ // shell with a 200, which API consumers can't tell from data. (This was
1088
+ // also the mechanism that swallowed real failures like the tokenizer
1089
+ // 404 — the client saw 200/HTML instead of the error.)
1090
+ if (url.pathname.startsWith('/debug/')) {
1091
+ return Response.json({ error: `Unknown debug route: ${url.pathname}` }, { status: 404 });
1092
+ }
1093
+
1094
+ // Static SPA. Bundle assets get real 404s: falling back to index.html
1095
+ // for a missing /assets/*.js serves HTML where the browser expects JS —
1096
+ // a blank page with a MIME error instead of a diagnosable miss.
1077
1097
  const requested = url.pathname === '/' ? '/index.html' : url.pathname;
1078
- return this.serveStatic(requested);
1098
+ return this.serveStatic(requested, { spaFallback: !url.pathname.startsWith('/assets/') });
1079
1099
  }
1080
1100
 
1081
1101
  /** The minimal app slice the shared panel-data layer needs, or null before
@@ -1281,7 +1301,11 @@ export class WebUiModule implements Module {
1281
1301
  }
1282
1302
  }
1283
1303
 
1284
- private async serveStatic(requestedPath: string): Promise<Response> {
1304
+ private async serveStatic(
1305
+ requestedPath: string,
1306
+ opts: { spaFallback?: boolean } = {},
1307
+ ): Promise<Response> {
1308
+ const spaFallback = opts.spaFallback ?? true;
1285
1309
  // Path containment: resolve and verify the result is still under staticRoot.
1286
1310
  // Plain startsWith without a separator is unsafe — both `<root>` and
1287
1311
  // `<root>-evil/...` pass `startsWith('<root>')`. The current callers
@@ -1297,11 +1321,15 @@ export class WebUiModule implements Module {
1297
1321
  try {
1298
1322
  const s = await stat(safePath);
1299
1323
  if (s.isDirectory()) {
1300
- return this.serveStatic(join(requestedPath, 'index.html'));
1324
+ return this.serveStatic(join(requestedPath, 'index.html'), opts);
1301
1325
  }
1302
1326
  const data = await readFile(safePath);
1303
1327
  return new Response(data, { headers: { 'content-type': mimeFor(safePath) } });
1304
1328
  } catch {
1329
+ // Missing bundle assets are honest misses, not SPA routes.
1330
+ if (!spaFallback) {
1331
+ return new Response('Not Found', { status: 404 });
1332
+ }
1305
1333
  // Fall back to index.html so the SPA can handle client-side routing.
1306
1334
  try {
1307
1335
  const indexPath = join(sharedServer!.staticRoot, 'index.html');