@eir-labs/coltrane 0.5.0 → 0.6.2
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/README.md +33 -4
- package/dist/src/bifrost_invoker.js +5 -3
- package/dist/src/bifrost_invoker.js.map +1 -1
- package/dist/src/claude_invoker.d.ts +20 -3
- package/dist/src/claude_invoker.js +30 -5
- package/dist/src/claude_invoker.js.map +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/conduct_daemon.d.ts +59 -0
- package/dist/src/conduct_daemon.js +178 -0
- package/dist/src/conduct_daemon.js.map +1 -0
- package/dist/src/genome_schema.d.ts +366 -0
- package/dist/src/genome_schema.js +129 -0
- package/dist/src/genome_schema.js.map +1 -1
- package/dist/src/genome_store.d.ts +29 -0
- package/dist/src/genome_store.js +326 -0
- package/dist/src/genome_store.js.map +1 -0
- package/dist/src/hosted_tools.d.ts +23 -0
- package/dist/src/hosted_tools.js +170 -0
- package/dist/src/hosted_tools.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp.js +10 -1
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/output_mirror.d.ts +70 -0
- package/dist/src/output_mirror.js +381 -0
- package/dist/src/output_mirror.js.map +1 -0
- package/dist/src/outputs.d.ts +2 -0
- package/dist/src/outputs.js +37 -9
- package/dist/src/outputs.js.map +1 -1
- package/dist/src/play_worker.d.ts +24 -0
- package/dist/src/play_worker.js +49 -0
- package/dist/src/play_worker.js.map +1 -0
- package/dist/src/registry.d.ts +2 -0
- package/dist/src/registry.js +63 -33
- package/dist/src/registry.js.map +1 -1
- package/dist/src/runtime.js +19 -1
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/seal_drill.d.ts +32 -0
- package/dist/src/seal_drill.js +138 -0
- package/dist/src/seal_drill.js.map +1 -0
- package/dist/src/server.d.ts +28 -1
- package/dist/src/server.js +243 -16
- package/dist/src/server.js.map +1 -1
- package/dist/src/server_relay.d.ts +2 -0
- package/dist/src/server_relay.js +31 -8
- package/dist/src/server_relay.js.map +1 -1
- package/dist/src/skill_subprocess.d.ts +9 -17
- package/dist/src/skill_subprocess.js +39 -4
- package/dist/src/skill_subprocess.js.map +1 -1
- package/dist/src/supabase_genome.d.ts +28 -0
- package/dist/src/supabase_genome.js +49 -0
- package/dist/src/supabase_genome.js.map +1 -0
- package/dist/src/tool_surface.d.ts +5 -0
- package/dist/src/tool_surface.js +15 -0
- package/dist/src/tool_surface.js.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +11 -7
- package/dist/src/version.js.map +1 -1
- package/package.json +22 -7
package/dist/src/server.js
CHANGED
|
@@ -9,12 +9,14 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
9
9
|
import { MCP_TOOLS, requiresApproval, AGENT_STATUS_ORDER, STANDARD_STATUS_ORDER, SKILL_STATUS_ORDER, checkPromotion, PromotionError, } from "./mcp.js";
|
|
10
10
|
import { loadRegistry, domainTypeDefect } from "./registry.js";
|
|
11
11
|
import { resolveGenome } from "./loader.js";
|
|
12
|
-
import { SkillSchema, AgentSchema, StandardSchema } from "./genome_schema.js";
|
|
12
|
+
import { SkillSchema, AgentSchema, StandardSchema, DomainTypeSchema } from "./genome_schema.js";
|
|
13
13
|
import { runSkillFixtures, executeSkill, loadFixtures } from "./skill_subprocess.js";
|
|
14
14
|
import { evolveSkill } from "./skills.js";
|
|
15
15
|
import { sealAgentDefinition, sealDefinition, sealSkillPackage, recordIdentity } from "./genome_writer.js";
|
|
16
16
|
import { createOutputStore, defaultOutputsPersistDir } from "./outputs.js";
|
|
17
|
+
import { createOutputMirror, defaultMirrorDir, outputPreview, mirrorStorageRef } from "./output_mirror.js";
|
|
17
18
|
import { FileLedger, LedgerError, LEDGER_SCHEMA_VERSION, defaultLedgerPath, } from "./ledger.js";
|
|
19
|
+
import { sealDrill } from "./seal_drill.js";
|
|
18
20
|
import { standardSimulate } from "./simulate.js";
|
|
19
21
|
import { runGig, BudgetExhausted, GigAborted, ResumeRefused, partialGigUsage, partialBudgetState } from "./runtime.js";
|
|
20
22
|
import { createCheckpointStore, createReuseStore } from "./reuse.js";
|
|
@@ -172,6 +174,46 @@ function syncMap(target, source, before) {
|
|
|
172
174
|
}
|
|
173
175
|
return { added, modified, removed };
|
|
174
176
|
}
|
|
177
|
+
// A queryable output row: the Tier-1 metadata projection (+ preview + storage_ref), with the
|
|
178
|
+
// full `data` payload only when the caller wants it. Keeps output_query's default shape
|
|
179
|
+
// backward-compatible (data present) while making the compact traversal and the deep second
|
|
180
|
+
// pass first-class.
|
|
181
|
+
function outputRow(rec, deps, withData) {
|
|
182
|
+
const row = {
|
|
183
|
+
id: rec.id,
|
|
184
|
+
gig_id: rec.gig_id,
|
|
185
|
+
agent_slug: rec.agent_slug,
|
|
186
|
+
from_role: rec.from_role,
|
|
187
|
+
phase: rec.phase,
|
|
188
|
+
primitive: rec.primitive,
|
|
189
|
+
core_type: rec.core_type,
|
|
190
|
+
domain_type: rec.domain_type,
|
|
191
|
+
domain_type_version: rec.domain_type_version,
|
|
192
|
+
domain: rec.domain,
|
|
193
|
+
content_sha: rec.content_sha,
|
|
194
|
+
input_refs: rec.input_refs,
|
|
195
|
+
input_shas: rec.input_shas,
|
|
196
|
+
cost_usd: rec.cost_usd,
|
|
197
|
+
tokens_used: rec.tokens_used,
|
|
198
|
+
duration_ms: rec.duration_ms,
|
|
199
|
+
model: rec.model,
|
|
200
|
+
model_tier: rec.model_tier,
|
|
201
|
+
created_at: rec.created_at,
|
|
202
|
+
preview: outputPreview(rec.data),
|
|
203
|
+
storage_ref: deps.output_mirror ? mirrorStorageRef(rec.content_sha) : "",
|
|
204
|
+
};
|
|
205
|
+
if (withData)
|
|
206
|
+
row["data"] = rec.data;
|
|
207
|
+
if (rec.skill_provenance !== undefined)
|
|
208
|
+
row["skill_provenance"] = rec.skill_provenance;
|
|
209
|
+
if (rec.reused_from !== undefined)
|
|
210
|
+
row["reused_from"] = rec.reused_from;
|
|
211
|
+
return row;
|
|
212
|
+
}
|
|
213
|
+
// A Tier-1 metadata row (from the mirror) shaped as an output_query row.
|
|
214
|
+
function metaToRow(meta) {
|
|
215
|
+
return { ...meta };
|
|
216
|
+
}
|
|
175
217
|
/**
|
|
176
218
|
* Pure tool dispatcher. Routes a tool call to its implementation. No transport,
|
|
177
219
|
* no I/O beyond the injected deps — fully unit-testable.
|
|
@@ -347,28 +389,70 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
347
389
|
...(std ? { standard: { slug: std.slug, phases: std.phases.map((p) => ({ name: p.name, chairs: p.chairs.length })) } } : {}),
|
|
348
390
|
...(observed.length > 0 ? { observed_costs_usd: observed } : {}),
|
|
349
391
|
});
|
|
350
|
-
|
|
392
|
+
// WU-0008 — the seal drill: before quoting a price, prove every chair contract
|
|
393
|
+
// can seal AT ALL. A cost estimate for a run whose terminal chair is doomed is
|
|
394
|
+
// worse than useless (it prices a failure as if it were work). Failures ride in
|
|
395
|
+
// the SUCCESS payload loudly; dispatch stays unchanged — the operator decides.
|
|
396
|
+
const drill = sealDrill({ phases: std.phases.map((p) => ({ name: p.name, chairs: p.chairs.map((c) => ({ role: c.role, output_contract: c.output_contract })) })) }, deps.registry);
|
|
397
|
+
return { ok: true, requires_approval: approval, data: { ...res, seal_drill: drill } };
|
|
351
398
|
}
|
|
352
399
|
case "output_query": {
|
|
353
|
-
|
|
400
|
+
const mirror = deps.output_mirror;
|
|
401
|
+
// SECOND PASS — a single output's FULL payload by id or content_sha. This is the deeper
|
|
402
|
+
// Tier-2 read: prefer the content-addressed artifact tier (local mirror, or remote when
|
|
403
|
+
// drained), falling back to the store. Returns exactly the one output, with its data.
|
|
404
|
+
const wantId = args["output_id"] ? String(args["output_id"]) : undefined;
|
|
405
|
+
const wantSha = args["content_sha"] ? String(args["content_sha"]) : undefined;
|
|
406
|
+
if (wantId || wantSha) {
|
|
407
|
+
const rec = wantId
|
|
408
|
+
? deps.outputs.get(wantId)
|
|
409
|
+
: deps.outputs.all().find((o) => o.content_sha === wantSha);
|
|
410
|
+
let row = rec ? outputRow(rec, deps, true) : undefined;
|
|
411
|
+
// Store miss (or a mirror-only payload): read the artifact tier directly.
|
|
412
|
+
if ((!row || row["data"] === undefined) && mirror) {
|
|
413
|
+
const sel = {};
|
|
414
|
+
if (wantId)
|
|
415
|
+
sel.id = wantId;
|
|
416
|
+
if (wantSha)
|
|
417
|
+
sel.content_sha = wantSha;
|
|
418
|
+
const hit = mirror.readPayload(sel);
|
|
419
|
+
if (hit?.meta)
|
|
420
|
+
row = { ...metaToRow(hit.meta), data: hit.data };
|
|
421
|
+
else if (row && hit?.data !== undefined)
|
|
422
|
+
row["data"] = hit.data;
|
|
423
|
+
}
|
|
424
|
+
const outputs = row ? [row] : [];
|
|
425
|
+
return { ok: true, requires_approval: approval, data: { outputs, total_count: outputs.length } };
|
|
426
|
+
}
|
|
427
|
+
// LIST — Tier-1 traversal. The store is now cross-process-fresh (the `fullyHydrated`
|
|
428
|
+
// latch is gone), so a gig sealed by a separate CLI process is visible here. `data` is
|
|
429
|
+
// carried by default (backward-compat) and on `data_filter`; pass `include_data:false`
|
|
430
|
+
// for the compact Tier-1 rows (metadata + preview + storage_ref, no payload).
|
|
431
|
+
const dataFilter = args["data_filter"];
|
|
432
|
+
const hasDataFilter = Boolean(dataFilter && typeof dataFilter === "object" && !Array.isArray(dataFilter));
|
|
433
|
+
const includeData = args["include_data"] === false ? false : true;
|
|
434
|
+
let recs = deps.outputs.all();
|
|
354
435
|
if (args["domain_type"])
|
|
355
|
-
|
|
436
|
+
recs = recs.filter((o) => o.domain_type === args["domain_type"]);
|
|
356
437
|
if (args["gig_id"])
|
|
357
|
-
|
|
438
|
+
recs = recs.filter((o) => o.gig_id === args["gig_id"]);
|
|
358
439
|
if (args["agent_slug"])
|
|
359
|
-
|
|
440
|
+
recs = recs.filter((o) => o.agent_slug === args["agent_slug"]);
|
|
360
441
|
// #234 — `data_filter` was advertised and ignored, so a caller narrowing a query by
|
|
361
442
|
// payload got the UNFILTERED set back and a `total_count` describing it. Every key must
|
|
362
443
|
// match (AND), compared structurally so an object or array value filters as written.
|
|
363
|
-
|
|
364
|
-
if (dataFilter && typeof dataFilter === "object" && !Array.isArray(dataFilter)) {
|
|
444
|
+
if (hasDataFilter) {
|
|
365
445
|
const entries = Object.entries(dataFilter);
|
|
366
|
-
|
|
446
|
+
recs = recs.filter((o) => {
|
|
367
447
|
const data = (o.data ?? {});
|
|
368
448
|
return entries.every(([k, v]) => canonJson(data[k]) === canonJson(v));
|
|
369
449
|
});
|
|
370
450
|
}
|
|
371
|
-
|
|
451
|
+
// Payload is dropped only when the caller explicitly asks for the compact rows AND is not
|
|
452
|
+
// filtering by payload (a data_filter needs the payload to have been read).
|
|
453
|
+
const withData = includeData || hasDataFilter;
|
|
454
|
+
const outputs = recs.map((o) => outputRow(o, deps, withData));
|
|
455
|
+
return { ok: true, requires_approval: approval, data: { outputs, total_count: outputs.length } };
|
|
372
456
|
}
|
|
373
457
|
case "output_trace": {
|
|
374
458
|
const id = String(args["output_id"] ?? "");
|
|
@@ -501,6 +585,19 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
501
585
|
const warnings = stdStatus === "deprecated"
|
|
502
586
|
? [`standard "${slug2}" is deprecated — it still runs, but should not be built on.`]
|
|
503
587
|
: [];
|
|
588
|
+
// WU-0008 preflight: run the same sealDrill used by standard_simulate BEFORE spending
|
|
589
|
+
// on any chair. A structurally-unsealable standard is refused here (pennies) instead of
|
|
590
|
+
// after a chair runs and aborts. Gate is placed once, above the wait/async split, so a
|
|
591
|
+
// single check covers both runGig call-sites below.
|
|
592
|
+
const drill = sealDrill({ phases: standard.phases.map((p) => ({ name: p.name, chairs: p.chairs.map((c) => ({ role: c.role, output_contract: c.output_contract })) })) }, deps.registry);
|
|
593
|
+
if (!drill.ok) {
|
|
594
|
+
return {
|
|
595
|
+
ok: false, requires_approval: approval,
|
|
596
|
+
error: `standard "${slug2}" cannot seal: ` +
|
|
597
|
+
drill.failures.map((f) => `${f.phase}/${f.role} → ${f.domain_type} (${f.errors.join("; ")})`).join(", "),
|
|
598
|
+
data: { seal_drill: drill },
|
|
599
|
+
};
|
|
600
|
+
}
|
|
504
601
|
// Optional budget arg — when present, runtime enforces per-gig cost-budget
|
|
505
602
|
// and raises BudgetExhausted on depletion (PR for T10 gap, see runtime.ts).
|
|
506
603
|
const budgetArg = args["budget"];
|
|
@@ -935,6 +1032,9 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
935
1032
|
slug: baseDef.slug,
|
|
936
1033
|
extends: baseDef.extends,
|
|
937
1034
|
schema: { properties: nextProps },
|
|
1035
|
+
// 2026-08-08 — the undeclared-required check needs the extension's required
|
|
1036
|
+
// set, or an extend could version-in a requirement no producer can see.
|
|
1037
|
+
required_fields: nextRequired,
|
|
938
1038
|
});
|
|
939
1039
|
if (extendDefect) {
|
|
940
1040
|
return { ok: false, requires_approval: approval, error: `type_extend rejected: ${extendDefect}` };
|
|
@@ -1620,6 +1720,46 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1620
1720
|
// production status, and never RUN, TESTED, LISTED or REVISED through the engine. The
|
|
1621
1721
|
// fixture gate on promotion made that gap sharper — you could be refused for failing
|
|
1622
1722
|
// fixtures with no way to run them and see why.
|
|
1723
|
+
// ── discoverability parity — a dispatcher must be able to FIND a slug over MCP ──────
|
|
1724
|
+
// (tests/genome_browse_parity.test.ts). Backed by the deps maps, so the same handler
|
|
1725
|
+
// serves a working-tree load and a hosted store load identically; no filesystem.
|
|
1726
|
+
case "standard_browse": {
|
|
1727
|
+
if (!deps.standards)
|
|
1728
|
+
return { ok: false, not_implemented: true, requires_approval: approval, error: "standard_browse needs a standards map (bootstrap from a genome)" };
|
|
1729
|
+
let list = [...deps.standards.values()];
|
|
1730
|
+
if (args["domain"])
|
|
1731
|
+
list = list.filter((s) => s.domain === args["domain"]);
|
|
1732
|
+
if (args["status"])
|
|
1733
|
+
list = list.filter((s) => (s.status ?? "active") === args["status"]);
|
|
1734
|
+
const standards = list
|
|
1735
|
+
.map((s) => ({
|
|
1736
|
+
slug: s.slug, domain: s.domain, status: s.status ?? "active",
|
|
1737
|
+
phases: s.phases.map((p) => p.name), phase_count: s.phases.length,
|
|
1738
|
+
chair_count: s.phases.reduce((n, p) => n + p.chairs.length, 0),
|
|
1739
|
+
input_types: s.input_types ?? [], output_types: s.output_types ?? [],
|
|
1740
|
+
eval_slugs: s.eval_slugs ?? [], description: s.description ?? null,
|
|
1741
|
+
}))
|
|
1742
|
+
.sort((a, b) => (a.slug < b.slug ? -1 : 1));
|
|
1743
|
+
return { ok: true, requires_approval: approval, data: { standards, count: standards.length } };
|
|
1744
|
+
}
|
|
1745
|
+
case "agent_browse": {
|
|
1746
|
+
if (!deps.agents)
|
|
1747
|
+
return { ok: false, not_implemented: true, requires_approval: approval, error: "agent_browse needs an agents map (bootstrap from a genome)" };
|
|
1748
|
+
let list = [...deps.agents.values()];
|
|
1749
|
+
if (args["domain"])
|
|
1750
|
+
list = list.filter((a) => a.domain === args["domain"]);
|
|
1751
|
+
if (args["primitive"])
|
|
1752
|
+
list = list.filter((a) => a.primitives.includes(args["primitive"]));
|
|
1753
|
+
const agents = list
|
|
1754
|
+
.map((a) => ({
|
|
1755
|
+
slug: a.slug, primitives: a.primitives, domain: a.domain,
|
|
1756
|
+
input_types: a.input_types, output_types: a.output_types,
|
|
1757
|
+
behavioral_primitives: a.behavioral_primitives,
|
|
1758
|
+
skill_slugs: a.skill_slugs ?? [], model_tier: a.model_tier ?? null,
|
|
1759
|
+
}))
|
|
1760
|
+
.sort((x, y) => (x.slug < y.slug ? -1 : 1));
|
|
1761
|
+
return { ok: true, requires_approval: approval, data: { agents, count: agents.length } };
|
|
1762
|
+
}
|
|
1623
1763
|
case "skill_browse": {
|
|
1624
1764
|
if (!deps.skills)
|
|
1625
1765
|
return { ok: false, not_implemented: true, requires_approval: approval, error: "skill_browse needs a skills map (bootstrap from a genome)" };
|
|
@@ -2153,18 +2293,99 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
2153
2293
|
return { ok: false, requires_approval: approval, error: e instanceof Error ? e.message : String(e) };
|
|
2154
2294
|
}
|
|
2155
2295
|
}
|
|
2156
|
-
|
|
2296
|
+
// Tools whose implementation is inherently local-process. Each entry names WHY, so the
|
|
2297
|
+
// hosted refusal teaches instead of stonewalling.
|
|
2298
|
+
const HOSTED_BLOCKED = {
|
|
2299
|
+
server_restart: "server_restart is a local-process concern (the stdio relay restarts its child); a hosted, per-request surface has no server process to restart",
|
|
2300
|
+
skill_execute: "skill_execute runs a skill's code half in a local subprocess against its on-disk package; hosted skills carry no local package — run code skills where the genome files live",
|
|
2301
|
+
skill_evolve: "skill_evolve runs candidate code against local fixtures in a subprocess; hosted skills carry no local package to evolve",
|
|
2302
|
+
charter_read: "charter_read reads a charter file from a local path; a hosted surface has no filesystem",
|
|
2303
|
+
gig_logs: "gig_logs tails per-chair log files under the local outputs dir; hosted runs are executed by the drain worker and their record lives in the store",
|
|
2304
|
+
genome_reload: "genome_reload re-reads genome files from disk; a hosted surface loads its genome from the store per-request, so there is nothing to reload",
|
|
2305
|
+
};
|
|
2306
|
+
// The genome-mutation tools whose success must ALSO land in the hosted store, and the class
|
|
2307
|
+
// each persists as. Payload is filtered by the class's own Zod schema key list (the same
|
|
2308
|
+
// single source the handlers copy from), so the store payload can't drift from the schema.
|
|
2309
|
+
const HOSTED_UPSERT = {
|
|
2310
|
+
agent_define: { cls: "agent", keys: Object.keys(AgentSchema.shape) },
|
|
2311
|
+
standard_compose: { cls: "standard", keys: Object.keys(StandardSchema.shape) },
|
|
2312
|
+
type_register: { cls: "domain_type", keys: Object.keys(DomainTypeSchema.shape) },
|
|
2313
|
+
skill_define: { cls: "skill", keys: Object.keys(SkillSchema.shape) },
|
|
2314
|
+
};
|
|
2315
|
+
async function callSurfaceTool(slug, args, deps) {
|
|
2316
|
+
if (deps.hosted) {
|
|
2317
|
+
const blocked = HOSTED_BLOCKED[slug];
|
|
2318
|
+
if (blocked)
|
|
2319
|
+
return { ok: false, hosted_unsupported: true, error: blocked };
|
|
2320
|
+
if (slug === "gig_dispatch") {
|
|
2321
|
+
// Hosted dispatch NEVER spawns. With a queue seam it queues (the gig table is the
|
|
2322
|
+
// queue; a drain worker claims and runs); without one it says so, typed.
|
|
2323
|
+
if (deps.queueGig) {
|
|
2324
|
+
try {
|
|
2325
|
+
const data = await deps.queueGig(args);
|
|
2326
|
+
return { ok: true, data };
|
|
2327
|
+
}
|
|
2328
|
+
catch (e) {
|
|
2329
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return {
|
|
2333
|
+
ok: false,
|
|
2334
|
+
hosted_unsupported: true,
|
|
2335
|
+
error: "hosted dispatch goes through the queue RPC (coltrane_gig_dispatch) — nothing spawns in a hosted " +
|
|
2336
|
+
"surface. Wire deps.queueGig (e.g. postgrestQueueGig(ctx) from ./genome_store) to queue the gig " +
|
|
2337
|
+
"for the drain worker.",
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
const result = await dispatchTool(slug, args, deps);
|
|
2342
|
+
// Hosted persistence: without a genome_dir the handlers compute + validate + seal identity
|
|
2343
|
+
// but write no file (the validation path). The store upsert is the hosted write half; a
|
|
2344
|
+
// refused upsert fails the mutation loudly — a definition that only ever lived in this
|
|
2345
|
+
// request's memory must not report success.
|
|
2346
|
+
const up = HOSTED_UPSERT[slug];
|
|
2347
|
+
if (deps.hosted && deps.store && result.ok && up) {
|
|
2348
|
+
const payload = {};
|
|
2349
|
+
for (const k of up.keys)
|
|
2350
|
+
if (args[k] !== undefined)
|
|
2351
|
+
payload[k] = args[k];
|
|
2352
|
+
try {
|
|
2353
|
+
await deps.store.upsert(up.cls, payload);
|
|
2354
|
+
}
|
|
2355
|
+
catch (e) {
|
|
2356
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
return result;
|
|
2360
|
+
}
|
|
2361
|
+
/** The engine's FULL MCP tool surface as a transport-agnostic registry. The stdio server
|
|
2362
|
+
* below consumes this internally; a hosted host mounts it per-request with hosted deps. */
|
|
2363
|
+
export function createToolSurface(deps) {
|
|
2364
|
+
return MCP_TOOLS.map((t) => ({
|
|
2365
|
+
name: t.slug,
|
|
2366
|
+
description: `${t.category} tool`,
|
|
2367
|
+
input_schema: t.input_schema,
|
|
2368
|
+
call: (args) => callSurfaceTool(t.slug, args, deps),
|
|
2369
|
+
}));
|
|
2370
|
+
}
|
|
2371
|
+
/** Build the low-level MCP Server with ListTools + CallTool wired to the tool surface. */
|
|
2157
2372
|
export function createColtraneServer(deps, recorder) {
|
|
2158
2373
|
const server = new Server({ name: "coltrane", version: COLTRANE_VERSION }, { capabilities: { tools: {} } });
|
|
2374
|
+
const surface = createToolSurface(deps);
|
|
2375
|
+
const byName = new Map(surface.map((t) => [t.name, t]));
|
|
2159
2376
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2160
|
-
tools:
|
|
2161
|
-
name: t.
|
|
2162
|
-
description:
|
|
2377
|
+
tools: surface.map((t) => ({
|
|
2378
|
+
name: t.name,
|
|
2379
|
+
description: t.description,
|
|
2163
2380
|
inputSchema: t.input_schema,
|
|
2164
2381
|
})),
|
|
2165
2382
|
}));
|
|
2166
2383
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
2167
|
-
const
|
|
2384
|
+
const args = (req.params.arguments ?? {});
|
|
2385
|
+
const tool = byName.get(req.params.name);
|
|
2386
|
+
// A name outside the surface falls through to the dispatcher's own unknown-tool answer,
|
|
2387
|
+
// keeping the reply byte-identical to the pre-surface path.
|
|
2388
|
+
const result = tool ? await tool.call(args) : await dispatchTool(req.params.name, args, deps);
|
|
2168
2389
|
if (recorder) {
|
|
2169
2390
|
recorder.recordToolCall(req.params.name);
|
|
2170
2391
|
recorder.recordObservability(`call:${req.params.name}`, { ok: result.ok });
|
|
@@ -2234,13 +2455,19 @@ export function bootstrapServerDeps(genomeRoot) {
|
|
|
2234
2455
|
// the tool as mcp__coltrane__<slug> — the name the server exposes — so a bare-slug grant (the form
|
|
2235
2456
|
// agent_define accepts) is actually callable instead of a silent dead name that seals nothing.
|
|
2236
2457
|
const toolProviders = new Map([...REGISTERED_TOOL_SLUGS].map((slug) => [slug, { tool: slug, kind: "in_house", server: ENGINE_MCP_SERVER }]));
|
|
2458
|
+
// The two-tier local mirror, rooted at `<root>/.coltrane` (gitignored, beside the ledger).
|
|
2459
|
+
// The SAME instance is handed to the OutputStore and exposed as `output_mirror`, so every
|
|
2460
|
+
// sealed output — whether this process ran the gig or a separate CLI process did — lands in
|
|
2461
|
+
// one content-addressed store MCP retrieval reads. COLTRANE_MIRROR_DIR overrides (tests).
|
|
2462
|
+
const output_mirror = createOutputMirror(defaultMirrorDir(root));
|
|
2237
2463
|
return {
|
|
2238
2464
|
registry,
|
|
2239
2465
|
toolProviders,
|
|
2466
|
+
output_mirror,
|
|
2240
2467
|
// PR #78 follow-up: persist outputs to disk so the audit chain survives an
|
|
2241
2468
|
// MCP session close (Rob cold-trial requirement). COLTRANE_OUTPUTS_DIR
|
|
2242
2469
|
// overrides the default ~/.eir/coltrane_outputs path (tests + sandboxes).
|
|
2243
|
-
outputs: createOutputStore(registry, { persistDir: defaultOutputsPersistDir() }),
|
|
2470
|
+
outputs: createOutputStore(registry, { persistDir: defaultOutputsPersistDir(), mirror: output_mirror }),
|
|
2244
2471
|
// #209 — the audit spine is durable by default. The line above gives OUTPUTS a persistDir
|
|
2245
2472
|
// under an explicit "the audit chain must survive an MCP session close" requirement
|
|
2246
2473
|
// (PR #78); the ledger sat in RAM directly beneath it, which made absence-of-row mean
|