@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.
- package/.env.example +12 -5
- package/.github/PULL_REQUEST_TEMPLATE.md +3 -2
- package/.github/workflows/changelog.yml +9 -4
- package/.github/workflows/ci.yml +5 -3
- package/.github/workflows/publish.yml +12 -6
- package/CHANGELOG.md +320 -0
- package/CONTRIBUTING.md +47 -19
- package/README.md +27 -0
- package/bun.lock +26 -32
- package/changelog.d/README.md +28 -0
- package/package.json +6 -6
- package/recipes/SETUP.md +11 -5
- package/recipes/TRIUMVIRATE-SETUP.md +68 -14
- package/recipes/knowledge-miner.json +0 -30
- package/recipes/mock-test.json +19 -0
- package/recipes/triumvirate.json +6 -1
- package/scripts/release-changelog.ts +210 -21
- package/src/cache-keepalive-log.ts +41 -0
- package/src/commands.ts +221 -32
- package/src/framework-agent-config.ts +3 -0
- package/src/framework-strategy.ts +42 -0
- package/src/gate-telemetry.ts +134 -0
- package/src/headless.ts +10 -0
- package/src/index.ts +194 -55
- package/src/mcpl-config.ts +99 -1
- package/src/modules/identity-module.ts +310 -2
- package/src/modules/instructions-module.ts +265 -0
- package/src/modules/mcpl-admin-module.ts +58 -11
- package/src/modules/subagent-module.ts +18 -0
- package/src/modules/web-ui-module.ts +32 -4
- package/src/recipe.ts +821 -25
- package/src/web/panel-data.ts +44 -1
- package/src/workspace-mounts.ts +73 -0
- package/test/audit-module-optins.test.ts +10 -3
- package/test/cache-keepalive-log.test.ts +83 -0
- package/test/commands-qa-family.test.ts +239 -0
- package/test/conversations-recipe.test.ts +142 -0
- package/test/count-tokens-model.test.ts +31 -0
- package/test/framework-fkm-composition.test.ts +35 -3
- package/test/framework-strategy-defaults.test.ts +60 -0
- package/test/gate-telemetry-adapter.test.ts +84 -0
- package/test/gate-telemetry.test.ts +124 -0
- package/test/identity-and-surfaces.test.ts +212 -1
- package/test/instructions-module.test.ts +258 -0
- package/test/mcpl-admin-module.test.ts +41 -0
- package/test/mcpl-agent-overlay.test.ts +51 -3
- package/test/mcpl-child-env.test.ts +64 -0
- package/test/nudge-command.test.ts +47 -0
- package/test/recipe-cache-keepalive.test.ts +59 -0
- package/test/recipe-compression-fallback.test.ts +19 -0
- package/test/recipe-hybrid-prose-routing.test.ts +12 -0
- package/test/recipe-instructions.test.ts +176 -0
- package/test/recipe-kv-unified.test.ts +87 -0
- package/test/recipe-mcp-source.test.ts +54 -0
- package/test/recipe-openai-compatible.test.ts +54 -0
- package/test/recipe-path-resolution.test.ts +19 -8
- package/test/recipe-provider.test.ts +14 -0
- package/test/recipe-save-unresolved.test.ts +244 -0
- package/test/recipe-source-only.test.ts +38 -0
- package/test/release-changelog.test.ts +202 -0
- package/test/subagent-prose-routing.test.ts +109 -0
- package/test/subconscious-recipe.test.ts +86 -0
- package/test/tool-wrapper-prose-guard-recipe.test.ts +37 -0
- package/test/web-ui-module.test.ts +41 -0
- package/test/workspace-mounts.test.ts +68 -0
- package/web/src/App.tsx +10 -0
- 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,
|
|
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 (
|
|
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}
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
//
|
|
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(
|
|
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');
|