@eir-labs/coltrane 0.10.0 → 0.11.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.
- package/agents/bandleader.json +54 -0
- package/agents/bill.json +4 -2
- package/agents/change-verifier.json +2 -1
- package/agents/code-implementer.json +7 -4
- package/agents/john.json +5 -3
- package/agents/lineage-scout-external.json +19 -6
- package/agents/lineage-scribe.json +20 -9
- package/agents/lineage-weaver.json +26 -11
- package/agents/miles.json +2 -1
- package/agents/players/bandleader.md +70 -0
- package/agents/pr-publisher.json +4 -2
- package/agents/red-spec-drafter.json +12 -6
- package/agents/room-prober.json +29 -0
- package/agents/source-walker.json +1 -1
- package/charts/room-probe-chart-v1.json +14 -0
- package/charts/worker-contract-docker-v2.json +135 -0
- package/charts/worker-contract-v3.json +206 -0
- package/dist/src/boundary_check.d.ts +18 -0
- package/dist/src/boundary_check.js +70 -0
- package/dist/src/boundary_check.js.map +1 -0
- package/dist/src/boundary_scan.d.ts +62 -0
- package/dist/src/boundary_scan.js +93 -0
- package/dist/src/boundary_scan.js.map +1 -0
- package/dist/src/chart.js +34 -1
- package/dist/src/chart.js.map +1 -1
- package/dist/src/claude_invoker.d.ts +2 -1
- package/dist/src/claude_invoker.js +150 -17
- package/dist/src/claude_invoker.js.map +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +61 -26
- package/dist/src/cli.js.map +1 -1
- package/dist/src/drain_preflight.d.ts +33 -0
- package/dist/src/drain_preflight.js +97 -0
- package/dist/src/drain_preflight.js.map +1 -0
- package/dist/src/genome_schema.d.ts +390 -9
- package/dist/src/genome_schema.js +163 -8
- package/dist/src/genome_schema.js.map +1 -1
- package/dist/src/genome_store.js +9 -0
- package/dist/src/genome_store.js.map +1 -1
- package/dist/src/genome_writer.d.ts +1 -1
- package/dist/src/genome_writer.js +10 -2
- package/dist/src/genome_writer.js.map +1 -1
- package/dist/src/gig_conformance.d.ts +66 -0
- package/dist/src/gig_conformance.js +153 -0
- package/dist/src/gig_conformance.js.map +1 -0
- package/dist/src/gig_tracker.js +8 -0
- package/dist/src/gig_tracker.js.map +1 -1
- package/dist/src/grounding.d.ts +44 -0
- package/dist/src/grounding.js +127 -0
- package/dist/src/grounding.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/institution_loader.d.ts +29 -5
- package/dist/src/institution_loader.js +67 -6
- package/dist/src/institution_loader.js.map +1 -1
- package/dist/src/lineage_adoption.d.ts +82 -0
- package/dist/src/lineage_adoption.js +82 -0
- package/dist/src/lineage_adoption.js.map +1 -0
- package/dist/src/lineage_persist.d.ts +34 -0
- package/dist/src/lineage_persist.js +60 -0
- package/dist/src/lineage_persist.js.map +1 -0
- package/dist/src/loader.d.ts +13 -0
- package/dist/src/loader.js +43 -2
- package/dist/src/loader.js.map +1 -1
- package/dist/src/mcp.js +37 -4
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/org_hire.d.ts +23 -0
- package/dist/src/org_hire.js +42 -0
- package/dist/src/org_hire.js.map +1 -0
- package/dist/src/outputs.d.ts +17 -0
- package/dist/src/outputs.js +86 -1
- package/dist/src/outputs.js.map +1 -1
- package/dist/src/player_to_claude_code.js +26 -2
- package/dist/src/player_to_claude_code.js.map +1 -1
- package/dist/src/registry.d.ts +3 -1
- package/dist/src/registry.js +43 -3
- package/dist/src/registry.js.map +1 -1
- package/dist/src/repo_index.d.ts +58 -0
- package/dist/src/repo_index.js +232 -0
- package/dist/src/repo_index.js.map +1 -0
- package/dist/src/reuse.d.ts +44 -4
- package/dist/src/reuse.js +0 -0
- package/dist/src/reuse.js.map +1 -1
- package/dist/src/runtime.d.ts +82 -0
- package/dist/src/runtime.js +254 -25
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/server.d.ts +37 -0
- package/dist/src/server.js +422 -11
- package/dist/src/server.js.map +1 -1
- package/dist/src/server_relay.d.ts +40 -2
- package/dist/src/server_relay.js +186 -19
- package/dist/src/server_relay.js.map +1 -1
- package/dist/src/tool_providers.d.ts +27 -0
- package/dist/src/tool_providers.js +48 -0
- package/dist/src/tool_providers.js.map +1 -1
- package/dist/src/venue_credential.d.ts +41 -0
- package/dist/src/venue_credential.js +57 -0
- package/dist/src/venue_credential.js.map +1 -0
- package/dist/src/venue_realize.d.ts +23 -0
- package/dist/src/venue_realize.js +43 -3
- package/dist/src/venue_realize.js.map +1 -1
- package/dist/src/venue_realizer.d.ts +224 -0
- package/dist/src/venue_realizer.js +889 -0
- package/dist/src/venue_realizer.js.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/worker.d.ts +37 -0
- package/dist/src/worker.js +127 -24
- package/dist/src/worker.js.map +1 -1
- package/dist/src/worker_env.d.ts +79 -0
- package/dist/src/worker_env.js +322 -0
- package/dist/src/worker_env.js.map +1 -0
- package/dist/src/workspace.d.ts +12 -1
- package/dist/src/workspace.js +10 -3
- package/dist/src/workspace.js.map +1 -1
- package/domain_types/change-context.json +59 -13
- package/domain_types/lineage-adoption-target.json +22 -0
- package/domain_types/lineage-map.json +7 -2
- package/domain_types/lineage-record.json +55 -5
- package/domain_types/prior-art-hit.json +16 -1
- package/domain_types/red-spec.json +5 -3
- package/domain_types/woodshed-record.json +149 -0
- package/evals/gist-present.json +7 -0
- package/institutions/coltrane.json +178 -6
- package/package.json +6 -3
- package/skills/non-vacuity-probe/fixtures/fixture-001.json +17 -0
- package/skills/non-vacuity-probe/meta.json +12 -0
- package/skills/non-vacuity-probe/skill.md +27 -0
- package/skills/non-vacuity-probe/skill.mjs +81 -0
- package/standards/lineage-adopt-v0.json +37 -0
- package/standards/room-probe-v1.json +28 -0
- package/standards/software-change-pr-v1.json +15 -15
- package/standards/software-change-red-first-v0.json +183 -0
- package/standards/spec-review-and-sequence-v0.json +112 -0
- package/standards/studio-session-v0.json +101 -0
- package/tours/coltrane.json +72 -0
- package/venues/engine-room-v1.json +32 -0
package/dist/src/server.js
CHANGED
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
// The wire names of the reserved relay↔child methods (restart guard, venue/8). Defined by the
|
|
11
|
+
// relay — the relay owns the conversation — and answered by the child handlers registered below.
|
|
12
|
+
import { RUNNING_GIGS_METHOD, ABORT_FOR_RESTART_METHOD } from "./server_relay.js";
|
|
9
13
|
import { MCP_TOOLS, requiresApproval, AGENT_STATUS_ORDER, STANDARD_STATUS_ORDER, SKILL_STATUS_ORDER, checkPromotion, PromotionError, } from "./mcp.js";
|
|
10
14
|
import { loadRegistry, domainTypeDefect } from "./registry.js";
|
|
11
15
|
import { resolveGenome } from "./loader.js";
|
|
12
|
-
import { SkillSchema, AgentSchema, StandardSchema, DomainTypeSchema, ChartSchema, VenueSchema, venueDefect } from "./genome_schema.js";
|
|
16
|
+
import { SkillSchema, AgentSchema, StandardSchema, DomainTypeSchema, ChartSchema, VenueSchema, VenueObjectSchema, venueDefect } from "./genome_schema.js";
|
|
13
17
|
import { composeChart, runChart, chartHash, chartEntrySeedTypes, dispatchTarget, } from "./chart.js";
|
|
14
18
|
import { runSkillFixtures, executeSkill, loadFixtures } from "./skill_subprocess.js";
|
|
15
19
|
import { evolveSkill } from "./skills.js";
|
|
16
20
|
import { sealAgentDefinition, sealDefinition, sealSkillPackage, recordIdentity } from "./genome_writer.js";
|
|
17
|
-
import { createOutputStore, defaultOutputsPersistDir, } from "./outputs.js";
|
|
21
|
+
import { createOutputStore, defaultOutputsPersistDir, performanceRoot, } from "./outputs.js";
|
|
18
22
|
import { createOutputMirror, defaultMirrorDir, outputPreview, mirrorStorageRef } from "./output_mirror.js";
|
|
19
23
|
import { FileLedger, LedgerError, LEDGER_SCHEMA_VERSION, defaultLedgerPath, } from "./ledger.js";
|
|
20
24
|
import { sealDrill } from "./seal_drill.js";
|
|
@@ -22,8 +26,10 @@ import { standardSimulate } from "./simulate.js";
|
|
|
22
26
|
import { runGig, BudgetExhausted, GigAborted, ResumeRefused, partialGigUsage, partialBudgetState } from "./runtime.js";
|
|
23
27
|
import { createCheckpointStore, createReuseStore } from "./reuse.js";
|
|
24
28
|
import { makeClaudeInvoker, killLiveChairChildren } from "./claude_invoker.js";
|
|
29
|
+
import { dockerComposeRealizer } from "./venue_realizer.js";
|
|
25
30
|
import { isDepth, DEPTHS } from "./pricing.js";
|
|
26
31
|
import { ENGINE_MCP_SERVER } from "./tool_providers.js";
|
|
32
|
+
import { gigScopeRefusal, missingWorkerEnv, } from "./venue_credential.js";
|
|
27
33
|
import { composeStandard, defineAgent, CompositionError } from "./composition.js";
|
|
28
34
|
import { PRIMITIVE_OUTPUT_TYPE } from "./core_types.js";
|
|
29
35
|
import { proposeTypeChange } from "./type_versioning.js";
|
|
@@ -526,9 +532,13 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
526
532
|
throw new Error(verdict.reason ??
|
|
527
533
|
`output rejected: "${domain_type || core_type}" did not satisfy its output contract`);
|
|
528
534
|
}
|
|
535
|
+
// Name what did NOT happen. Validation genuinely succeeded (ok:true, validated:true), but
|
|
536
|
+
// the runtime is the one sealer — this branch does NOT persist. A truthful compose chair
|
|
537
|
+
// once read `validated` as `sealed` and filed a false completion; `sealed:false` makes
|
|
538
|
+
// VALIDATED unmistakable from SEALED so no reader can launder one into the other.
|
|
529
539
|
return {
|
|
530
540
|
ok: true, requires_approval: approval,
|
|
531
|
-
data: { validated: true, validation_result: { valid: true } },
|
|
541
|
+
data: { validated: true, sealed: false, validation_result: { valid: true } },
|
|
532
542
|
};
|
|
533
543
|
}
|
|
534
544
|
const rec = deps.outputs.write({
|
|
@@ -585,6 +595,23 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
585
595
|
// player; an unseated one produces a gig that can only fail at genome load.
|
|
586
596
|
const actingFor = args["acting_for"] === undefined || args["acting_for"] === null ? undefined : String(args["acting_for"]);
|
|
587
597
|
void actingFor; // forwarded to the queue seam via `args` below; named here to be legible.
|
|
598
|
+
// WHERE it plays, as distinct from what it plays. This is a CEILING, and the whole design is
|
|
599
|
+
// fail-closed — so the one outcome the contract forbids is proceeding as if no venue was
|
|
600
|
+
// asked. It is threaded into BOTH runGig calls below (the sync and async paths) via the same
|
|
601
|
+
// conditional-spread trio the chart path uses (server.ts:935-941), so `runGig`'s own venue
|
|
602
|
+
// block either realizes the room or refuses fail-closed. It used to be read into this local
|
|
603
|
+
// and then DISCARDED (`void venue`, with a comment claiming the queue seam forwarded it — it
|
|
604
|
+
// did not for a local run): gig a77f6f7f dispatched room-probe-v1 with a named venue and got
|
|
605
|
+
// NO room, NO refusal, and a 'complete' status whose output was byte-identical to the
|
|
606
|
+
// venue-less control. An unnamed gig carries `undefined` here and is threaded to nothing —
|
|
607
|
+
// the venue-less path stays byte-identical.
|
|
608
|
+
const venue = args["venue"] === undefined || args["venue"] === null ? undefined : String(args["venue"]);
|
|
609
|
+
// WHAT REPOSITORY it works on, as distinct from WHERE it plays. The SUBJECT of the run, named
|
|
610
|
+
// EXPLICITLY at dispatch and threaded to runGig (the same conditional-spread trio venue uses)
|
|
611
|
+
// so the realized room's workspace is populated with THIS repository — never process.cwd() or
|
|
612
|
+
// any ambient host path. A venue is at rest and serves many repositories, so the repository
|
|
613
|
+
// belongs on the RUN, not the room. Absent → the room declines to populate (empty workspace).
|
|
614
|
+
const repoUrl = args["repo_url"] === undefined || args["repo_url"] === null ? undefined : String(args["repo_url"]);
|
|
588
615
|
const target = dispatchTarget({
|
|
589
616
|
standard_slug: args["standard_slug"] === undefined || args["standard_slug"] === null ? undefined : String(args["standard_slug"]),
|
|
590
617
|
chart_slug: args["chart_slug"] === undefined || args["chart_slug"] === null ? undefined : String(args["chart_slug"]),
|
|
@@ -704,10 +731,18 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
704
731
|
if (reuseOn && !deps.reuse) {
|
|
705
732
|
return { ok: false, requires_approval: approval, error: `gig_dispatch: reuse was requested but this server has no reuse store wired` };
|
|
706
733
|
}
|
|
734
|
+
// #20 — the CLI signals an OMITTED --input as a boolean on args, so a human-only resume can
|
|
735
|
+
// inherit the checkpoint's gig_input_sha instead of drifting to sha256('{}') and refusing.
|
|
736
|
+
// It is advertised in gig_dispatch's input_schema (src/mcp.ts): a control the handler reads
|
|
737
|
+
// must be discoverable by a caller (#234). Only true when the caller stated no payload; an
|
|
738
|
+
// explicit `{}` is a supplied value and leaves this false, so a disagreeing payload still
|
|
739
|
+
// gates (see src/runtime.ts).
|
|
740
|
+
const gigInputOmitted = args["gig_input_omitted"] === true;
|
|
707
741
|
const reuseWiring = {
|
|
708
742
|
...(deps.checkpoints ? { checkpoints: deps.checkpoints } : {}),
|
|
709
743
|
...(resumeArg !== undefined ? { resume_from: resumeArg } : {}),
|
|
710
744
|
...(reuseOn && deps.reuse ? { reuse: deps.reuse } : {}),
|
|
745
|
+
...(gigInputOmitted ? { gig_input_omitted: true } : {}),
|
|
711
746
|
};
|
|
712
747
|
// ── the human seat's door ────────────────────────────────────────────────────────
|
|
713
748
|
// A chair marked `human: true` parks the run until its incumbent's verdict arrives
|
|
@@ -758,6 +793,21 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
758
793
|
outputs: deps.outputs, ledger: deps.ledger, invoke: deps.invoke,
|
|
759
794
|
model_version: deps.model_version, skills: deps.skills, skill_dirs: deps.skill_dirs, evals: deps.evals, budget,
|
|
760
795
|
toolProviders: deps.toolProviders, mcpServerConfigs: deps.mcpServerConfigs, // each movement's preflight resolves against the invoker's environment
|
|
796
|
+
// THE ROOM THE ARRANGEMENT NAMED, carried into the run.
|
|
797
|
+
//
|
|
798
|
+
// Without this the venue branch in `runGig` is unreachable in production: `composeChart`
|
|
799
|
+
// checks the ceiling at AUTHORING time (R10) and `deps.venues` is passed here only so it
|
|
800
|
+
// can, but the chart's own `venue` was never threaded into the run deps — so no shipped
|
|
801
|
+
// caller ever set `deps.venue`, and `resolveAndRealize` ran for tests alone. A room that
|
|
802
|
+
// is checked when the chart is written and forgotten when it is performed is a ceiling
|
|
803
|
+
// on paper.
|
|
804
|
+
...(chartDef.venue ? { venue: chartDef.venue } : {}),
|
|
805
|
+
...(deps.venues ? { venues: deps.venues } : {}),
|
|
806
|
+
// The substrate seam, carried beside the room the arrangement named: when that room
|
|
807
|
+
// declares mcp_servers, runGig stands it up on this realizer and threads its transports
|
|
808
|
+
// onto each chair's spawn. Absent = the substrate is skipped (server-less venues, or a
|
|
809
|
+
// bare deps without a realizer wired).
|
|
810
|
+
...(deps.venueRealizer ? { venueRealizer: deps.venueRealizer } : {}),
|
|
761
811
|
...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
|
|
762
812
|
};
|
|
763
813
|
/** The ARRANGEMENT's manifest. A chart has no single genome_hash or run_fingerprint — it
|
|
@@ -916,6 +966,15 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
916
966
|
outputs: deps.outputs, ledger: deps.ledger, invoke: deps.invoke,
|
|
917
967
|
model_version: deps.model_version, skills: deps.skills, skill_dirs: deps.skill_dirs, evals: deps.evals, budget,
|
|
918
968
|
toolProviders: deps.toolProviders, mcpServerConfigs: deps.mcpServerConfigs, // dispatch preflight resolves against the invoker's environment
|
|
969
|
+
// THE ROOM THIS DISPATCH NAMED, threaded so runGig realizes it or refuses fail-closed —
|
|
970
|
+
// the same conditional-spread trio the chart path uses (server.ts:935-941). `venue` is
|
|
971
|
+
// threaded only when non-empty so runGig's `deps.venue !== undefined` gate is never
|
|
972
|
+
// tripped by an absent one; `venues`/`venueRealizer` come from bootstrap (ServerDeps).
|
|
973
|
+
...(venue ? { venue } : {}),
|
|
974
|
+
...(deps.venues ? { venues: deps.venues } : {}),
|
|
975
|
+
...(deps.venueRealizer ? { venueRealizer: deps.venueRealizer } : {}),
|
|
976
|
+
// The repository the run names, threaded so runGig populates the room's tree from it.
|
|
977
|
+
...(repoUrl ? { repoUrl } : {}),
|
|
919
978
|
...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
|
|
920
979
|
});
|
|
921
980
|
return {
|
|
@@ -1002,6 +1061,15 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1002
1061
|
outputs: deps.outputs, ledger: deps.ledger, invoke: deps.invoke,
|
|
1003
1062
|
model_version: deps.model_version, skills: deps.skills, skill_dirs: deps.skill_dirs, evals: deps.evals, budget,
|
|
1004
1063
|
toolProviders: deps.toolProviders, mcpServerConfigs: deps.mcpServerConfigs, // dispatch preflight resolves against the invoker's environment
|
|
1064
|
+
// Same venue trio as the sync path above — the DEFAULT dispatch mode must honour a named
|
|
1065
|
+
// room too, or the fix covers only the deterministic wait:true test path and leaves the
|
|
1066
|
+
// path the product actually dispatches through discarding the venue.
|
|
1067
|
+
...(venue ? { venue } : {}),
|
|
1068
|
+
...(deps.venues ? { venues: deps.venues } : {}),
|
|
1069
|
+
...(deps.venueRealizer ? { venueRealizer: deps.venueRealizer } : {}),
|
|
1070
|
+
// Same repository wire as the sync path above — the default async dispatch must populate a
|
|
1071
|
+
// named room's tree too, not only the deterministic wait:true path.
|
|
1072
|
+
...(repoUrl ? { repoUrl } : {}),
|
|
1005
1073
|
gig_id: gigId, onProgress, signal: controller.signal, ...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
|
|
1006
1074
|
});
|
|
1007
1075
|
// A REFUSED resume must be answered in THIS reply, not discovered later by polling. The
|
|
@@ -1104,8 +1172,22 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1104
1172
|
// Prefer the live state map (async runs). Falls back to the ledger/outputs read for a
|
|
1105
1173
|
// synchronously-completed gig (or one from a prior server lifetime, not in the map).
|
|
1106
1174
|
const live = deps.gig_runs?.get(gid);
|
|
1175
|
+
// A MOVEMENT SEALS UNDER ITS OWN ID, so a raw `===` can never match a chart's records: a
|
|
1176
|
+
// movement runs as `<performance>.m.<movement_id>` (src/chart.ts `movementGigId`), and the
|
|
1177
|
+
// id an operator holds — the one `gig_dispatch` returned — is the performance's. Resolve
|
|
1178
|
+
// through `performanceRoot`, which src/outputs.ts:142-147 declares the ONE owner of that
|
|
1179
|
+
// scheme, rather than re-deriving the separator here; that second copy is the drift the
|
|
1180
|
+
// note there warns about, and it is what `output_trace` already had to fix (#248,
|
|
1181
|
+
// tests/cross_movement_trace.test.ts). A plain gig id is its own root, so a
|
|
1182
|
+
// single-standard run is unmoved, and a shared PREFIX is not a root — the infix carries
|
|
1183
|
+
// dots on both sides precisely so a uuid cannot be mistaken for a parent of another.
|
|
1184
|
+
// BOTH READINGS RESOLVE, and the exact match is not redundant: asking about a MOVEMENT's
|
|
1185
|
+
// own id must keep naming its own records, and `performanceRoot(<perf>.m.<mv>)` is
|
|
1186
|
+
// `<perf>`, which would not equal the movement id the caller asked about. A root-only
|
|
1187
|
+
// predicate trades one false negative for another.
|
|
1188
|
+
const ofPerformance = (o) => o.gig_id === gid || performanceRoot(o.gig_id) === gid;
|
|
1107
1189
|
if (live) {
|
|
1108
|
-
const outs = deps.outputs.all().filter(
|
|
1190
|
+
const outs = deps.outputs.all().filter(ofPerformance);
|
|
1109
1191
|
return {
|
|
1110
1192
|
ok: true, requires_approval: approval,
|
|
1111
1193
|
data: {
|
|
@@ -1141,7 +1223,10 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1141
1223
|
},
|
|
1142
1224
|
};
|
|
1143
1225
|
}
|
|
1144
|
-
|
|
1226
|
+
// The non-live path is the DAMAGING one: `status` and `phases_complete` below are derived
|
|
1227
|
+
// from this list, so an unresolved chart id did not merely omit the records — it answered
|
|
1228
|
+
// "unknown" and "0 phases" about a performance that had demonstrably done work.
|
|
1229
|
+
const outs = deps.outputs.all().filter(ofPerformance);
|
|
1145
1230
|
const entry = deps.ledger.query({ kind: "gig", gig_id: gid })[0];
|
|
1146
1231
|
return {
|
|
1147
1232
|
ok: true, requires_approval: approval,
|
|
@@ -1201,7 +1286,12 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1201
1286
|
// plain string slugs. Now: when an entry is a string or a slug-only
|
|
1202
1287
|
// object, look it up in deps.agents (populated by bootstrap from the
|
|
1203
1288
|
// loaded genome). Full Agent objects are passed through unchanged.
|
|
1204
|
-
|
|
1289
|
+
// The input_schema advertises BOTH keys: `agents` is the compose-input shape; `agent_slugs`
|
|
1290
|
+
// is the shape the PERSISTED standards/<slug>.json file carries (written at line ~1413).
|
|
1291
|
+
// Reading only `agents` made `agent_slugs` a silent no-op that failed via the misleading
|
|
1292
|
+
// "agent not found" error — and broke the read-file/re-compose round-trip. Fall back to
|
|
1293
|
+
// `agent_slugs` when `agents` is absent so the documented parameter actually composes.
|
|
1294
|
+
const sAgentsRaw = args["agents"] ?? args["agent_slugs"] ?? [];
|
|
1205
1295
|
const sAgents = sAgentsRaw.map((a) => {
|
|
1206
1296
|
if (typeof a === "string") {
|
|
1207
1297
|
const loaded = deps.agents?.get(a);
|
|
@@ -1313,6 +1403,20 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1313
1403
|
...(args["credential_surface"] !== undefined ? { credential_surface: args["credential_surface"] } : {}),
|
|
1314
1404
|
...(args["lifecycle"] !== undefined ? { lifecycle: args["lifecycle"] } : {}),
|
|
1315
1405
|
...(args["responsible_chair"] !== undefined ? { responsible_chair: args["responsible_chair"] } : {}),
|
|
1406
|
+
// The worker-contract fields, passed through by the same rule: only what was stated, so the
|
|
1407
|
+
// schema keeps ownership of every default (an unstated `mcp_servers`/`devices`/`architectures`
|
|
1408
|
+
// is the EMPTY list, an unstated `substrate`/`floor`/`max_concurrent_chairs` the deployment
|
|
1409
|
+
// default). Read here so a room authored through this tool actually carries its substrate,
|
|
1410
|
+
// and so the advertised schema and the handler stay one statement of the same fact (#234).
|
|
1411
|
+
...(args["substrate"] !== undefined ? { substrate: args["substrate"] } : {}),
|
|
1412
|
+
...(args["mcp_servers"] !== undefined ? { mcp_servers: args["mcp_servers"] } : {}),
|
|
1413
|
+
...(args["devices"] !== undefined ? { devices: args["devices"] } : {}),
|
|
1414
|
+
...(args["architectures"] !== undefined ? { architectures: args["architectures"] } : {}),
|
|
1415
|
+
...(args["max_concurrent_chairs"] !== undefined ? { max_concurrent_chairs: args["max_concurrent_chairs"] } : {}),
|
|
1416
|
+
...(args["floor"] !== undefined ? { floor: args["floor"] } : {}),
|
|
1417
|
+
// NO repo_url here: the repository is the SUBJECT of a RUN, named at dispatch, not an
|
|
1418
|
+
// at-rest venue field. A venue serves many repositories; pinning one on the room would mint
|
|
1419
|
+
// a venue per repository. See VenueObjectSchema's gap note.
|
|
1316
1420
|
};
|
|
1317
1421
|
const parsedVenue = VenueSchema.safeParse(venueInputDef);
|
|
1318
1422
|
if (!parsedVenue.success) {
|
|
@@ -1391,8 +1495,32 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1391
1495
|
args["fields_to_add"] ?? {};
|
|
1392
1496
|
const nextProps = { ...baseProps, ...addProps };
|
|
1393
1497
|
const nextRequired = extension?.schema?.required ?? baseDef.required_fields;
|
|
1498
|
+
// A MUTATION THAT CHANGES NOTHING SAYS SO. `addProps` reads exactly two shapes —
|
|
1499
|
+
// `extension.schema.properties` and `fields_to_add`. An `extension` supplied in any OTHER
|
|
1500
|
+
// shape (top-level JSON Schema keywords, say) matches neither, so addProps is {} and the
|
|
1501
|
+
// caller's intent is DROPPED — while this handler went on to bump the version and seal
|
|
1502
|
+
// content/effective hashes, reporting "additive: +0 field(s)" as success. The caller is told
|
|
1503
|
+
// the genome moved when it did not, and learns otherwise only downstream, when the thing
|
|
1504
|
+
// they authored never fires. Observed 2026-08-20 authoring a conditional constraint.
|
|
1505
|
+
// Sibling of the agent_evolve no-op reported in #325; both refuse here rather than guess.
|
|
1506
|
+
const addedNothing = Object.keys(addProps).length === 0;
|
|
1507
|
+
const requiredUnchanged = JSON.stringify([...nextRequired].sort()) === JSON.stringify([...baseDef.required_fields].sort());
|
|
1508
|
+
if (addedNothing && requiredUnchanged) {
|
|
1509
|
+
return {
|
|
1510
|
+
ok: false,
|
|
1511
|
+
requires_approval: approval,
|
|
1512
|
+
error: `type_extend would change nothing about "${baseDef.slug}" — no field was added and ` +
|
|
1513
|
+
`required_fields is unchanged, so no version is warranted. Field additions are read ` +
|
|
1514
|
+
`from \`extension.schema.properties\` or \`fields_to_add\`; an extension supplied in ` +
|
|
1515
|
+
`any other shape (e.g. top-level JSON Schema keywords) is not read by this verb.`,
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1394
1518
|
const base = {
|
|
1395
|
-
|
|
1519
|
+
// Read the base's OWN version (widened onto DomainType) so proposeTypeChange computes
|
|
1520
|
+
// next_version = base.version + 1 from reality, not a constant. `?? 1` covers the freshly
|
|
1521
|
+
// registered projection that carries no version (always v1). The hardcoded 1 here forced
|
|
1522
|
+
// every extend to next_version 2 — the first half of PR #433 AC6's second-extend defect.
|
|
1523
|
+
slug: baseDef.slug, version: baseDef.version ?? 1, extends: baseDef.extends, domain: baseDef.domain,
|
|
1396
1524
|
status: "active", schema: { type: "object", properties: baseProps }, required_fields: baseDef.required_fields,
|
|
1397
1525
|
};
|
|
1398
1526
|
// THE THIRD DOOR. `{...baseProps, ...addProps}` above is the exact merge #264 is
|
|
@@ -1418,10 +1546,37 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1418
1546
|
};
|
|
1419
1547
|
const proposal = proposeTypeChange(base, next);
|
|
1420
1548
|
const newFields = Object.keys(nextProps).length - Object.keys(baseProps).length;
|
|
1421
|
-
// substrate seal: the
|
|
1422
|
-
//
|
|
1549
|
+
// substrate seal: PERSIST the extended definition the way type_register does, rather
|
|
1550
|
+
// than only recording its identity in the ledger. The prior `recordIdentity` path
|
|
1551
|
+
// deferred file materialization to a "version-aware loader path" that does not exist —
|
|
1552
|
+
// so `ok:true` named a version nothing wrote: a fresh load still resolved the old
|
|
1553
|
+
// definition, and the sealed genome_mutation row asserted a state the genome did not
|
|
1554
|
+
// hold. `sealDefinition` seals-before-writes and materializes a loadable file, so an
|
|
1555
|
+
// `ok:true` now means the type is resolvable. The loader keys domain types by version
|
|
1556
|
+
// from file CONTENT and DomainTypeMap.get returns the highest-version record, so the
|
|
1557
|
+
// in-place overwrite of `domain_types/<slug>.json` at the bumped version resolves today
|
|
1558
|
+
// with no versioned filename — an `@v` file would be the same defect wearing a version
|
|
1559
|
+
// number. The ledger subject stays the VERSIONED identity `<slug>@v<n>` (its content
|
|
1560
|
+
// hash), while the file is the bare `<slug>.json` the loader reads.
|
|
1423
1561
|
const versioned = { ...next, version: proposal.next_version };
|
|
1424
|
-
const tx = deps.genome_dir
|
|
1562
|
+
const tx = deps.genome_dir
|
|
1563
|
+
? sealDefinition("type_extend", `${base.slug}@v${proposal.next_version}`, versioned, deps.ledger, deps.genome_dir, "domain_types", args["reason"] != null ? { reason: args["reason"] } : undefined, base.slug)
|
|
1564
|
+
: undefined;
|
|
1565
|
+
// Refresh the in-memory registry to match what was just sealed to disk. sealDefinition
|
|
1566
|
+
// overwrites domain_types/<slug>.json at the bumped version, but the registry map still
|
|
1567
|
+
// holds the pre-extend entry — so a SECOND type_extend on the same slug would re-read the
|
|
1568
|
+
// stale version and compute next_version from it again (v1→v2→v2 instead of v3). That is
|
|
1569
|
+
// the second half of PR #433 AC6's defect: the write became real, so the collision became
|
|
1570
|
+
// real. Swap just this slug's entry (bumped version + the merged schema/required_fields
|
|
1571
|
+
// just sealed) into a snapshot and replaceTypes it. replaceTypes — not registerType — is
|
|
1572
|
+
// used deliberately: registerType runs reuse enforcement via score(), and an extended type
|
|
1573
|
+
// scores >=80 against its own prior version (score() never inspects slug), so registerType
|
|
1574
|
+
// would REFUSE it. replaceTypes is the existing bypass (genome_reload uses it) and is a
|
|
1575
|
+
// sync, not authorship. See rationale in the sealed change-set re: score()'s slug-blindness.
|
|
1576
|
+
const snapshot = deps.registry.listTypes().map((t) => t.slug === base.slug
|
|
1577
|
+
? { ...t, version: proposal.next_version, schema: { type: "object", properties: nextProps }, required_fields: [...nextRequired] }
|
|
1578
|
+
: t);
|
|
1579
|
+
deps.registry.replaceTypes(snapshot);
|
|
1425
1580
|
return { ok: true, requires_approval: proposal.approval_required, data: { new_version: proposal.next_version, changelog_entry: `${proposal.change_class}: +${newFields} field(s)`, change_class: proposal.change_class, effective_hash: tx?.effective_hash, content_hash: tx?.content_hash } };
|
|
1426
1581
|
}
|
|
1427
1582
|
case "charter_read": {
|
|
@@ -1574,6 +1729,9 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1574
1729
|
domain: d.domain,
|
|
1575
1730
|
schema: d.schema,
|
|
1576
1731
|
required_fields: [...d.required_fields],
|
|
1732
|
+
// Carry the on-disk version through the reload so a version decision reads it (mirrors
|
|
1733
|
+
// loadRegistry). Editing domain_types/ and reloading must not reset a type to version-less.
|
|
1734
|
+
version: d.version,
|
|
1577
1735
|
}));
|
|
1578
1736
|
const typeDiff = deps.registry.replaceTypes(typeDefs);
|
|
1579
1737
|
// standards — mutate in place so callers holding deps.standards see updates.
|
|
@@ -1948,6 +2106,12 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1948
2106
|
}
|
|
1949
2107
|
// The substrate loop: validate → canonical hash → (if genome_dir) persist + ledger-seal.
|
|
1950
2108
|
const sealed = sealAgentDefinition(def, deps.ledger, deps.genome_dir);
|
|
2109
|
+
// Write-through to the LIVE map so a same-session standard_compose resolves this agent
|
|
2110
|
+
// WITHOUT a genome_reload. agent_define wrote agents/<slug>.json but never refreshed
|
|
2111
|
+
// deps.agents, so standard_compose (which resolves slugs from deps.agents) denied an agent
|
|
2112
|
+
// this very tool had just created. This mirrors the write-through standard_compose does for
|
|
2113
|
+
// deps.standards and tool_register does for deps.toolProviders.
|
|
2114
|
+
deps.agents?.set(sealed.agent.slug, sealed.agent);
|
|
1951
2115
|
return {
|
|
1952
2116
|
ok: true,
|
|
1953
2117
|
requires_approval: approval,
|
|
@@ -2025,6 +2189,20 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
2025
2189
|
const evolveSlug = typeof args["slug"] === "string" ? args["slug"] : undefined;
|
|
2026
2190
|
const changes = (args["changes"] && typeof args["changes"] === "object")
|
|
2027
2191
|
? args["changes"] : undefined;
|
|
2192
|
+
// A MUTATION THAT CHANGES NOTHING SAYS SO (#325). Field edits are read ONLY from a
|
|
2193
|
+
// `changes` object. A caller who puts them at the top level leaves `changes` undefined, the
|
|
2194
|
+
// guarded block below is skipped, and this case used to fall through to `ok: true` with a
|
|
2195
|
+
// `new_version` — success, a version bump, and nothing changed. Refuse instead, naming the
|
|
2196
|
+
// shape, so the caller learns in one call rather than discovering it when the edit is absent.
|
|
2197
|
+
if (evolveSlug && !changes) {
|
|
2198
|
+
return {
|
|
2199
|
+
ok: false,
|
|
2200
|
+
requires_approval: approval,
|
|
2201
|
+
error: `agent_evolve: no \`changes\` object was supplied for "${evolveSlug}", so nothing ` +
|
|
2202
|
+
`would be applied. Wrap the field edits in a \`changes\` object — top-level fields are ` +
|
|
2203
|
+
`not read by this verb, and a call that changes nothing warrants no new version.`,
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2028
2206
|
if (evolveSlug && changes && (deps.genome_dir || deps.agents?.has(evolveSlug))) {
|
|
2029
2207
|
// The base definition: the genome file when a working tree exists, else the loaded
|
|
2030
2208
|
// agents map (a hosted surface has no filesystem — the STORE genome is the base,
|
|
@@ -2832,6 +3010,49 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
2832
3010
|
},
|
|
2833
3011
|
};
|
|
2834
3012
|
}
|
|
3013
|
+
case "org_hire": {
|
|
3014
|
+
// NOT the live path — admission is intercepted in callSurfaceTool (which holds the caller
|
|
3015
|
+
// identity and the deps.hireMember backend that dispatchTool never receives). This block
|
|
3016
|
+
// exists, like venue_credential_mint's, so the verb's advertised schema has a matching
|
|
3017
|
+
// handler that reads exactly its two arguments (advertised_args_are_read.test.ts), and it
|
|
3018
|
+
// must live INSIDE this case body so those reads are attributed to org_hire, not the
|
|
3019
|
+
// preceding case. Reaching this at runtime means the surface interception was bypassed —
|
|
3020
|
+
// answer honestly rather than pretend a hire happened.
|
|
3021
|
+
//
|
|
3022
|
+
// ORDER MATTERS: this case sits BEFORE venue_credential_mint so venue_credential_mint stays
|
|
3023
|
+
// the LAST string case in dispatchTool. The advertised_args_are_read parser slices the last
|
|
3024
|
+
// case's body to end-of-file, sweeping in callSurfaceTool's own `args["…"]` reads; keeping
|
|
3025
|
+
// venue last means that slurped tail's bracket reads stay {org_slug, instance} — exactly
|
|
3026
|
+
// venue's schema. (The org_hire callSurfaceTool intercept reads its args by dot access for
|
|
3027
|
+
// the same reason, so it contributes nothing to that tail.)
|
|
3028
|
+
const org_slug = String(args["org_slug"] ?? "");
|
|
3029
|
+
const agent_slug = String(args["agent_slug"] ?? "");
|
|
3030
|
+
return {
|
|
3031
|
+
ok: false, refusal: "no_backend", requires_approval: approval,
|
|
3032
|
+
error: `org_hire is served by the tool surface (createToolSurface), which wires the caller and ` +
|
|
3033
|
+
`the deps.hireMember backend; the bare dispatcher cannot admit agent "${agent_slug}" to ` +
|
|
3034
|
+
`org "${org_slug}". Call it through the surface.`,
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
case "venue_credential_mint": {
|
|
3038
|
+
// NOT the live path — minting is intercepted in callSurfaceTool (which holds the caller
|
|
3039
|
+
// identity and the deps.mintVenueCredential backend that dispatchTool never receives).
|
|
3040
|
+
// This block exists so the verb's advertised schema has a matching handler reading exactly
|
|
3041
|
+
// its two arguments, and so it must live INSIDE this case body, not before the label:
|
|
3042
|
+
// tests/advertised_args_are_read.test.ts slices each case's body from its own label to the
|
|
3043
|
+
// next one, so any argument-read text placed above this label would be attributed to the
|
|
3044
|
+
// PRECEDING case (learning_synthesize) — a control reported against a tool that never took
|
|
3045
|
+
// it. Reaching this block at runtime means the surface interception was bypassed — answer
|
|
3046
|
+
// honestly rather than pretend a mint happened.
|
|
3047
|
+
const org_slug = String(args["org_slug"] ?? "");
|
|
3048
|
+
const instance = String(args["instance"] ?? "");
|
|
3049
|
+
return {
|
|
3050
|
+
ok: false, refusal: "no_backend", requires_approval: approval,
|
|
3051
|
+
error: `venue_credential_mint is served by the tool surface (createToolSurface), which wires the ` +
|
|
3052
|
+
`caller and the deps.mintVenueCredential backend; the bare dispatcher cannot mint for ` +
|
|
3053
|
+
`org "${org_slug}" instance "${instance}". Call it through the surface.`,
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
2835
3056
|
default:
|
|
2836
3057
|
return { ok: false, not_implemented: true, requires_approval: approval, error: `"${slug}" has no v0 handler` };
|
|
2837
3058
|
}
|
|
@@ -2876,9 +3097,147 @@ const HOSTED_UPSERT = {
|
|
|
2876
3097
|
// class travels the port it is supposed to travel, and the missing half announces itself instead
|
|
2877
3098
|
// of the engine quietly declining to try. (Store-side work: two tables + two upsert branches.)
|
|
2878
3099
|
chart_define: { cls: "chart", keys: Object.keys(ChartSchema.shape) },
|
|
2879
|
-
venue_define: { cls: "venue", keys: Object.keys(
|
|
3100
|
+
venue_define: { cls: "venue", keys: Object.keys(VenueObjectSchema.shape) },
|
|
2880
3101
|
};
|
|
2881
3102
|
async function callSurfaceTool(slug, args, deps) {
|
|
3103
|
+
if (slug === "venue_credential_mint") {
|
|
3104
|
+
// The engine half of the venue credential: shape validation and the three refusals around
|
|
3105
|
+
// whatever backend a deployment injects. This fires for ALL callers, not only hosted, because
|
|
3106
|
+
// its refusals are structural facts about the credential — not a hosted-transport concern.
|
|
3107
|
+
//
|
|
3108
|
+
// (a) A gig-scoped caller may NOT mint a venue credential. This is decided from caller identity
|
|
3109
|
+
// alone, BEFORE the backend is reached, because the escalation (a one-lease gig token
|
|
3110
|
+
// minting an org-scoped key that outlives every gig) is a credential-scope fact no store-side
|
|
3111
|
+
// gate catches. A refused mint must never touch the backend.
|
|
3112
|
+
const escalation = gigScopeRefusal(deps.caller);
|
|
3113
|
+
if (escalation) {
|
|
3114
|
+
return {
|
|
3115
|
+
ok: false,
|
|
3116
|
+
refusal: escalation,
|
|
3117
|
+
error: "a gig-scoped credential may not mint a venue credential: a gig token is issued to one " +
|
|
3118
|
+
"agent for one gig and expires with that gig's lease, while a venue credential is " +
|
|
3119
|
+
"org-scoped and outlives every gig. Mint from a member or venue credential instead.",
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
// (b) No backend wired → the verb answers honestly rather than throwing, naming the seam to
|
|
3123
|
+
// wire (the same shape gig_dispatch/gig_approve/gig_cancel use when their store seams are
|
|
3124
|
+
// absent). A caller that cannot tell "minting is unwired here" from "your request was bad"
|
|
3125
|
+
// retries the wrong thing forever.
|
|
3126
|
+
if (!deps.mintVenueCredential) {
|
|
3127
|
+
return {
|
|
3128
|
+
ok: false,
|
|
3129
|
+
refusal: "no_backend",
|
|
3130
|
+
error: "no minting backend is wired on this surface — venue_credential_mint ships its schema " +
|
|
3131
|
+
"and refusals, but a deployment supplies the credential. Wire deps.mintVenueCredential " +
|
|
3132
|
+
"(parallel to deps.queueGig) to stand up the worker environment.",
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
// (c) Mint, then check the grant is COMPLETE. A backend that answers with a half-set is refused,
|
|
3136
|
+
// not forwarded — handing an incomplete environment back moves the assembly problem to the
|
|
3137
|
+
// caller while looking like success, which is the failure this verb exists to end.
|
|
3138
|
+
let grant;
|
|
3139
|
+
try {
|
|
3140
|
+
grant = await deps.mintVenueCredential({
|
|
3141
|
+
org_slug: String(args["org_slug"] ?? ""),
|
|
3142
|
+
instance: String(args["instance"] ?? ""),
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
catch (e) {
|
|
3146
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
3147
|
+
}
|
|
3148
|
+
const missing = missingWorkerEnv(grant.env);
|
|
3149
|
+
if (missing.length) {
|
|
3150
|
+
return {
|
|
3151
|
+
ok: false,
|
|
3152
|
+
refusal: "incomplete_env",
|
|
3153
|
+
error: `the minted grant is missing required worker environment: ${missing.join(", ")}. A grant ` +
|
|
3154
|
+
"that is not complete is refused, not returned — a half-set moves the assembly problem to " +
|
|
3155
|
+
"the caller while looking like success.",
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
// (d) The class names pass through UNCHANGED — the engine does not validate class vocabulary
|
|
3159
|
+
// (that is the room contract's job, checked by realize before dispatch). The grant is the
|
|
3160
|
+
// answer, returned exactly once; the engine does not persist it and there is no read-back.
|
|
3161
|
+
return { ok: true, data: grant };
|
|
3162
|
+
}
|
|
3163
|
+
if (slug === "org_hire") {
|
|
3164
|
+
// The engine half of org admission: the two engine-decided refusals and the ledger seal around
|
|
3165
|
+
// whatever backend a deployment injects. Intercepted here (like venue_credential_mint) for ALL
|
|
3166
|
+
// callers, BEFORE the hosted check, so its refusals are structural facts about the act — not a
|
|
3167
|
+
// hosted-transport concern — and so it never reaches a store upsert path.
|
|
3168
|
+
//
|
|
3169
|
+
// These two args are read by DOT access, not `args["…"]`, on purpose: the
|
|
3170
|
+
// advertised_args_are_read parser slurps the LAST dispatchTool case's body to end-of-file and
|
|
3171
|
+
// counts every bracket-with-string-literal read in it as that case's reads (this comment must
|
|
3172
|
+
// not spell that pattern out, or the parser counts the EXPLANATION as an instance of the thing
|
|
3173
|
+
// it explains — which is exactly how it first went red). venue_credential_mint is the last
|
|
3174
|
+
// case (org_hire sits before it precisely to keep it so); a bracket read of `agent_slug` here
|
|
3175
|
+
// would be miscounted as venue reading an arg it does not advertise. Dot access is invisible to
|
|
3176
|
+
// that parser and keeps the slurped tail's reads honestly {org_slug, instance}.
|
|
3177
|
+
const org_slug = String(args.org_slug ?? "");
|
|
3178
|
+
const agent_slug = String(args.agent_slug ?? "");
|
|
3179
|
+
// (a) HIRING IS NEVER SELF-SERVICE. Only a human 'member' caller may admit an agent; a
|
|
3180
|
+
// player/venue/gig token is an AGENT token, and an agent may not admit an agent — that is
|
|
3181
|
+
// the whole point of the verb. Decided from caller identity ALONE, before the backend is
|
|
3182
|
+
// reached, so a refused hire never touches deps.hireMember. Absent caller (a bare surface)
|
|
3183
|
+
// is not a member either, so it is refused too — the gate fails closed.
|
|
3184
|
+
if (deps.caller?.kind !== "member") {
|
|
3185
|
+
return {
|
|
3186
|
+
ok: false,
|
|
3187
|
+
refusal: "not_a_human_member",
|
|
3188
|
+
error: "hiring is never self-service: only a human member may admit an agent to an org. This " +
|
|
3189
|
+
"caller presented an agent token, and an agent may not hire an agent. ADMISSION IS NOT " +
|
|
3190
|
+
"AUTHORITY — a member performs the hire.",
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
// (b) No backend wired → the verb answers honestly rather than throwing, naming the seam to
|
|
3194
|
+
// wire (the same shape venue_credential_mint uses). A caller that cannot tell "hiring is
|
|
3195
|
+
// unwired here" from "your request was bad" retries the wrong thing forever.
|
|
3196
|
+
if (!deps.hireMember) {
|
|
3197
|
+
return {
|
|
3198
|
+
ok: false,
|
|
3199
|
+
refusal: "no_backend",
|
|
3200
|
+
error: "no admission backend is wired on this surface — org_hire ships its schema and refusals, " +
|
|
3201
|
+
"but a deployment supplies the insert. Wire deps.hireMember (parallel to " +
|
|
3202
|
+
"deps.mintVenueCredential) to admit the agent to the org.",
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
// (c) Admit, then map the store's TYPED answer. Existence (`unknown_agent`) and idempotency
|
|
3206
|
+
// (`already_member`) are facts only the store holds — the engine never checks the
|
|
3207
|
+
// agent_record's status ('named'/'active'), because governance and naming are separate acts
|
|
3208
|
+
// (coltrane-proposer is active yet was never named). Existence is the ONLY precondition, and
|
|
3209
|
+
// it is the store's answer, not the engine's.
|
|
3210
|
+
let result;
|
|
3211
|
+
try {
|
|
3212
|
+
result = await deps.hireMember({ org_slug, agent_slug });
|
|
3213
|
+
}
|
|
3214
|
+
catch (e) {
|
|
3215
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
3216
|
+
}
|
|
3217
|
+
if (!result.ok) {
|
|
3218
|
+
// A refused hire seals NOTHING — the ledger row is written only inside the {ok:true} branch
|
|
3219
|
+
// below, so a hire that did not happen leaves no trace claiming it did.
|
|
3220
|
+
return { ok: false, refusal: result.code };
|
|
3221
|
+
}
|
|
3222
|
+
// (d) SEAL THE ACT to the ledger BEFORE reporting success — a kind:"genome_mutation" row via
|
|
3223
|
+
// recordIdentity (ledger-only, NOT sealDefinition: a hire writes no genome file), so
|
|
3224
|
+
// who-hired-whom, when, and on whose authority lives in the append-only chain rather than
|
|
3225
|
+
// only in a database row. subject_slug is the agent admitted; event is 'org_hire'; org_slug
|
|
3226
|
+
// rides in the hashed detail. #218 — if the audit row does not land, say so (the store row
|
|
3227
|
+
// may already exist), rather than reporting a success whose seal never happened.
|
|
3228
|
+
try {
|
|
3229
|
+
recordIdentity("org_hire", agent_slug, { org_slug, agent_slug }, deps.ledger, { org_slug });
|
|
3230
|
+
}
|
|
3231
|
+
catch (e) {
|
|
3232
|
+
if (e instanceof LedgerError) {
|
|
3233
|
+
return { ok: false, audit_write_failed: true, error: `audit write failed — "org_hire" was NOT sealed: ${e.message}` };
|
|
3234
|
+
}
|
|
3235
|
+
throw e;
|
|
3236
|
+
}
|
|
3237
|
+
// (e) Admission REPORTS the belonging it created — the {org_slug, agent_slug} pair, and nothing
|
|
3238
|
+
// carrying authority.
|
|
3239
|
+
return { ok: true, data: { org_slug, agent_slug } };
|
|
3240
|
+
}
|
|
2882
3241
|
if (deps.hosted) {
|
|
2883
3242
|
const blocked = HOSTED_BLOCKED[slug];
|
|
2884
3243
|
if (blocked)
|
|
@@ -2984,6 +3343,13 @@ export function createToolSurface(deps) {
|
|
|
2984
3343
|
call: (args) => callSurfaceTool(t.slug, args, deps),
|
|
2985
3344
|
}));
|
|
2986
3345
|
}
|
|
3346
|
+
// Reserved relay↔child request schemas (restart guard, venue/8). The MCP SDK keys a request
|
|
3347
|
+
// handler by the `method` literal in its schema; `.passthrough()` lets the full JSON-RPC envelope
|
|
3348
|
+
// (jsonrpc/id/params) pass validation. These carry no params — the relay asks, the child answers
|
|
3349
|
+
// from deps.gig_runs. Registering them as request handlers (not tools) is what keeps them off
|
|
3350
|
+
// tools/list, so they are invisible to Claude Code and callable only by the parent relay.
|
|
3351
|
+
const RunningGigsRequestSchema = z.object({ method: z.literal(RUNNING_GIGS_METHOD) }).passthrough();
|
|
3352
|
+
const AbortForRestartRequestSchema = z.object({ method: z.literal(ABORT_FOR_RESTART_METHOD) }).passthrough();
|
|
2987
3353
|
/** Build the low-level MCP Server with ListTools + CallTool wired to the tool surface. */
|
|
2988
3354
|
export function createColtraneServer(deps, recorder) {
|
|
2989
3355
|
const server = new Server({ name: "coltrane", version: COLTRANE_VERSION }, { capabilities: { tools: {} } });
|
|
@@ -3011,6 +3377,45 @@ export function createColtraneServer(deps, recorder) {
|
|
|
3011
3377
|
isError: !result.ok,
|
|
3012
3378
|
};
|
|
3013
3379
|
});
|
|
3380
|
+
// ── Restart guard (venue/8): the child's half of the relay↔child conversation ──────────────
|
|
3381
|
+
// The stdio relay (src/server_relay.ts) holds NO gig state, so before it kills this child to pick
|
|
3382
|
+
// up new bytes it ASKS the child what a restart would destroy. These two reserved methods answer.
|
|
3383
|
+
// They are deliberately NOT MCP tools — they are not in `createToolSurface`, so they never appear
|
|
3384
|
+
// in tools/list and Claude Code cannot call them; only the parent relay, over the same stdio pipe,
|
|
3385
|
+
// does. That is what keeps the relay's blindness structural: it asks, the child answers from the
|
|
3386
|
+
// ONE authority (deps.gig_runs), the relay acts on the answer.
|
|
3387
|
+
const runningGigIds = () => [...(deps.gig_runs?.entries() ?? [])]
|
|
3388
|
+
.filter(([, s]) => s.status === "running")
|
|
3389
|
+
.map(([id]) => id);
|
|
3390
|
+
// "Which gigs are running right now?" — the pre-restart check. Answering with the empty list is a
|
|
3391
|
+
// POSITIVE statement ("nothing in flight, restart freely"); the relay only treats a NON-answer
|
|
3392
|
+
// (timeout) as unhealthy, never an empty answer.
|
|
3393
|
+
server.setRequestHandler(RunningGigsRequestSchema, async () => ({ running: runningGigIds() }));
|
|
3394
|
+
// The FORCE path: the operator chose to restart with gigs in flight. Abort each running gig and —
|
|
3395
|
+
// the whole point — LEDGER the abort BEFORE this child dies, so a killed gig is a recorded fact
|
|
3396
|
+
// rather than an absence (the defect: two publish seats, gigs 8146142e / 18726459, died mid-phase
|
|
3397
|
+
// with sealed outputs banked and no row saying they were killed). This reuses the EXACT sanctioned
|
|
3398
|
+
// gig_abort path: mark the run, abort its controller, then governanceRow('gig_abort', …).
|
|
3399
|
+
server.setRequestHandler(AbortForRestartRequestSchema, async () => {
|
|
3400
|
+
const aborted = [];
|
|
3401
|
+
for (const [gid, live] of deps.gig_runs?.entries() ?? []) {
|
|
3402
|
+
if (live.status !== "running")
|
|
3403
|
+
continue;
|
|
3404
|
+
live.abort_requested = true;
|
|
3405
|
+
live.abort_reason = "server_restart override";
|
|
3406
|
+
let cancelled = false;
|
|
3407
|
+
if (live.controller) {
|
|
3408
|
+
try {
|
|
3409
|
+
live.controller.abort(live.abort_reason);
|
|
3410
|
+
}
|
|
3411
|
+
catch { /* an already-aborted signal is fine */ }
|
|
3412
|
+
cancelled = true;
|
|
3413
|
+
}
|
|
3414
|
+
deps.ledger.append(governanceRow("gig_abort", gid, { reason: "server_restart override", status: "aborting", cancelled }, gid));
|
|
3415
|
+
aborted.push(gid);
|
|
3416
|
+
}
|
|
3417
|
+
return { aborted };
|
|
3418
|
+
});
|
|
3014
3419
|
return server;
|
|
3015
3420
|
}
|
|
3016
3421
|
/**
|
|
@@ -3101,6 +3506,12 @@ export function bootstrapServerDeps(genomeRoot) {
|
|
|
3101
3506
|
standards: genome.standards, // ← gig_dispatch can now resolve file-defined standards
|
|
3102
3507
|
charts: genome.charts, // ← gig_dispatch resolves a chart_slug; chart_browse lists them
|
|
3103
3508
|
venues: genome.venues, // ← the ceiling a chart's venue imposes has to resolve to something
|
|
3509
|
+
// The production construction of a realizer — the wire from dispatch to the container substrate.
|
|
3510
|
+
// A venue-with-mcp_servers gig is stood up on this (real docker by default; the seam's `run` is
|
|
3511
|
+
// the daemon-free test substitute). Before this, dockerComposeRealizer was defined and reachable
|
|
3512
|
+
// from nowhere in src/, so a venue-named gig got no room. runGig only realizes when the venue
|
|
3513
|
+
// declares servers, so wiring it here changes no server-less venue's behaviour.
|
|
3514
|
+
venueRealizer: dockerComposeRealizer(),
|
|
3104
3515
|
invoke: makeClaudeInvoker({
|
|
3105
3516
|
registry,
|
|
3106
3517
|
model: process.env["COLTRANE_MODEL"],
|