Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .trinity/seals/t27mcp_T27mcpService.json
Original file line number Diff line number Diff line change
@@ -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: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:34:31Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:3d7957837be8ac3de14b18ce1c0ecdde805f3f71d4d801b62d476b2e82f4a1ae",
"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"
}
}
206 changes: 206 additions & 0 deletions specs/t27mcp/service.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// 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"];

/// 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;
}
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: 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';
if ((lower or digit or c == '_') == false) { return false; }
i += 1;
}
return true;
}

/// The home that owns a name.
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;
}
return H_TRI_CLAW;
}

/// What the one registry does with a tool that side `offered_by` offers under `name`.
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) {
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: string) -> 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 : u32 = 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;
}
Loading