From 6f9fa65eb2e9de81c98a9947d224706a42c806d9 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 21:27:11 +0700 Subject: [PATCH 1/2] spec(t27mcp): service.t27 -- one MCP service, TRI CLAW's /mcp with the t27 tools mounted Names belong to a home by prefix (t27_ and t27f_ to the t27 module, the rest to TRI CLAW). admit() mounts the t27 tools, drops the t27-mcp tg_ forwarders, and refuses a collision or a name MCP would not carry. The mounted tools are shown to the owner's scope only. The keyless local server keeps the t27 names, and TRI CLAW's coder persona carries four t27 tools. 9 of 9 tests pass, 0 are vacuous, and the seal is saved. Refs #10483 Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/t27mcp_T27mcpService.json | 28 ++++ specs/t27mcp/service.t27 | 201 +++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 .trinity/seals/t27mcp_T27mcpService.json create mode 100644 specs/t27mcp/service.t27 diff --git a/.trinity/seals/t27mcp_T27mcpService.json b/.trinity/seals/t27mcp_T27mcpService.json new file mode 100644 index 0000000000..12d6fc899b --- /dev/null +++ b/.trinity/seals/t27mcp_T27mcpService.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:627414f825f5c627debe571d967e96328ede778d79e5ae111e877da4707ebde4", + "gen_hash_rust": "sha256:1cb380aa09042218982298ac62d8529a7fd9c4ced7243abcf753ccc848b777c6", + "gen_hash_verilog": "sha256:59589733f94ecb650f8add71a30b42e0de1c3fd97c0f3b7322166ccbd9c6b821", + "gen_hash_zig": "sha256:5801f30d1403d57c123f5f5cacb826aee7d65ab92a4d16cde7dcea4148725afb", + "module": "T27mcpService", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T14:26:31Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:fbd86c42c75e669a1211f0385f6bed25e9e5809db9518be2876821a3d9420715", + "spec_path": "specs/t27mcp/service.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 9, + "total": 9, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/specs/t27mcp/service.t27 b/specs/t27mcp/service.t27 new file mode 100644 index 0000000000..ac63c6d781 --- /dev/null +++ b/specs/t27mcp/service.t27 @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27mcp/service.t27 -- one MCP service: the owner's TRI CLAW and the t27 MCP server behind one +; endpoint. Which side owns a tool name, which tools of the t27 server are mounted into TRI CLAW, which +; retire, and who may see the mounted ones. See the epic #10066. +; -- +; Today there are two servers. TRI CLAW (gHashTag/999-multibots-telegraf, apps/vibee-editor/render, the +; vibee-render service on Railway) serves about 170 tools over JSON-RPC at POST /mcp: crm_*, tg_*, +; browser_*, mail_*, plan_*, hive_* and more, behind the owner's agent key, with its own chat agent in the +; Telegram bot and the mini app. The t27 MCP server (gHashTag/t27-mcp) runs on the owner's machine over +; stdio and serves the t27 tools, whose logic is t27 compiled to wasm and whose data is the published +; corpus at t27.ai, plus seven tg_* tools that only forward to TRI CLAW. So an agent needs two servers and +; two configurations, TRI CLAW's own chat agent cannot check or find a .t27 spec (the t27 rules reach its +; coder persona as prompt text only), and the seven forwarders are a second path to the same account. +; -- +; The one service is TRI CLAW's /mcp, with the t27 tools mounted into it: +; - A name belongs to one home, by prefix. The t27 module owns T27_PREFIXES (t27_ and t27f_); every other +; name, tg_ included, is TRI CLAW's. "t27ai" has no underscore after t27, so it is not a t27 name. +; - admit() decides each tool a side offers to the one registry. A t27 tool is mounted. A tool the t27 +; server offers under a TRI CLAW name is a forwarder: inside TRI CLAW it would call the service it is +; part of, so it is dropped, and the app's own tool of that name answers. A TRI CLAW tool under a t27 +; name is a collision and is refused: the t27 module's tool keeps the name. A name that MCP would not +; carry (NAME_MAX characters of a-z 0-9 _) is refused on either side. +; - The mounted tools read public data and change nothing: they never send, never write the app's state +; and never touch the owner's accounts. So they need no confirmation card, and they are shown to the +; owner's scope only (an agent key, an app session or the bot's signed data). An editorial key and a +; game token keep the fixed tool sets TRI CLAW gives them; this spec adds nothing to those sets. +; - The local t27 server stays, for a client with no key: it serves the t27 names only (local_serves()). +; Its tg_* forwarders retire, since the same tools answer at the one endpoint. +; - TRI CLAW's coder persona carries CODER_TOOLS, so the chat agent that writes .t27 checks a spec, finds +; one by intent, reads one and reuses an example before it writes a line. +; The endpoint, its key variable and the forwarders' address stay in telegram.t27 until the forwarders +; retire; this spec does not copy them. The per-tool schemas stay in tool_schema.t27. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module T27mcpService; + +/// Homes: where a tool's handler runs. +pub const H_T27 : u32 = 0; +pub const H_TRI_CLAW : u32 = 1; +pub const HOME_NAME : [2]str = ["t27", "TRI CLAW"]; + +/// The name prefixes the t27 module owns. Every other name is TRI CLAW's. +pub const T27_PREFIXES : [2]str = ["t27_", "t27f_"]; + +/// MCP carries a tool name of at most 64 characters; both sides use a-z 0-9 _ only. +pub const NAME_MAX : u32 = 64; + +/// admit() answers. +pub const A_MOUNT : u32 = 0; +pub const A_DROP_FORWARDER : u32 = 1; +pub const A_REFUSE_COLLISION : u32 = 2; +pub const A_REFUSE_NAME : u32 = 3; +pub const ADMIT_TEXT : [4]str = [ + "mounted", + "dropped: a forwarder to TRI CLAW, whose own tool of this name answers", + "refused: the name belongs to the t27 module", + "refused: a tool name is 1 to 64 of a-z 0-9 _" +]; + +/// Caller scopes at the one endpoint. +pub const S_OWNER : u32 = 0; +pub const S_EDITORIAL : u32 = 1; +pub const S_GAME : u32 = 2; +pub const S_NONE : u32 = 3; +pub const SCOPE_COUNT : u32 = 4; + +/// The t27 tools TRI CLAW's coder persona carries. +pub const CODER_TOOLS : [4]str = ["t27_check", "t27_find", "t27_spec", "t27_examples"]; + +pub fn has_prefix(name: str, prefix: str) -> bool { + if (name.len < prefix.len) { return false; } + var i: usize = 0; + while (i < prefix.len) { + if (name[i] != prefix[i]) { return false; } + i += 1; + } + return true; +} + +/// A name MCP carries and both sides accept: 1 to NAME_MAX characters of a-z 0-9 _. +pub fn name_ok(name: str) -> bool { + if (name.len == 0) { return false; } + if (name.len > NAME_MAX) { return false; } + var i: usize = 0; + while (i < name.len) { + let c = name[i]; + let lower : bool = c >= 'a' and c <= 'z'; + let digit : bool = c >= '0' and c <= '9'; + if ((lower or digit or c == '_') == false) { return false; } + i += 1; + } + return true; +} + +/// The home that owns a name. +pub fn home_of(name: str) -> u32 { + var i: usize = 0; + while (i < 2) { + if (has_prefix(name, T27_PREFIXES[i])) { return H_T27; } + i += 1; + } + return H_TRI_CLAW; +} + +/// What the one registry does with a tool that side `offered_by` offers under `name`. +pub fn admit(name: str, offered_by: u32) -> u32 { + if (name_ok(name) == false) { return A_REFUSE_NAME; } + let owner : u32 = home_of(name); + if (offered_by == H_T27) { + if (owner == H_T27) { return A_MOUNT; } + return A_DROP_FORWARDER; + } + if (owner == H_T27) { return A_REFUSE_COLLISION; } + return A_MOUNT; +} + +/// Whether a caller of scope `scope` sees a mounted t27 tool. +pub fn t27_visible(scope: u32) -> bool { + return scope == S_OWNER; +} + +/// Whether the local t27 server, the keyless one, still serves `name`. +pub fn local_serves(name: str) -> bool { + if (name_ok(name) == false) { return false; } + return home_of(name) == H_T27; +} + +test "the tables are the size they say" { + assert HOME_NAME[H_TRI_CLAW] == "TRI CLAW"; + assert T27_PREFIXES[0] == "t27_"; + assert T27_PREFIXES[1] == "t27f_"; + assert ADMIT_TEXT[A_MOUNT] == "mounted"; + assert SCOPE_COUNT == 4; +} + +test "home_of: t27 names by prefix, everything else is TRI CLAW's" { + assert home_of("t27_check") == H_T27; + assert home_of("t27_paper_claims") == H_T27; + assert home_of("t27f_view") == H_T27; + assert home_of("tg_dialogs") == H_TRI_CLAW; + assert home_of("crm_leads") == H_TRI_CLAW; + assert home_of("whoami") == H_TRI_CLAW; +} + +test "admit: the t27 server's own tools are mounted, its tg_ forwarders are dropped" { + assert admit("t27_status", H_T27) == A_MOUNT; + assert admit("t27_find", H_T27) == A_MOUNT; + assert admit("t27f_graph", H_T27) == A_MOUNT; + assert admit("tg_unanswered", H_T27) == A_DROP_FORWARDER; + assert admit("tg_scheduled", H_T27) == A_DROP_FORWARDER; +} + +test "admit: TRI CLAW's tools are mounted as they are" { + assert admit("tg_send", H_TRI_CLAW) == A_MOUNT; + assert admit("browser_open", H_TRI_CLAW) == A_MOUNT; + assert admit("hdl_run", H_TRI_CLAW) == A_MOUNT; +} + +test "the mounted t27 tools are the owner's only, and the coder persona's set is all t27" { + assert t27_visible(S_OWNER); + var s : u32 = 1; + while (s < SCOPE_COUNT) { + assert t27_visible(s) == false; + s = s + 1; + } + var i: usize = 0; + while (i < 4) { + assert admit(CODER_TOOLS[i], H_T27) == A_MOUNT; + i += 1; + } +} + +test "local_serves: the keyless local server keeps the t27 names and retires the forwarders" { + assert local_serves("t27_check"); + assert local_serves("t27f_outline"); + assert local_serves("tg_history") == false; + assert local_serves("tg_search") == false; +} + +test "negative control: t27ai and t2_ are not t27 names, a short name is not a prefix match" { + assert home_of("t27ai") == H_TRI_CLAW; + assert home_of("t2_check") == H_TRI_CLAW; + assert home_of("t27") == H_TRI_CLAW; + assert home_of("t27f") == H_TRI_CLAW; + assert has_prefix("t2", "t27_") == false; +} + +test "negative control: a TRI CLAW tool under a t27 name is refused" { + assert admit("t27_check", H_TRI_CLAW) == A_REFUSE_COLLISION; + assert admit("t27f_view", H_TRI_CLAW) == A_REFUSE_COLLISION; +} + +test "negative control: a name MCP would not carry is refused on either side" { + assert admit("", H_T27) == A_REFUSE_NAME; + assert admit("T27_check", H_T27) == A_REFUSE_NAME; + assert admit("tg send", H_TRI_CLAW) == A_REFUSE_NAME; + assert admit("tg-send", H_TRI_CLAW) == A_REFUSE_NAME; + assert name_ok("t27_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + assert name_ok("t27_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") == false; + assert local_serves("") == false; +} From 34acfcb1c8542d7a3ec999bb3ad46f77f4911b2d Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 21:34:42 +0700 Subject: [PATCH 2/2] spec(t27mcp): service.t27 takes string parameters, so the C route builds the wasm both servers call gen-c lowers a `string` parameter as NUL-terminated text and needs u32 indices; with `str` it emitted name.len on a const char*. Checked on the lab: 9 of 9 tests, 0 vacuous; gen-c plus zig cc gives a 736-byte wasm exporting admit, local_serves, t27_visible, home_of and name_ok. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/t27mcp_T27mcpService.json | 12 ++++----- specs/t27mcp/service.t27 | 33 ++++++++++++++---------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/.trinity/seals/t27mcp_T27mcpService.json b/.trinity/seals/t27mcp_T27mcpService.json index 12d6fc899b..32ecc8b134 100644 --- a/.trinity/seals/t27mcp_T27mcpService.json +++ b/.trinity/seals/t27mcp_T27mcpService.json @@ -2,16 +2,16 @@ "built_by": "t27c-bootstrap@0.5.2+9448ab4be", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:627414f825f5c627debe571d967e96328ede778d79e5ae111e877da4707ebde4", - "gen_hash_rust": "sha256:1cb380aa09042218982298ac62d8529a7fd9c4ced7243abcf753ccc848b777c6", - "gen_hash_verilog": "sha256:59589733f94ecb650f8add71a30b42e0de1c3fd97c0f3b7322166ccbd9c6b821", - "gen_hash_zig": "sha256:5801f30d1403d57c123f5f5cacb826aee7d65ab92a4d16cde7dcea4148725afb", + "gen_hash_c": "sha256:1cef360bbc9f2c02bb01db68061f22cfffa8d11a590020cf985226ebc4c49fa0", + "gen_hash_rust": "sha256:fbc4aeb5c58e6ff12047914adfa9e78afa20b3c0a929f147695fdf02eeff0206", + "gen_hash_verilog": "sha256:d93a159aedd2755e732a99b3bee762950a28841a7f929b5b850dac1f0dafc534", + "gen_hash_zig": "sha256:ec231f1bc026c9b8575d9c4c62c7ed2ae5d8e34c4e0490d5bf9da1d28c1fa8e1", "module": "T27mcpService", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T14:26:31Z", + "sealed_at": "2026-10-18T14:34:31Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:fbd86c42c75e669a1211f0385f6bed25e9e5809db9518be2876821a3d9420715", + "spec_hash": "sha256:3d7957837be8ac3de14b18ce1c0ecdde805f3f71d4d801b62d476b2e82f4a1ae", "spec_path": "specs/t27mcp/service.t27", "tests": { "failed": 0, diff --git a/specs/t27mcp/service.t27 b/specs/t27mcp/service.t27 index ac63c6d781..5d0c73fcdb 100644 --- a/specs/t27mcp/service.t27 +++ b/specs/t27mcp/service.t27 @@ -67,10 +67,14 @@ pub const SCOPE_COUNT : u32 = 4; /// The t27 tools TRI CLAW's coder persona carries. pub const CODER_TOOLS : [4]str = ["t27_check", "t27_find", "t27_spec", "t27_examples"]; -pub fn has_prefix(name: str, prefix: str) -> bool { - if (name.len < prefix.len) { return false; } - var i: usize = 0; - while (i < prefix.len) { +/// The functions take `string`, so the C route (gen-c, then a wasm the t27 MCP server ships) lowers them as +/// NUL-terminated text; TRI CLAW calls the same wasm, so the rule has one home. +pub fn has_prefix(name: string, prefix: string) -> bool { + var nn : u32 = (name.len as u32); + var pn : u32 = (prefix.len as u32); + if (nn < pn) { return false; } + var i : u32 = 0; + while (i < pn) { if (name[i] != prefix[i]) { return false; } i += 1; } @@ -78,11 +82,12 @@ pub fn has_prefix(name: str, prefix: str) -> bool { } /// A name MCP carries and both sides accept: 1 to NAME_MAX characters of a-z 0-9 _. -pub fn name_ok(name: str) -> bool { - if (name.len == 0) { return false; } - if (name.len > NAME_MAX) { return false; } - var i: usize = 0; - while (i < name.len) { +pub fn name_ok(name: string) -> bool { + var n : u32 = (name.len as u32); + if (n == 0) { return false; } + if (n > NAME_MAX) { return false; } + var i : u32 = 0; + while (i < n) { let c = name[i]; let lower : bool = c >= 'a' and c <= 'z'; let digit : bool = c >= '0' and c <= '9'; @@ -93,8 +98,8 @@ pub fn name_ok(name: str) -> bool { } /// The home that owns a name. -pub fn home_of(name: str) -> u32 { - var i: usize = 0; +pub fn home_of(name: string) -> u32 { + var i : u32 = 0; while (i < 2) { if (has_prefix(name, T27_PREFIXES[i])) { return H_T27; } i += 1; @@ -103,7 +108,7 @@ pub fn home_of(name: str) -> u32 { } /// What the one registry does with a tool that side `offered_by` offers under `name`. -pub fn admit(name: str, offered_by: u32) -> u32 { +pub fn admit(name: string, offered_by: u32) -> u32 { if (name_ok(name) == false) { return A_REFUSE_NAME; } let owner : u32 = home_of(name); if (offered_by == H_T27) { @@ -120,7 +125,7 @@ pub fn t27_visible(scope: u32) -> bool { } /// Whether the local t27 server, the keyless one, still serves `name`. -pub fn local_serves(name: str) -> bool { +pub fn local_serves(name: string) -> bool { if (name_ok(name) == false) { return false; } return home_of(name) == H_T27; } @@ -163,7 +168,7 @@ test "the mounted t27 tools are the owner's only, and the coder persona's set is assert t27_visible(s) == false; s = s + 1; } - var i: usize = 0; + var i : u32 = 0; while (i < 4) { assert admit(CODER_TOOLS[i], H_T27) == A_MOUNT; i += 1;