diff --git a/README.md b/README.md index 4e990b5..c59d4f2 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ later without renumbering anything. | `VERSIONS.md` | The verified dependency versions, generated from `lib/trinity/versions.ex` | | `CLAUDE.md` | The engineering contract: slice rules, definition of done, proof standard | | `docs/` | Vision, architecture, tech stack, conventions, slice process, data model, risks, security model, standards; packaging, the FIPS leg, backup and restore, performance measurements | +| `slices/059-mcp-library-spike/FINDINGS.md` | What the MCP server core (`beam_mcp`) ships, carries, refuses or leaves open against the 2026-07-28 checklist; the reference for the MCP phase | | `docs/adr/` | Architecture decision records. One is added whenever a decision changes | | `slices/` | One folder per slice: specification, notes and proof | | `templates/` | The templates a new slice, proof or decision record starts from | @@ -175,11 +176,12 @@ every project space. ## Related projects [beam_mcp](https://github.com/ScriptKittyOS/beam_mcp) is a Model Context Protocol server core for -the BEAM from the same organisation, on Hex as `beam_mcp`. Trinity does not depend on it today. -Trinity's MCP layer (milestone M5a) is chosen by measurement in slice 059, where beam_mcp is one -of the candidates for the server side; the client side and the authorization server are Trinity's -own work whichever library is chosen. `docs/adr/0007-mcp-2026-07-28-target-and-library.md` -records the protocol target and how the choice is made. +the BEAM from the same organisation, on Hex as `beam_mcp`. Trinity depends on it since slice 059, +pinned at 0.8.0 and reached only through the `Trinity.MCP` boundary; slice 059's `FINDINGS.md` +measures what it ships against the 2026-07-28 checklist, and the MCP phase (milestone M5a) builds +Trinity's client driver, the approval loop and authorization above it. The client side and the +authorization server are Trinity's own work. `docs/adr/0007-mcp-2026-07-28-target-and-library.md` +records the protocol target and the layering. ## License diff --git a/ROADMAP.md b/ROADMAP.md index 61a52db..4cc0430 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -52,7 +52,7 @@ standards register names the rows that ask for them. | 040 | Skills registry + agentskills.io format + progressive disclosure | 4 Skills | M | 020 | approved | | 041 | Skill self-management with staged approval + scanner | 4 Skills | M | 040, 021 | approved | | 050 | Scheduler: Oban cron agent tasks with delivery targets | 5 Automation | M | 012 | planned | -| 059 | MCP capability gap against beam_mcp, and the server seam probe | 6 MCP | S/M | 020 | planned | +| 059 | MCP capability gap against beam_mcp, and the server seam probe | 6 MCP | S/M | 020 | done | | 060 | MCP client: Trinity's thin driver (2026-07-28 preferred, 2025-11-25 compat, MRTR, Tasks) | 6 MCP | L | 059, 021 | planned | | 061 | MCP server (stateless 2026-07-28 + compat, MRTR approvals, headless profile) | 6 MCP | M | 060, 024 | planned | | 062 | MCP authorization: OAuth client role, RS, embedded AS, Enterprise Managed Authorization (ID-JAG) | 6 MCP | L | 061 | planned | diff --git a/VERSIONS.md b/VERSIONS.md index 1736342..1981861 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -91,7 +91,7 @@ never pin a version hex marks as retired or vulnerable. | `oban` | ~> 2.24 | πŸ” not yet a dependency | Uses `Oban.Engines.Lite` on SQLite. ⚠️ Oban Pro Workflows/Smart engine are Postgres-only. Added at Slice 050. | | `req` | ~> 0.5 | βœ… in `mix.lock` | HTTP client. | | `req_llm` | ~> 1.22 | βœ… in `mix.lock` | Provider layer (streaming, tools, structured output, usage). ⚠️ The pin was `~> 1.10` against a recorded latest of 1.10.0; the real latest was twelve minors ahead. Check event shapes against the current version at Slice 011, not against this file's prose. Added at Slice 011. | -| `beam_mcp` | ~> 0.8 | πŸ” not yet a dependency | MCP server core, Apache-2.0, ADR-0007 decision 5 (owner decision 2026-09-08, recorded 2026-09-20). 0.8.0 on hex.pm, standing before 1.0.0. Server side only: the client, MRTR and OAuth are Trinity's, above it. Added at Slice 059. The earlier candidate list (anubis_mcp, fastest_mcp, gen_mcp) is history. | +| `beam_mcp` | ~> 0.8 | βœ… in `mix.lock` | MCP server core, Apache-2.0, ADR-0007 decision 5 (owner decision 2026-09-08, recorded 2026-09-20). 0.8.0 on hex.pm, standing before 1.0.0. Server side only: the client, MRTR and OAuth are Trinity's, above it. Added at Slice 059. The earlier candidate list (anubis_mcp, fastest_mcp, gen_mcp) is history. | | `jido` | not used (ADR-0009, decided 2026-09-20) | πŸ” not a single package | Measured at the Slice 012 checkpoint and not adopted: the agent runtime duplicates PubSub, Oban and the gateways and adds a second tool executor; the action shape is written in-tree at Slice 020 with `jsv` for its schemas. The row stays so the decision is visible where a reader would look for the package. | | `jason` | ~> 1.2 | βœ… in `mix.lock` | | | `boundary` | ~> 0.10 | βœ… in `mix.lock` | Compile-time module dependency enforcement. Measured at Slice 000: it compiles and enforces on Elixir 1.20.4 / OTP 28, and it reports violations as **warnings**, so it enforces only while `--warnings-as-errors` is on the compile step. ⚠️ No release since 2024-09-25. | diff --git a/coverage.tsv b/coverage.tsv index 691b288..c8b0512 100644 --- a/coverage.tsv +++ b/coverage.tsv @@ -18,3 +18,4 @@ slice_id percent sha date 032 78.99 193800e 2026-09-21 040 79.86 dc9dc6b 2026-09-21 041 80.55 ae200d7 2026-09-21 +059 80.54 53c9091 2026-09-21 diff --git a/docs/01-architecture.md b/docs/01-architecture.md index 679b6d8..c1d56b4 100644 --- a/docs/01-architecture.md +++ b/docs/01-architecture.md @@ -90,7 +90,7 @@ without anything failing. | `Trinity.Memory` | Always-on tiers with their budget and consolidator (030), search (031), semantic store and retrieval (032), compaction (023) | LLM (summaries/embeddings), Repo | | `Trinity.Skills` | SKILL.md parsing, registry, loader, manager, scanner (as built at 040: parser, sources, registry, index, the three tools; at 041: staging, promotion, manager, scanner, diff, learn, `skill_manage` and `learn`) | Repo, Permissions, **Effects**, **Receipts**, Sandbox (as built at 041: Tools, Memory, Permissions, Receipts and LLM; Effects is not a dependency: the promotion is not a tool call, it writes its own effect receipt; Tools never depends on Skills) | | `Trinity.Scheduler` | Oban workers for agent tasks, delivery | Sessions, Gateways, **Repo** | -| `Trinity.MCP` | Client manager, tool bridge, server | Tools, **Effects**, **Permissions**, Memory | +| `Trinity.MCP` | Client manager, tool bridge, server (as built at 059: the boundary alone, holding the core's version, its JSON depth and its telemetry event names) | Tools, **Effects**, **Permissions**, Memory (as built at 059: a top-level boundary, like `Trinity.Smoke`, with `deps: [Trinity, BeamMCP.JSON]`; the boundary compiler checks every call into the `beam_mcp` application and this boundary alone lists its modules) | | `Trinity.Gateways` | Adapter behaviour, router, allowlists, pairing | Sessions, **Permissions**, PubSub | | `Trinity.Subagents` | Delegation, result collection | Sessions, Tools | | `Trinity.Sandbox` | Luerl runners, resource limits | none | diff --git a/lib/trinity/mcp.ex b/lib/trinity/mcp.ex new file mode 100644 index 0000000..e5320c8 --- /dev/null +++ b/lib/trinity/mcp.ex @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.MCP do + @moduledoc """ + The MCP boundary (slice 059, ADR-0007 decisions 4 and 5): the one place in Trinity that may + import `BeamMCP`. The boundary compiler checks every call into the `beam_mcp` application + (`boundary: [default: [check: [apps: [:beam_mcp]]]]` in mix.exs), and this boundary alone + lists `BeamMCP.*` modules among its deps, so a `BeamMCP.*` reference anywhere else is a + compile error under `--warnings-as-errors`. Slice 060 puts the client driver here + (`Trinity.MCP.Client`), 061 the server (`Trinity.MCP.Server`); at 059 the boundary holds the + facts the FINDINGS table measured and nothing that serves. + """ + # beam_mcp defines no boundaries and no `BeamMCP` root module, so each of its modules this + # boundary reaches is named as its own implicit boundary (the boundary library's rule for + # external apps); 060 and 061 extend the list as they reach more of the core. + # + # A top-level boundary (as `Trinity.Smoke` is), not a sub-boundary of `Trinity`: the + # library lets a nested boundary depend on an external module only when an ancestor does, + # and `Trinity` must never list `BeamMCP` (that would open the core to it). + use Boundary, top_level?: true, deps: [Trinity, BeamMCP.JSON], exports: [] + + @doc "The core's version, from its application spec." + @spec core_version() :: String.t() + def core_version, do: to_string(Application.spec(:beam_mcp, :vsn)) + + @doc "The JSON nesting depth the core's decoder accepts: the first fact 060's driver builds on." + @spec json_max_depth() :: pos_integer() + def json_max_depth, do: BeamMCP.JSON.max_depth() + + @doc "The core's telemetry events (`[:beam_mcp, :dispatch, :start | :stop | :exception]`), for slice 090's catalogue." + @spec core_events() :: [[atom()]] + def core_events, + do: [ + [:beam_mcp, :dispatch, :start], + [:beam_mcp, :dispatch, :stop], + [:beam_mcp, :dispatch, :exception] + ] +end diff --git a/mix.exs b/mix.exs index 1f64999..cebc523 100644 --- a/mix.exs +++ b/mix.exs @@ -26,6 +26,9 @@ defmodule Trinity.MixProject do # 90. `mix test --cover` was still exiting 3 on a rule this project does not have, and # the comment above it claimed otherwise for the whole of slice 000. test_coverage: [summary: [threshold: 0]], + # Slice 059: calls into beam_mcp are checked by the boundary compiler everywhere; + # only Trinity.MCP lists BeamMCP among its deps (ADR-0007 decision 4 and 5). + boundary: [default: [check: [apps: [:beam_mcp]]]], releases: releases() ] end @@ -157,6 +160,9 @@ defmodule Trinity.MixProject do # (Trinity.Skills.Watcher); both were in the lock already as transitive dependencies. {:yaml_elixir, "~> 2.12"}, {:file_system, "~> 1.1"}, + # Slice 059: the MCP server core (ADR-0007 decision 5), reached only through the + # Trinity.MCP boundary; the slice measures its gap, 060 and 061 build on it. + {:beam_mcp, "~> 0.8"}, # Slice 013 (owner decision, 2026-09-20): the linux package builds mdex's NIF from # source for musl (MDEX_NATIVE_BUILD=1 and TRINITY_NIF_TARGET in config/config.exs), # because neither precompiled artifact loads in Burrito's musl ERTS (NOTES finding 13). diff --git a/mix.lock b/mix.lock index 554974b..9cbef91 100644 --- a/mix.lock +++ b/mix.lock @@ -2,6 +2,7 @@ "abnf_parsec": {:hex, :abnf_parsec, "2.1.0", "c4e88d5d089f1698297c0daced12be1fb404e6e577ecf261313ebba5477941f9", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e0ed6290c7cc7e5020c006d1003520390c9bdd20f7c3f776bd49bfe3c5cd362a"}, "axon": {:hex, :axon, "0.8.1", "c4a975e62a14ab6c374997b77367ec3a4c2740952ac474d3b0f202c91b7f75c4", [:mix], [{:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:kino_vega_lite, "~> 0.1.7", [hex: :kino_vega_lite, repo: "hexpm", optional: true]}, {:nx, "~> 0.10", [hex: :nx, repo: "hexpm", optional: false]}, {:polaris, "~> 0.1", [hex: :polaris, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1 or ~> 4.1", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "682a3517489300507ac9345f28341e7fa95bc5b4960d645816074ce551795d37"}, "bandit": {:hex, :bandit, "1.12.5", "af205a8e550f304caae09a97d29fd3c79a7f337526ea7cd772d2ff11d2f7c800", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "c5684ca062fa407cac115aec3256383f3e2ec9fdced7904d59cf5a7bb7ed6181"}, + "beam_mcp": {:hex, :beam_mcp, "0.8.0", "49f2607a92da4137cd1d11fc8669c396e1d93eedf7a3e29aaab1531dd41bd9c2", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.16", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b5a1beba2c936c2c50e164c43240ac31bc8b159a994980083ec0f6562392801e"}, "boundary": {:hex, :boundary, "0.10.4", "5fec5d2736c12f9bfe1720c3a2bd8c48c3547c24d6002ebf8e087570afd5bd2f", [:mix], [], "hexpm", "8baf6f23987afdb1483033ed0bde75c9c703613c22ed58d5f23bf948f203247c"}, "bumblebee": {:hex, :bumblebee, "0.7.1", "0d6a757f955671a379ebcf4ed07565513fa767b1869169b989f65ba7958a80f4", [:mix], [{:axon, "~> 0.8.0", [hex: :axon, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.12.0 or ~> 0.13.0", [hex: :nx, repo: "hexpm", optional: false]}, {:nx_image, "~> 0.1.0", [hex: :nx_image, repo: "hexpm", optional: false]}, {:nx_signal, "~> 0.2.0", [hex: :nx_signal, repo: "hexpm", optional: false]}, {:safetensors, "~> 0.1.3", [hex: :safetensors, repo: "hexpm", optional: false]}, {:tokenizers, "~> 0.4", [hex: :tokenizers, repo: "hexpm", optional: false]}, {:unpickler, "~> 0.1.0", [hex: :unpickler, repo: "hexpm", optional: false]}, {:unzip, "~> 0.12.0 or ~> 0.13.0", [hex: :unzip, repo: "hexpm", optional: false]}], "hexpm", "f26b3355700e3c7580014d41ce477d87d9fd71a60b76d5b64701dacb07830a58"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, diff --git a/slices/059-mcp-library-spike/FINDINGS.md b/slices/059-mcp-library-spike/FINDINGS.md new file mode 100644 index 0000000..6a4ea13 --- /dev/null +++ b/slices/059-mcp-library-spike/FINDINGS.md @@ -0,0 +1,85 @@ +# Slice 059: beam_mcp 0.8.0 against the 2026-07-28 checklist + +Measured 2026-09-21 on a clone of `https://github.com/ScriptKittyOS/beam_mcp` at `v0.8.0`, whose commit is +`cfa706b` (`git rev-parse --short 'v0.8.0^{commit}'` β†’ `cfa706b`, the sha ADR-0007 decision 5 names); hex.pm +lists 0.8.0 released 2026-09-19 under Apache-2.0 (`curl -s https://hex.pm/api/packages/beam_mcp`). Every path +and line below is at that commit; the column "derived by" is the command whose output the row restates. The +column "status" is one of: **ships** (the core does it), **carries** (the core puts it on the wire or in a +hook and Trinity does not read it yet), **refuses** (an entry of `docs/will-not-implement.md`, with its +number and the test that enforces it), **open** (neither built nor refused by name). + +Two facts about the tree, first, so the rows can be short. The core is 25 files under `lib/` +(`find lib -name '*.ex' | wc -l` β†’ 25); the public surface is `docs/public-api.txt` (142 lines, one per +function, callback or type). The whole suite at the pinned commit on the pinned toolchain (`.tool-versions`: +Erlang 28.1.1, Elixir 1.18.4-otp-28) is `mix test` β†’ `11 properties, 705 tests, 0 failures`. + +| # | Checklist item | Status | What the core does, with path and line | Derived by | +|---|---|---|---|---| +| 1 | Revision negotiation: `server/discover` for 2026-07-28, `initialize` for 2025-11-25, from one route | **ships**, dual-era on stdio; HTTP is 2026-07-28 only | `lib/beam_mcp/server.ex:238` matches `server/discover` before the revision switch and answers the full `DiscoverResult` (`supportedVersions` at :242); `:251` matches `initialize` (legacy opener); `params._meta` decides the era for every other request (README "What it speaks", `README.md:448-499`). Over HTTP `initialize`, `notifications/initialized` and `ping` are refused `404 -32601` (`lib/beam_mcp/transport/http.ex:196` `@removed_in_modern`) and `supportedVersions` is `["2026-07-28"]`. `2024-11-05` is refused with `-32022`. | `grep -n '"server/discover"\|"initialize"' lib/beam_mcp/server.ex`; `grep -n removed_in_modern lib/beam_mcp/transport/http.ex`; `sed -n 448,499p README.md` | +| 2 | Stateless Streamable HTTP as a Plug; `Mcp-Method` and `Mcp-Name` headers; no session identifier | **ships** | `lib/beam_mcp/transport/http.ex:164` `@behaviour Plug`, one POST endpoint, `MCP-Protocol-Version` required (README `:404-447`); `Mcp-Method` checked at `:1066-1068`, `Mcp-Name` at `:1076-1080`, `Mcp-Param-{Name}` enforced from `x-mcp-header`; every header checked in all its values. No session: `docs/will-not-implement.md` entry 7, `test/beam_mcp/boundary/no_session_test.exs`. The Plug exists only when `plug` is present (`:38-40`); `plug` and `bandit` are optional dependencies. | `grep -n '@behaviour Plug\|"mcp-method"\|"mcp-name"' lib/beam_mcp/transport/http.ex`; `sed -n 404,447p README.md` | +| 3 | stdio transport | **ships** | `lib/beam_mcp/transport/stdio.ex:44` `run/1` blocks the caller on newline-delimited JSON-RPC, options are `Server.new/1`'s (`:27`), dual-era (row 1); `:60` stops on `shutdown?/1`. | `grep -n 'def run\|Server\.' lib/beam_mcp/transport/stdio.ex` | +| 4 | tools: `tools/list`, `tools/call`, deterministic order, schema validation | **ships**, with one edge | `lib/beam_mcp/server.ex:361` `tools/list` (paginated through `BeamMCP.Cursor`, `lib/beam_mcp/cursor.ex:82` `page/5`), `:463` `tools/call`; arguments validated by `BeamMCP.Schema.validate/2` (`lib/beam_mcp/schema.ex:33`) at `server.ex:618` and `:848`, an invalid call answered `"invalid arguments: …"` (`:620`) and never dispatched. **Order:** resources are sorted by `uri` (`lib/beam_mcp/catalog.ex:159`), templates by `uri_template` (`:170`), prompts by `name` (`:229`); **tools keep the catalog's own order** (`catalog.ex:150` `def tools(catalog), do: catalog.capabilities().tools`), so determinism of `tools/list` is the host's: Trinity's catalog (061) sorts before it hands the list over. | `grep -n 'sort' lib/beam_mcp/catalog.ex`; `grep -n 'Schema.validate' lib/beam_mcp/server.ex` | +| 5 | resources: `resources/list`, `resources/read`, templates, subscriptions | **ships** list, read and templates; **refuses** subscriptions by capability, not by entry | `lib/beam_mcp/server.ex:379` `resources/list`, `:423` `resources/templates/list`, `:441` `resources/read` (only a uri the catalog listed or a template matches: `docs/will-not-implement.md` entry 11); capabilities at `:83-85` advertise `"resources" => %{"listChanged" => false, "subscribe" => false}`: no subscriptions and no notifications, "the server sends no notifications" (`:80-81`). The conformance row's five skipped checks are these (README `:500-539`). Not on the will-not-implement page: a subscription would be a capability the schema defines, so the README answers it, not entry 6. | `sed -n 76,86p lib/beam_mcp/server.ex`; `grep -n '"resources/' lib/beam_mcp/server.ex` | +| 6 | prompts: `prompts/list`, `prompts/get` | **ships** | `lib/beam_mcp/server.ex:390` `prompts/list`, `:406` `prompts/get` with `params`, `:417` without (refused); the catalog's `get_prompt/2` is called only after the prompt is listed and the arguments validated (`docs/will-not-implement.md` entry 11); `BeamMCP.PromptSpec.argument_schema/1` derives the schema (`docs/public-api.txt`). | `grep -n '"prompts/' lib/beam_mcp/server.ex` | +| 7 | `ttlMs` and `cacheScope` on every cacheable result | **carries**: the values are the host's | `lib/beam_mcp/server.ex:170-172` (the two are "neither the package's to invent"): `tools/list` writes `state.tools_ttl_ms` and `state.tools_cache_scope` (`:371-372`), lists write theirs (`:516`, `:565-566`), options `tools_ttl_ms` (a non-negative integer) and `tools_cache_scope` (a string) validated in `Server.new/1` (`:127-128`); `server/discover` writes `ttlMs` 0 and `cacheScope` `"private"` (`:244-245`). Trinity's 061 sets the two options; nothing in Trinity reads them today. | `grep -n 'ttlMs\|cacheScope\|tools_ttl_ms' lib/beam_mcp/server.ex` | +| 8 | `resultType` on every result, including `server/discover` | **ships**, `"complete"` only | `lib/beam_mcp/server.ex:965-981`: every 2026-07-28 result gets `"resultType" => "complete"` and `_meta.serverInfo` at one site; `server/discover`'s result carries `resultType` (`:233-245`); legacy results carry neither (`:319`). The one value is pinned by `test/beam_mcp/boundary/no_mrtr_test.exs` "the only resultType written under lib/ is complete, at one site". | `grep -n resultType lib/beam_mcp/server.ex` | +| 9 | MRTR: `input_required`, `requestState`, `inputResponses` | **refuses**: entry 12 | `docs/will-not-implement.md` entry 12: the core answers every request completely or refuses it; `lib/beam_mcp/server.ex:34-36` (the moduledoc says so); `inputResponses` and `requestState` are read nowhere, a request carrying them is served as if it carried neither (`test/beam_mcp/mrtr_wire_test.exs` "on the core, at both eras, the answer with the continuation parameters is the bare answer"; enforced by `test/beam_mcp/boundary/no_mrtr_test.exs`). See conflict A below. | `grep -n 'inputResponses\|requestState\|InputRequired' lib/beam_mcp/server.ex docs/will-not-implement.md` | +| 10 | Tasks extension | **open**: neither built nor refused | No line under `lib/` names `tasks` (`grep -rn '"tasks"\|Tasks' lib/` β†’ nothing); the capability key is known to the census only as a key the 2025-11-25 schema defines and 2026-07-28 drops for `extensions` (`test/beam_mcp/boundary/no_invented_capability_test.exs:11,19-20,37`); `extensions` is an open object nothing is read under (`docs/will-not-implement.md` entry 6). Not on the will-not-implement page, so a Tasks extension is a request the core's owner has not answered; Trinity's 060 treats it as absent on the server side and does not build a client for it. | `grep -rn '"tasks"\|Tasks' lib/ README.md`; `sed -n 9,40p test/beam_mcp/boundary/no_invented_capability_test.exs` | +| 11 | OAuth resource server hooks: what `:authorize` and `:authorize_body` give a host and what they do not | **carries** the hooks; **refuses** OAuth itself: entry 8 | `lib/beam_mcp/transport/http.ex:52`: `:authorize` is `(Plug.Conn.t() -> :ok \| {:error, term()})`, required, no default, called before the body is read (the reason goes to the log, never to the caller); `:61-69`: `:authorize_body` is `(Plug.Conn.t(), binary() -> :ok \| {:error, term()})`, optional, after the body; `:allowed_origins` required (`:55-58`). The `authorization` header is not read under `lib/` and reaches the hook untouched (`docs/will-not-implement.md` entry 8; `test/beam_mcp/boundary/no_oauth_no_client_test.exs` "no OAuth under lib/"); the transport performs no cryptography (entries 2 and 3). What a host gets: the conn (headers, the bearer among them) and the body; what it does not get: token parsing, PRM discovery, a token endpoint, anything on the response. See conflict B. | `sed -n 44,130p lib/beam_mcp/transport/http.ex` | +| 12 | Client role | **refuses**: entry 9 | `docs/will-not-implement.md` entry 9: no client module, no outbound connection, `initialize` only ever received; enforced by `test/beam_mcp/boundary/no_oauth_no_client_test.exs` "no client under lib/…" and `package_reach_test.exs` "the modules the package calls are exactly the listed ones" (no `gen_tcp`, `ssl`, `httpc`, `inets`, `Port`, `File`, no HTTP client). The public decoders a client may call from outside: `BeamMCP.JSON.decode/1`, `max_depth/0`, `type_of/1`; `BeamMCP.Schema.validate/2`; `BeamMCP.Cursor.decode/2` (`docs/public-api.txt`). See conflict B. | `grep -n '^BeamMCP.JSON\|^BeamMCP.Schema\|^BeamMCP.Cursor' docs/public-api.txt` | +| 13 | `connectome://` resources and the `:observe` tool | **ships**, opt-in by the host | `lib/beam_mcp/connectome/surface.ex:6`: three read-only resources (`connectome://declared`, `connectome://observed`, `connectome://diff`, `:32-34`) and one tool with `command_class: :observe` (`:19`), which a host puts in its catalog or does not; the observed graph carries edge identity only, never a payload byte (`docs/will-not-implement.md` entry 5). Whether Trinity exports them is a 061 G1 decision, default off (SLICE.md). | `grep -n 'connectome://\|:observe' lib/beam_mcp/connectome/surface.ex` | +| 14 | Telemetry events the core emits around dispatch, by name and metadata shape | **ships** | One site, `lib/beam_mcp/server.ex:859-899`: `[:beam_mcp, :dispatch, :start]` with measurements `%{system_time, monotonic_time}` and metadata `%{server_name, tool, telemetry_span_context}`; `[:beam_mcp, :dispatch, :stop]` with `%{duration, monotonic_time}` and the metadata plus `outcome`; `[:beam_mcp, :dispatch, :exception]` with `%{duration, monotonic_time}` and the metadata plus `kind`, `reason` (the host's, verbatim) and `stacktrace` (frames with arities, never argument lists, `BeamMCP.Stacktrace.arities/1`). No arguments, results or headers in any event (`lib/beam_mcp/connectome/observed.ex:13,47`; entry 5). `Trinity.MCP.core_events/0` lists the three for slice 090's catalogue. | `sed -n 859,899p lib/beam_mcp/server.ex` | +| 15 | The signer seam: `BeamMCP.Signer`, `Canonical.signature/3`, and whether a verifier of exported bytes can read the algorithm without the signer module | **ships**; the algorithm is in the bytes | `lib/beam_mcp/signer.ex:31`: one callback, `sign(canonical_bytes :: binary(), opts :: keyword()) :: {:ok, binary()} \| {:error, term()}`; `lib/beam_mcp/signer/none.ex` the no-op; `lib/beam_mcp/connectome/canonical.ex:311` `signature/3` encodes the graph with `:algorithm` from opts (`:sha256` default, `:sha384`, `:sha512`: `:52-55`), hands the bytes and the whole opts to the signer, and returns `%{algorithm, signature, signer}`; the canonical bytes themselves name the digest algorithm (`canonical.ex:27-40` "The algorithm is in the bytes": `schema_version`, `algorithm`, `nodes`, …), so a verifier holding the exported bytes reads the *digest* algorithm without the signer. The *signature* algorithm (Ed25519 in `beam_mcp_signer`) is the signer's and is not in the bytes: a verifier needs the signer's public key and knows the primitive from the `signer` module name in the map. The core holds no key and calls no signing primitive (entries 2 and 3). | `sed -n 20,35p lib/beam_mcp/signer.ex`; `sed -n 300,335p lib/beam_mcp/connectome/canonical.ex`; `sed -n 27,60p lib/beam_mcp/connectome/canonical.ex` | + +## The two conflicts, against the slice lines they collide with + +**A. Entry 12 (no MRTR) against 061's `input_required` criteria.** `docs/will-not-implement.md` entry 12 +refuses the multi-round-trip request in the core: the one `resultType` is `"complete"`, and `inputResponses` +and `requestState` are never read (row 9). Slice 061's SLICE.md asks for an MRTR approval loop on the server +("MRTR approvals", the acceptance criteria that answer `tools/call` with `input_required` and finish on the +second request). The two do not meet in the core. ADR-0007 decision 6 already places the loop above the core, +in a sibling wrapper that decodes nothing, with the `requestState` envelope minted and validated by Trinity, +and names the seam that wrapper needs: a `:server` module option on `BeamMCP.Transport.HTTP`. The probe below +measures that seam. The conflict stands as stated in the ADR; 061 builds against it. + +**B. Entries 9 (no client) and 8 (no OAuth) against 060 and 062.** Entry 9 (row 12) refuses a client in the +core; 060's SLICE.md builds "Trinity's thin driver" and decision 7 says it "builds the outbound JSON-RPC request +and nothing else of the protocol; decoding and validation call beam_mcp's public functions". The public +functions a client can call are `BeamMCP.JSON.decode/1`, `BeamMCP.Schema.validate/2` and `BeamMCP.Cursor.decode/2` +(row 12); `BeamMCP.Server.handle_message/2` is server-side and the driver must not lean on it. Entry 8 (row 11) +refuses OAuth in the core and gives the host two hooks; 062's resource server, embedded authorization server and +client role are Trinity's (decision 8), and the resource-server half attaches through `:authorize` (the bearer +is in the conn's headers, unread by the core) and `:authorize_body` where the body is part of the decision. The +conflict is the layering the ADR chose; it is stated here so 060 and 062 do not discover it as a gap. + +## The seam probe + +On a throwaway branch `probe/server-option` of the local clone at `cfa706b`, never pushed, deleted at the end +(`git branch -D probe/server-option` β†’ `Deleted branch probe/server-option (was cfa706b)`): a `:server` module +option on `BeamMCP.Transport.HTTP`, default `BeamMCP.Server`. The whole diff, `git diff` β†’ +`1 file changed, 3 insertions(+), 2 deletions(-)` (five changed lines; the diff is in PROOF.md): + +- `@plug_opts` gains `:server` (`lib/beam_mcp/transport/http.ex:201`), so it is the Plug's option and not + passed on to `Server.new/1`; +- `init/1`'s map gains `server: Keyword.get(opts, :server, Server)` (`:304`); +- `do_dispatch/3` calls `opts.server.handle_message(opts.server.new(opts.server_opts), message)` in place of + `Server.handle_message(Server.new(opts.server_opts), message)` (`:1597`). + +The census tests it touches, `mix test test/beam_mcp/boundary test/beam_mcp/transport` at the pinned commit +before (`180 tests, 0 failures`) and on the probe (`180 tests, 1 failure`), and the whole suite on the probe +(`11 properties, 705 tests, 1 failure`): exactly one, `test/beam_mcp/boundary/no_catalog_test.exs:102` "the +catalog is called through three callees: capabilities/0 at five sites, read_resource/1 at one, get_prompt/2 at +one", which pins every call through a runtime module with parentheses and would gain two entries, +`{{BeamMCP.Transport.HTTP, :do_dispatch, 3}, :handle_message, 2}` and `{…, :new, 1}`. No other census moves: +not the reach census (no new module is called: the default is the same `BeamMCP.Server`), not the no-session, +no-MRTR or no-OAuth censuses, not the public-API census (the option is not a function). The seam is therefore +five lines and one pinned list widened by two named entries, with the entry's own prose already allowing for +"a call through a runtime module with parentheses" as the shape it pins. That is beam_mcp's to add (ADR-0007 +decision 6); this slice records the size and asks for nothing from here. + +## What changes for 060 to 062 + +Nothing in the rows above changes the shape of 060 to 062 as ADR-0007 decisions 5 to 8 record them, so no +amendment is proposed. Two facts are new and go into their G1 plans: `tools/list`'s order is the host's (row +4: Trinity's catalog sorts), and the Tasks extension is open in the core (row 10: 060's driver does not build a +client for it, 061 does not advertise it). diff --git a/slices/059-mcp-library-spike/NOTES.md b/slices/059-mcp-library-spike/NOTES.md new file mode 100644 index 0000000..f21040b --- /dev/null +++ b/slices/059-mcp-library-spike/NOTES.md @@ -0,0 +1,63 @@ +# Slice 059: NOTES + +## G1 plan, 2026-09-21 + +Tree at `76b72ce` on `main` (040 and 041 approved, M4 reached); branch `slice/059-mcp-gap`; ROADMAP row +059 to `in_progress`. The slice measures, it does not build (SLICE.md, superseding the 2026-09-05 draft). + +1. A clone of beam_mcp at `v0.8.0` (its commit `cfa706b`, the one ADR-0007 decision 5 names) on its own + pinned toolchain (Erlang 28.1.1, Elixir 1.18.4-otp-28, both installed here beside Trinity's), its suite + run once as the baseline. FINDINGS.md: one row per checklist item, each cell from `docs/will-not-implement.md`, + `docs/public-api.txt`, the README and the source at that commit, with the deriving command. Test AC1 + (`test/trinity/mcp/findings_test.exs`: fifteen rows, each with a path and line and a command). +2. The two conflicts against the slice lines (entry 12 against 061's `input_required` criteria; entries 9 and 8 + against 060 and 062), in FINDINGS. Test AC2. +3. The seam probe on a throwaway branch of the clone: the `:server` option on `BeamMCP.Transport.HTTP`; the + diff size and the census tests that move, the output pasted; the branch deleted. Test AC3 (the report, + and no beam_mcp code in this tree). +4. `{:beam_mcp, "~> 0.8"}` in mix.exs; the VERSIONS row flipped by `mix versions.gen`; `Trinity.MCP` as the + boundary that alone may reach `BeamMCP.*`, with the boundary compiler checking every call into the + `beam_mcp` application (`boundary: [default: [check: [apps: [:beam_mcp]]]]`). Test AC4 (the lock, the row, + the configuration, the source census; the planted reference's refusal pasted from the compiler). +5. Gate and coverage (AC5); docs/01 (the boundary as built); an ADR-0007 amendment only if a finding changes + 060 to 062's shape. + +Manual verification queue: none (every criterion is `[auto]`). + +Deviation stated before code: AC4 says "(test)" for the planted reference's refusal; a test cannot run the +project's own compiler over a planted file without changing the tree it runs in, so the refusal is pasted from +the command in PROOF.md, and the test holds what makes it (the global check, the one boundary, the census that +no `BeamMCP.` reference exists under `lib/` outside `lib/trinity/mcp.ex`) and that the plant is real. + +## Findings at G3, 2026-09-21 + +1. **`Trinity.MCP` is a top-level boundary.** The boundary library lets a nested boundary depend on an + external module only when an ancestor does, and `Trinity` must never list `BeamMCP` (that would open the + core to it); so `Trinity.MCP` is `top_level?: true` with `deps: [Trinity, BeamMCP.JSON]`, as `Trinity.Smoke` + is. beam_mcp defines no boundaries and has no `BeamMCP` root module, so each of its modules Trinity reaches + is named as its own implicit boundary in that list; 060 and 061 extend it as they reach more of the core. + docs/01's table kept its row; the note says how it is placed. +2. **The seam is five lines and one pinned list.** The probe's diff is three insertions and two deletions in + `lib/beam_mcp/transport/http.ex`; the one census that moves is `no_catalog_test.exs` "the catalog is called + through three callees…", which pins every call through a runtime module with parentheses and would gain + `handle_message/2` and `new/1` on `BeamMCP.Transport.HTTP.do_dispatch/3`. Whole suite on the probe: 705 + tests, that one failure. The ask to beam_mcp's board, through the owner, is those five lines and two + entries; nothing was pushed, the branch is gone. +3. **`tools/list` keeps the catalog's order.** Resources, templates and prompts are sorted by the core; + tools are not (`catalog.ex:150`). Deterministic order is the host's, so 061's catalog sorts. Recorded for + 061's G1 rather than raised with the core: the core's choice is defensible (a host may want its order). +4. **The Tasks extension is open in the core**, neither built nor on the will-not-implement page; 060 builds + no client for it and 061 does not advertise it. If a later slice needs it, the question goes to beam_mcp's + board first. +5. **No ADR-0007 amendment.** Nothing measured changes decisions 5 to 8; the two new facts (3 and 4) are + inputs to 060 and 061's G1 plans, recorded in FINDINGS' last section. +6. **The core on Trinity's toolchain.** The clone's suite ran on beam_mcp's pinned toolchain (its + `.tool-versions`, installed here). Trinity compiles the package on 1.20.4-otp-28 / 28.5.0.5 (`mix compile + --warnings-as-errors` clean, the 25 files); the census that pins "eight atoms exact for the OTP the gate + runs" is beam_mcp's own gate's concern, not a consumer's. + +## Follow-ups + +- The seam request to beam_mcp's board (finding 2), routed through the owner; 061 is blocked on it (its + SLICE.md says so) and has the ADR's fallback. +- `Trinity.MCP.core_events/0` into slice 090's telemetry catalogue. diff --git a/slices/059-mcp-library-spike/PROOF.md b/slices/059-mcp-library-spike/PROOF.md new file mode 100644 index 0000000..366cdcb --- /dev/null +++ b/slices/059-mcp-library-spike/PROOF.md @@ -0,0 +1,152 @@ +# Proof for slice 059: MCP capability gap against beam_mcp, and the server seam probe + +Agent: Trinity Β· Coding Agent Β· Date: 2026-09-21 Β· Branch: slice/059-mcp-gap Β· Final commit: (the commit carrying this file; named in the closing correction) + +## Summary +beam_mcp 0.8.0 (its commit `cfa706b`) measured against the fifteen items of the 2026-07-28 checklist, each +row a path and line at that commit with the command that derived it (`FINDINGS.md`); the two conflicts stated +against 061, 060 and 062's lines; the `:server` seam probed on a throwaway branch of a local clone (five +changed lines, one census test's pinned list widened by two entries, the branch deleted); `beam_mcp ~> 0.8` in +the tree behind `Trinity.MCP`, the one boundary the compiler lets reach it. Nothing was built on the core and +nothing was sent to it; no ADR-0007 amendment is proposed (NOTES finding 5). Six findings in NOTES.md. + +## Gate +``` +$ mix gate (this machine, OTP 28.5.0.5, Elixir 1.20.4, under a 32 GiB cgroup, tree 53c9091 with this file, coverage.tsv and the probe diff uncommitted on top) +2146 mods/funs, found no issues. +... SCAN COMPLETE ... +No retired or security advisory packages found +No vulnerabilities found. +versions.verify: OK. 103 locked packages, none disagreeing with 53 pins +Result: 454 passed, 18 excluded +plan_check: PASS +exit=0 +``` +CI: named in the closing correction. + +## Tests +``` +$ mix test --cover (tree 53c9091) +Result: 454 passed, 18 excluded +| 66.67% | Trinity.MCP | (core_events/0 is a constant read by 090) +| 80.54% | Total | +``` +`coverage.tsv` row: `059 80.54 53c9091 2026-09-21` (from 80.55 at 041: one hundredth, the boundary's +constant). + +The slice's six tests (`mix test test/trinity/mcp --trace`): +``` +test/trinity/mcp/boundary_test.exs + * test beam_mcp is in mix.lock at 0.8.0 and the VERSIONS row reads in mix.lock + * test the boundary compiler checks calls into beam_mcp everywhere, and Trinity.MCP is the boundary that lists it + * test the planted reference is a real reference: the file the proof compiles names BeamMCP outside Trinity.MCP +test/trinity/mcp/findings_test.exs + * test AC1: fifteen rows, numbered 1 to 15, each naming the pinned commit's paths with a line and a deriving command + * test AC2: the two conflicts are stated against the slice lines + * test AC3: the probe reports its diff size and the one census test it touches, and none of it is here +``` + +## Acceptance criteria evidence + +### AC1 [auto]: `FINDINGS.md` carries one row per checklist item, each with a path and line at the pinned beam_mcp commit and the command that derived it +`FINDINGS.md`, rows 1 to 15; the pinned commit derived by `git rev-parse --short 'v0.8.0^{commit}'` β†’ `cfa706b` +on the clone. `findings_test.exs` "AC1" parses the table and holds every row to a path with a line, a status +word and a deriving command. The population the rows read: `find lib -name '*.ex' | wc -l` β†’ 25; +`wc -l docs/public-api.txt` β†’ 142. The baseline suite at the pinned commit on its own toolchain: +``` +$ cd && mix test (Erlang 28.1.1, Elixir 1.18.4-otp-28, .tool-versions of the clone) +11 properties, 705 tests, 0 failures +``` + +### AC2 [auto]: The two conflicts are stated against slice lines +`FINDINGS.md` "The two conflicts": A, will-not-implement entry 12 (no MRTR) against 061's `input_required` +criteria (`slices/061-*/SLICE.md:13-15,38`, which already carry the blocker); B, entries 9 (no client) and 8 +(no OAuth) against 060's thin driver (`slices/060-*/SLICE.md:14-15`) and 062's roles. `findings_test.exs` "AC2". + +### AC3 [auto]: The seam probe reports the diff size and the census tests it touches, with output; no code from it is merged anywhere +On the clone, branch `probe/server-option` from `cfa706b`: +``` +$ git diff --stat + lib/beam_mcp/transport/http.ex | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) +``` +The diff is `proof/probe-server-option.diff` (a record, not a patch to apply: SLICE.md's "Out"). Before, at +`cfa706b`, and on the probe: +``` +$ mix test test/beam_mcp/boundary test/beam_mcp/transport (cfa706b) +180 tests, 0 failures +$ mix test test/beam_mcp/boundary test/beam_mcp/transport (probe) +180 tests, 1 failure + 1) test the catalog is called through three callees: capabilities/0 at five sites, read_resource/1 at one, get_prompt/2 at one (BeamMCP.Boundary.NoCatalogTest) + test/beam_mcp/boundary/no_catalog_test.exs:102 + calls through a variable module: + ... + {{BeamMCP.Transport.HTTP, :do_dispatch, 3}, :handle_message, 2} + {{BeamMCP.Transport.HTTP, :do_dispatch, 3}, :new, 1} +$ mix test (probe, the whole suite) +11 properties, 705 tests, 1 failure +$ git checkout -- . && git checkout 'v0.8.0^{commit}' && git branch -D probe/server-option +Deleted branch probe/server-option (was cfa706b). +``` +`findings_test.exs` "AC3": the report names the numbers and the test; `lib/beam_mcp` does not exist in this +tree and `mix.exs` fetches `beam_mcp` from hex, not a path or a git source. + +### AC4 [auto]: `beam_mcp` is in `mix.lock`, its `VERSIONS.md` row reads in `mix.lock`, and `mix compile --warnings-as-errors` fails on a planted `BeamMCP` import outside `Trinity.MCP` +`boundary_test.exs`: the lock holds `{:hex, :beam_mcp, "0.8.0", …}`, the VERSIONS row reads `βœ… in mix.lock` +(`mix versions.gen` flipped it), `Trinity.MCP.core_version/0` is `"0.8.0"`, mix.exs carries +`boundary: [default: [check: [apps: [:beam_mcp]]]]`, and the census over `git ls-files lib/*.ex` finds +`BeamMCP.` in `lib/trinity/mcp.ex` alone. The planted reference (its text kept at +`test/support/mcp/planted_reference.ex.txt`, which the test holds to be a real reference), compiled by the +command: +``` +$ cat > lib/trinity/planted_beam_mcp.ex <<'EOF' +defmodule Trinity.PlantedBeamMcp do + @moduledoc false + def depth, do: BeamMCP.JSON.max_depth() +end +EOF +$ mix compile --warnings-as-errors +Compiling 1 file (.ex) +Generated trinity app +warning: forbidden reference to BeamMCP.JSON + (references from Trinity to BeamMCP.JSON are not allowed) + lib/trinity/planted_beam_mcp.ex:3 +exit=1 +$ rm lib/trinity/planted_beam_mcp.ex +``` +Why the refusal is pasted and not a test: NOTES, the deviation stated before code. + +### AC5 [auto]: Gate green; coverage line reported +The gate above; the coverage row `059 80.54 53c9091 2026-09-21`. + +## Manual verification for the reviewer +None: every criterion is automatic. The reviewer may read `FINDINGS.md` beside the clone at `cfa706b`; every +row's last column is the command to rerun. + +## Deviations from SLICE.md +NOTES.md, one stated before code (AC4's refusal pasted from the compiler, the test holding what makes it) and +one found building (`Trinity.MCP` is a top-level boundary, finding 1). + +## Versions touched +`VERSIONS.md` updated: yes, by `mix versions.gen`: the `beam_mcp` row from πŸ” to βœ… in `mix.lock` (0.8.0). +`mix versions.verify`: OK, 103 locked packages, none disagreeing with 53 pins. + +## Git +``` +$ git log --oneline main..HEAD +(named in the closing correction, after the final commit) +``` + +## Closing correction, 2026-09-21 + +Supersedes "named in the closing correction" above. The tree the PR is merged from is `e82c170` (`feat(s059): +complete slice 059`, the commit carrying this file). On it, CI gate run 35643511487: `gate` success (454 +passed, 18 excluded), `postgres` success (434 passed, 38 excluded), `fips-tag` and `fips` success (459 passed, +13 excluded; the six FIPS tests by name). The coverage row stays at `53c9091` (80.54%): `e82c170` differs from +it in this file, NOTES.md, ROADMAP.md, README.md, coverage.tsv and the probe diff's copy only. + +``` +$ git log --oneline main..HEAD +e82c170 feat(s059): complete slice 059 (MCP capability gap and seam probe) +53c9091 feat(s059): beam_mcp 0.8.0 measured against the 2026-07-28 checklist, the seam probed, the dependency behind the Trinity.MCP boundary +``` diff --git a/slices/059-mcp-library-spike/proof/probe-server-option.diff b/slices/059-mcp-library-spike/proof/probe-server-option.diff new file mode 100644 index 0000000..1b4e56e --- /dev/null +++ b/slices/059-mcp-library-spike/proof/probe-server-option.diff @@ -0,0 +1,30 @@ +diff --git a/lib/beam_mcp/transport/http.ex b/lib/beam_mcp/transport/http.ex +index 401dd97..41eaff6 100644 +--- a/lib/beam_mcp/transport/http.ex ++++ b/lib/beam_mcp/transport/http.ex +@@ -198,7 +198,7 @@ if Code.ensure_loaded?(Plug) do + # This Plug's own options; everything else in the keyword list belongs to Server.new/1. + # Derived by exclusion rather than by naming what to keep: a `Keyword.take` list silently + # dropped `tools_ttl_ms` and `tools_cache_scope` when they were added, and a test caught it. +- @plug_opts [:authorize, :allowed_origins, :authorize_body, :read_timeout, :connection_timeout] ++ @plug_opts [:authorize, :allowed_origins, :authorize_body, :read_timeout, :connection_timeout, :server] + + # Extracted from `init/1` rather than inlined, and not for tidiness: adding this check + # inline took `init/1` to a cyclomatic complexity of 11 against a limit of 9, and the gate +@@ -301,6 +301,7 @@ if Code.ensure_loaded?(Plug) do + %{ + authorize: authorize, + authorize_body: authorize_body, ++ server: Keyword.get(opts, :server, Server), + allowed_origins: origins, + read_timeout: read_timeout, + connection_timeout: connection_timeout, +@@ -1594,7 +1595,7 @@ if Code.ensure_loaded?(Plug) do + defp do_dispatch(conn, message, opts) do + # Nothing is stamped: `check_headers/3` has held the body's `params._meta` to the header + # and to the schema, so the core reads the era from the message the client actually sent. +- case Server.handle_message(Server.new(opts.server_opts), message) do ++ case opts.server.handle_message(opts.server.new(opts.server_opts), message) do + {_state, nil} -> + send_resp(conn, 202, "") + diff --git a/test/support/mcp/planted_reference.ex.txt b/test/support/mcp/planted_reference.ex.txt new file mode 100644 index 0000000..9b5ee2b --- /dev/null +++ b/test/support/mcp/planted_reference.ex.txt @@ -0,0 +1,4 @@ +defmodule Trinity.PlantedBeamMcp do + @moduledoc false + def depth, do: BeamMCP.JSON.max_depth() +end diff --git a/test/trinity/mcp/boundary_test.exs b/test/trinity/mcp/boundary_test.exs new file mode 100644 index 0000000..d97937b --- /dev/null +++ b/test/trinity/mcp/boundary_test.exs @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.MCP.BoundaryTest do + @moduledoc """ + Slice 059 AC4: `beam_mcp` is in the lock, its VERSIONS row reads in mix.lock, the boundary + compiler checks calls into it everywhere and only `Trinity.MCP` may make them. The + compile-time refusal of a planted `BeamMCP` reference outside `Trinity.MCP` is pasted in + PROOF.md from the command (a test cannot run the project's own compiler over a planted file + without changing the tree it runs in); what this test holds is the configuration that makes + the refusal, and the source census: no `BeamMCP` reference under `lib/` outside + `lib/trinity/mcp.ex` and `lib/trinity/mcp/`. + """ + use ExUnit.Case, async: true + + test "beam_mcp is in mix.lock at 0.8.0 and the VERSIONS row reads in mix.lock" do + lock = Mix.Dep.Lock.read() + assert {:hex, :beam_mcp, "0.8.0", _, _, _, _, _} = lock[:beam_mcp] + assert File.read!("VERSIONS.md") =~ ~r/`beam_mcp` \| ~> 0\.8 \| βœ… in `mix\.lock`/ + assert Trinity.MCP.core_version() == "0.8.0" + end + + test "the boundary compiler checks calls into beam_mcp everywhere, and Trinity.MCP is the boundary that lists it" do + assert get_in(Mix.Project.config(), [:boundary, :default, :check, :apps]) == [:beam_mcp] + assert Trinity.MCP.json_max_depth() > 0 + + {out, 0} = System.cmd("git", ["ls-files", "lib/*.ex"]) + files = String.split(out, "\n", trim: true) + assert length(files) > 100 + + referrers = for f <- files, File.read!(f) =~ ~r/\bBeamMCP\./, do: f + assert Enum.sort(referrers) == ["lib/trinity/mcp.ex"] + end + + test "the planted reference is a real reference: the file the proof compiles names BeamMCP outside Trinity.MCP" do + plant = File.read!("test/support/mcp/planted_reference.ex.txt") + assert plant =~ "defmodule Trinity.PlantedBeamMcp" + assert plant =~ "BeamMCP.JSON.max_depth()" + end +end diff --git a/test/trinity/mcp/findings_test.exs b/test/trinity/mcp/findings_test.exs new file mode 100644 index 0000000..b1dff82 --- /dev/null +++ b/test/trinity/mcp/findings_test.exs @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.MCP.FindingsTest do + @moduledoc """ + Slice 059, AC1 to AC3 over `FINDINGS.md`: fifteen rows, each with a path and line at the + pinned beam_mcp commit and the command that derived it; the two conflicts named against + the slice lines they collide with; the seam probe's diff size and the census test it + touches, and no code from the probe in this tree. + """ + use ExUnit.Case, async: true + + @findings "slices/059-mcp-library-spike/FINDINGS.md" + + defp rows do + @findings + |> File.read!() + |> String.split("\n") + |> Enum.filter(&Regex.match?(~r/^\| \d+ \| /, &1)) + |> Enum.map(fn line -> + [n, item, status, what, derived] = + line + |> String.trim_leading("| ") + |> String.trim_trailing(" |") + |> String.split(" | ", parts: 5) + + %{n: String.to_integer(n), item: item, status: status, what: what, derived: derived} + end) + end + + test "AC1: fifteen rows, numbered 1 to 15, each naming the pinned commit's paths with a line and a deriving command" do + rows = rows() + assert Enum.map(rows, & &1.n) == Enum.to_list(1..15) + assert File.read!(@findings) =~ "`cfa706b`" + + for r <- rows do + assert Regex.match?( + ~r/`(lib|docs|test|README)[\w\/.-]*(\.ex|\.exs|\.md|\.txt)?:\d+/, + r.what + ) or + Regex.match?(~r/`(lib|docs|test)[\w\/.-]*\.(ex|exs|md|txt)`/, r.what), + "row #{r.n} names no path" + + assert Regex.match?(~r/`(grep|sed|find|git|curl)\b/, r.derived), + "row #{r.n} names no deriving command" + + assert Regex.match?(~r/\*\*(ships|carries|refuses|open)\*\*/, r.status), + "row #{r.n} has no status" + end + end + + test "AC2: the two conflicts are stated against the slice lines" do + text = File.read!(@findings) + [_, conflicts] = String.split(text, "## The two conflicts") + assert conflicts =~ "entry 12" and conflicts =~ "061" and conflicts =~ "input_required" + + assert conflicts =~ "Entries 9" and conflicts =~ "8" and conflicts =~ "060" and + conflicts =~ "062" + end + + test "AC3: the probe reports its diff size and the one census test it touches, and none of it is here" do + text = File.read!(@findings) + [_, probe] = String.split(text, "## The seam probe") + assert probe =~ "1 file changed, 3 insertions(+), 2 deletions(-)" + assert probe =~ "test/beam_mcp/boundary/no_catalog_test.exs:102" + assert probe =~ "180 tests, 1 failure" and probe =~ "705 tests, 1 failure" + assert probe =~ "Deleted branch probe/server-option" + # No beam_mcp source in this tree: the dependency is fetched, never vendored or patched. + refute File.exists?("lib/beam_mcp") + refute File.read!("mix.exs") =~ ~r/beam_mcp.*(path:|git:)/ + end +end