diff --git a/AGENTS.md b/AGENTS.md index 79d0431a4c..8343b847a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -615,7 +615,7 @@ Two columns because there are two sets (see above): **Contrib** is `[features] d | `modules` | ON | ON | `openhuman::modules` — the dynamic module host: the loader that admits a compiled `cdylib` through tinybus's ABI descriptor, manifest, dependency and SHA-256 gates, the compiled-in registry of modules this build trusts, and the `modules` RPC namespace. Implied by `documents`. Off ⇒ `modules.*` is unknown-method and nothing can load a native module | none in the product profile (`ureq`, `flate2`, `tar`, `zip 2`, `tempfile`, `toml` are already there) — **but see the kernel-floor note**: this feature exists so `tinybus/modules` is not enabled on the dependency itself, which would put a `dlopen` loader into the kernel profile where `tinybus` is always-on | | `skills` | ON | ON | `openhuman::skills` + `openhuman::skills::runtime` + `openhuman::skills::catalog` domains — SKILL.md discovery/parse/install, workflow execution + run logs, remote catalogs, the `skill_setup` / `skill_executor` builtin agents, and the 16 skill agent tools | none (see below) | | `flows` | ON | ON | `openhuman::flows` (saved automation graphs — create/run/schedule, the `workflow_builder` + `flow_discovery` agents), `openhuman::flows::tinyflows` (engine seam), `openhuman::flows::rhai` (`.ragsh` language-workflow tool) | `tinyflows`, `jaq-core`, `jaq-std`, `jaq-json`, `rhai` | -| `mcp` | ON | ON | `openhuman::mcp::server` (the `openhuman mcp` stdio/HTTP server), `openhuman::mcp::registry` (dynamic Smithery installs — `mcp_clients` RPC namespace, SQLite, boot spawn, supervisor, OAuth), `openhuman::mcp::audit` (write-audit log), and the static config-declared server set in `openhuman::mcp::config_servers`. ~19 agent tools, ~20k LOC | **none** (see scope note) | +| `mcp` | ON | ON | `openhuman::mcp::server` (the `openhuman mcp` stdio/HTTP server), `openhuman::mcp::registry` (dynamic Smithery installs — `mcp_clients` RPC namespace, SQLite, boot spawn, supervisor, OAuth), `openhuman::mcp::audit` (write-audit log), and the static config-declared server set in `openhuman::mcp::config_servers`. ~19 agent tools, ~20k LOC | **none** — and the `tinymcp` module extraction does not change that either; see the scope note | | `tui` | OFF | — | `openhuman::tui` — the tabbed ratatui/crossterm CLI UI (Logs, Chat, Config, Settings), auto-opened by bare `openhuman` on interactive non-container hosts and forced with `openhuman tui` (alias `chat`). Runs the core in-process. No controllers, no agent tools. **Intentionally NOT forwarded to the desktop shell** (allowlisted in `check-feature-forwarding.mjs`). | `ratatui`, `crossterm` | | `channels` | ON | ON | `openhuman::channels` (external-messaging providers — Telegram/Discord/Slack/Signal/WhatsApp/iMessage/IRC/… — plus the channel runtime, controllers, host, proactive messaging + inbound dispatch) and the `channels::webview_accounts` / `webview_apis` / `webview_notifications` / `channels::whatsapp_data` webview-bridge domains (incl. the 3 `whatsapp_data_*` agent tools). **Carve-outs `channels::{traits, cli}` stay ungated.** | **28** via `tinychannels/{email,lark}` — the crate itself stays (load-bearing), its two heavy providers do not | | `memory-git` | OFF | ON | `openhuman::memory::diff` (git-backed snapshots/checkpoints/read markers, the `memory_diff` RPC namespace + agent tool) and the git wiki mirror in `memory::store::content::wiki_git`. **Type carve-out**: `memory::diff::types` compiles in BOTH builds — the always-on memory profile renders `CrossSourceDiff`/`ChangeKind` into prompts, and tinycortex makes the matching split (its `memory::diff::{types,source}` are ungated, only the `Ledger`/`DiffEngine` half sits behind `git-diff`). Off ⇒ `memory_diff` is unknown-method, the tool is absent, the embedded driver drops `Capability::Diff` **and** `as_diff()` returns `None` in lockstep (`audit_provider` fails on either half alone), and summary nodes are still written to disk but not mirrored into git. **This crate declares no `git2`** — tinycortex owns every libgit2 call in the stack (the diff ledger, the wiki mirror, the persona git-history reader), and the gate reaches the cohort by forwarding `tinycortex/git-diff` + `tinycortex/wiki-git`; `tinymemory-core/memory-git` forwards the same pair. Do not re-add a direct `git2` dependency to this crate or to `tinymemory-core`: it would buy no crates and invite a second major pin, which `links = "git2"` makes a hard cargo error. Test code that must read a ledger back goes through the `tinycortex::git2` re-export (`tests/memory_artifacts_e2e.rs`). | **3**: `git2`, `libgit2-sys`, `libz-sys` — two of the five native C builds in the kernel profile, the largest native shed in the program | @@ -667,7 +667,15 @@ Follows the voice facade+stub pattern for `mcp::server` / `mcp::registry` / `mcp - **Type carve-out.** Inert, dependency-free type modules stay **ungated**: `mcp::registry::types`, `mcp::audit::types`, `mcp::server::tools::types` (`McpToolSpec`). They are `serde`/`serde_json`-only data consumed by always-compiled callers (the orchestrator prompt builder, `tool_registry`). Both builds therefore share the **one real type definition** — the stubs carry behaviour only, so struct fields can never drift between the enabled and disabled builds. `ConnectedServerOverview` was moved from `connections.rs` into `types.rs` for exactly this reason and is re-exported from `connections` so existing paths still resolve. - **Split facade — the old `mcp_client` directory did not match the dependency graph, so the reorg split it three ways.** Its transport primitives went to the **ungated** `mcp::http_client` (`McpHttpClient`, `redact_endpoint`, `McpUnauthorizedError`); its static server set + stdio transport + setup agent went to the **leaf-gated** `mcp::config_servers`; and `sanitize` left the family entirely for `util::sanitize`. The `gitbooks` docs tool dials `McpHttpClient` directly (GitBook is modelled as a legacy MCP server), and the orchestrator prompt sanitizes **skill** descriptions through `util::sanitize::sanitize_for_llm` — neither has anything to do with MCP, and stubbing them would silently break a docs tool and corrupt the orchestrator prompt in slim builds. **The gate follows the real dependency graph, not the directory name.** A bonus of keeping `http_client` compiled: the `McpServerNeedsAuth` classifier coupling test in `core::observability` stays always-compiled — no `#[cfg]`, no wording-drift leak. -**Scope note — the `mcp` gate drops ZERO dependencies.** There is no MCP SDK in this crate: the dependency declarations contain no MCP-specific SDK or transport dependency; `test-mcp-stub` is the only MCP-named bin target. The entire protocol stack is hand-rolled over tokio process stdio + `reqwest` + `axum`, all of which are load-bearing for non-MCP domains. The gate is worth having for the ~20k LOC / ~19 agent tools / RPC surface it removes, but the issue-level DoD line claiming it "sheds the MCP SDK / transport stack" is superseded by this correction. The `mcp = []` feature list in `Cargo.toml` is intentionally empty — do not "fix" it by adding `dep:` entries. +**Scope note — the `mcp` gate drops ZERO dependencies, and the module extraction does not change that.** The history is worth keeping because both halves of it are counter-intuitive. + +Before the extraction there was no MCP SDK in this crate at all: the entire protocol stack was hand-rolled over tokio process stdio + `reqwest` + `axum`, every one of which is load-bearing for non-MCP domains. So the gate shed nothing, and the issue-level DoD line claiming it "sheds the MCP SDK / transport stack" was superseded by that correction. + +After the extraction the stack lives in `tinymcp`, and the natural expectation — recorded in the `Cargo.toml` comment and in `scripts/kernel-floor.limits`' 2026-08-22 entry — was that loading it as a TinyBus module would take `reqwest` and `rusqlite` out of the always-on graph with it. **Measured, it does not.** In the kernel profile `rusqlite` has six parents (`openhuman` itself, `tinyagents`, `tinychannels`, `tinycortex`, `tinymcp`, `tinymemory-core`) and `reqwest` has ten. `scripts/dep-sim.py --cut tinymcp` projects the whole shed at **−1 package / −1 name / 0 native**: the `tinymcp` package itself, and nothing underneath it. This is the same shape as the TinyMemory port — a module boundary buys a *compilation* boundary, not a dependency shed, whenever the module's dependencies are already shared with kernel surface. + +The gate is still worth having for the ~20k LOC / ~19 agent tools / RPC surface it removes. The `mcp = []` feature list in `Cargo.toml` is intentionally empty — do not "fix" it by adding `dep:` entries. + +**Step two of the extraction is registry-entered but not wired.** `src/openhuman/modules/registry.rs` pins the `tinymcp` v0.3.1 release, so the module can be downloaded, verified and loaded; the host still calls the library directly, and `Cargo.toml` still declares both `tinymcp` and `tinymcp-bus`. Cutting the path dependency needs contract additions that `tinymcp-bus` v0.3.1 does not carry — `OAuthComplete`, a connected-overview member for the already-exported `ConnectedServerOverview`, the boot-connect and reconnect-supervisor passes, the `ServerDetail` / `AuthDetection` / `AuthKind` reply types, the registry curation helpers, an error anchor for the `McpServerNeedsAuth` classifier coupling test in `src/core/observability.rs`, and `render_tool_result` / `redact_endpoint` for the ungated `gitbooks` tool. It also needs a per-`data_dir` object seam of the shape `modules::memory` already uses, because `mcp::host` keys one store per workspace and a loaded module receives one `data_dir` at load — and a desktop session moves workspace on login and again on logout. Those are upstream in `tinyhumansai/tinymcp` and must land and be released first. **Static vs dynamic — the naming is INVERTED from intuition.** Both halves must be gated or the gate is only half-applied: diff --git a/Cargo.toml b/Cargo.toml index be8a2607d6..cdb75030fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -271,10 +271,24 @@ tinymemory-core = { path = "vendor/tinymemory/crates/tinymemory-core" } # generates a settings schema for the desktop application from those types # rather than from a hand-kept copy. # -# The path dependency on `tinymcp` itself is the step-one arrangement: it is -# replaced by a loadable module entry in `src/openhuman/modules/registry.rs` -# once tinymcp cuts a release, at which point only `tinymcp-bus` remains and -# reqwest/rusqlite leave this build with it. +# The path dependency on `tinymcp` itself is still the step-one arrangement. The +# registry entry step two calls for now exists — `src/openhuman/modules/registry.rs` +# pins the v0.3.1 release — but the host still calls the library directly, because +# `tinymcp-bus` v0.3.1 does not yet publish everything this host reaches for: +# `OAuthComplete`, a member returning the already-exported `ConnectedServerOverview`, +# the boot-connect and reconnect-supervisor passes, the `ServerDetail` / +# `AuthDetection` / `AuthKind` reply types, the registry curation helpers, an error +# anchor for the `McpServerNeedsAuth` classifier coupling test, and +# `render_tool_result` / `redact_endpoint` for the ungated `gitbooks` tool. It also +# needs a per-`data_dir` object seam of the shape `modules::memory` already uses, +# since `mcp::host` keys one store per workspace and a module gets one `data_dir` at +# load. Those are upstream work in tinyhumansai/tinymcp. +# +# Correcting what this comment used to promise: dropping the path dependency does +# NOT take reqwest/rusqlite with it. Measured in the kernel profile, `rusqlite` has +# six parents and `reqwest` ten, so `scripts/dep-sim.py --cut tinymcp` projects the +# entire shed at -1 package / -1 name / 0 native builds — the `tinymcp` package and +# nothing beneath it. The boundary buys compilation isolation, not a dependency shed. # # After cloning: `git submodule update --init vendor/tinymcp`. # `default-features = false` drops the TinyBus adapter: this build calls the diff --git a/scripts/kernel-floor.limits b/scripts/kernel-floor.limits index b21e9fe3c5..330fac6e53 100644 --- a/scripts/kernel-floor.limits +++ b/scripts/kernel-floor.limits @@ -13,6 +13,37 @@ # Simulate with: scripts/dep-sim.py --cut # # History +# 286/268/2 2026-08-23 tinymcp registry entry, NO CHANGE to this profile — +# and the entry exists to correct the prediction in the +# 2026-08-22 line below, which is measurably wrong. +# That line said step two of the MCP extraction would drop +# the `tinymcp` path dependency "at which point the MCP +# transport stack leaves the always-on graph entirely. +# Lower it in that PR." It does not, and the ratchet is +# therefore NOT lowered here. +# Measured, not derived: `scripts/dep-sim.py --cut tinymcp` +# projects 286/268/2 -> 285/267/2, i.e. -1 package / -1 +# name / 0 native. The package that leaves is `tinymcp` +# itself and nothing beneath it, because its dependencies +# are shared with kernel surface: in this profile +# `rusqlite` has SIX parents (openhuman, tinyagents, +# tinychannels, tinycortex, tinymcp, tinymemory-core) and +# `reqwest` has TEN. Confirmed with `cargo tree +# --no-default-features --features flows -e normal -i +# rusqlite` / `-i reqwest`. +# This is the same lesson as the TinyMemory port: a module +# boundary buys a COMPILATION boundary, not a dependency +# shed, whenever the module's dependencies are already +# shared. Do not write "and its dependency tail leaves with +# it" into a History entry again without running dep-sim +# first — per-dependency intuition over-counts exactly the +# way per-dependency arithmetic does. +# What this PR does land: `TINYMCP` in +# `src/openhuman/modules/registry.rs`, pinning the v0.3.1 +# release so the module can be downloaded and verified. The +# path dependency stays until `tinymcp-bus` publishes the +# members and types the host reaches for (see the Cargo.toml +# comment); the -1 lands in that PR, not this one. # 286/268/2 2026-08-22 combined raise from two independent extractions # landing on the same day, each additive on the 283/265/2 # base. (1) The MCP client moved out to `tinymcp` (+2 diff --git a/src/openhuman/modules/registry.rs b/src/openhuman/modules/registry.rs index 28ac510513..039158eec8 100644 --- a/src/openhuman/modules/registry.rs +++ b/src/openhuman/modules/registry.rs @@ -495,6 +495,93 @@ const TINYRUNTIME_PYTHON: ModuleRecord = ModuleRecord { load: LoadPolicy::Lazy, }; +/// The `tinymcp` module: the Model Context Protocol client. +/// +/// Owns both transports (Streamable HTTP and a subprocess over stdio), the +/// statically declared server set a host puts in its own configuration, the +/// dynamic registry of user-installed servers with its SQLite store, the +/// reconnect supervisor, the browser sign-in flow, and the write-audit log. +/// +/// Lazy, because dialing an MCP server is something most sessions never do: a +/// host with no installed servers and no configured ones would otherwise pay a +/// download and a `dlopen` for a capability it never reaches. That differs from +/// the module's own `lazy = false` export hint, which speaks for a host whose +/// servers should be connected the moment it comes up — this host decides when +/// that moment is, and does so on the first ask. +/// +/// **What stays out of the module is host policy**, and the split is the same +/// one the contract's own documentation draws: the prompt-injection scan over +/// remote tool definitions, the `mcp_clients` / `mcp_setup` RPC surface, the +/// agent-facing tools, and the proxy *scoping* decision all belong to this +/// application's threat model, not to a protocol client. `tinymcp-bus` carries +/// the vocabulary; this table says which bytes may speak it. +const TINYMCP: ModuleRecord = ModuleRecord { + id: "tinymcp", + description: "Model Context Protocol client: transports, registry, and the write-audit log", + bus_name: "ai.tinyhumans.tinymcp.Mcp", + object_path: "/ai/tinyhumans/tinymcp/Mcp", + version: "0.3.1", + release_url: "https://github.com/tinyhumansai/tinymcp/releases/tag/v0.3.1", + assets: &[ + PlatformAsset { + host_key: "ubuntu-24.04-x86_64", + archive: "tinymcp-0.3.1-ubuntu-24.04-x86_64.tar.gz", + sha256: "f2ba8bfa0a74a9c234499e946936cc2de7f237e9772a85e7df0453a7c29669ab", + }, + PlatformAsset { + host_key: "ubuntu-24.04-arm64", + archive: "tinymcp-0.3.1-ubuntu-24.04-arm64.tar.gz", + sha256: "a68734086449b980a7de3cf87f6b6e00f4aa43bbd1f39187ac0803b4082d62dc", + }, + PlatformAsset { + host_key: "ubuntu-22.04-x86_64", + archive: "tinymcp-0.3.1-ubuntu-22.04-x86_64.tar.gz", + sha256: "da9225bbc008a3de0917da0280667e4a76a93d517f017a0962f420a0c0b311f6", + }, + PlatformAsset { + host_key: "ubuntu-22.04-arm64", + archive: "tinymcp-0.3.1-ubuntu-22.04-arm64.tar.gz", + sha256: "aaefc1f2c3ae51bb1447d18b25abc5b78f37b795c172ecb6d8fa31967b8d213c", + }, + PlatformAsset { + host_key: "macos-26-arm64", + archive: "tinymcp-0.3.1-macos-26-arm64.tar.gz", + sha256: "453624140df1d0df00a6e1bb1108fab086fb6b2ed3fbdced3f6765b534d4e0bc", + }, + PlatformAsset { + host_key: "macos-26-x86_64", + archive: "tinymcp-0.3.1-macos-26-x86_64.tar.gz", + sha256: "bcb8c68b0744bfc82479261a60c1beb3fcc0842321bcd5ed6519aef1b6194ac6", + }, + PlatformAsset { + host_key: "macos-15-arm64", + archive: "tinymcp-0.3.1-macos-15-arm64.tar.gz", + sha256: "a3e405131221a168bf35a887e1628f66e5e27a2cae65db8c260b00b4e190f4bb", + }, + PlatformAsset { + host_key: "macos-15-x86_64", + archive: "tinymcp-0.3.1-macos-15-x86_64.tar.gz", + sha256: "6e501853e76ba77b7fea2f828f3ff293c15ad3e41a5b70b7f3ddb73c6efee5d1", + }, + PlatformAsset { + host_key: "windows-2025-x86_64", + archive: "tinymcp-0.3.1-windows-2025-x86_64.zip", + sha256: "86d2d309e8c605c27f87bd652709683430449878843beb684842965e3fba2a41", + }, + PlatformAsset { + host_key: "windows-2022-x86_64", + archive: "tinymcp-0.3.1-windows-2022-x86_64.zip", + sha256: "7b04601dafd43eddf7d218900a1e70ecfa6471aa3af34f4dcab67fce2d872a99", + }, + PlatformAsset { + host_key: "windows-11-arm64", + archive: "tinymcp-0.3.1-windows-11-arm64.zip", + sha256: "db352cb7fffdbbd00a5cbd3e4fc610123e23c884c2d0cad89f1293a83f550406", + }, + ], + load: LoadPolicy::Lazy, +}; + /// Every module this build can load. pub const ALL: &[ModuleRecord] = &[ TINYDOCS, @@ -505,6 +592,7 @@ pub const ALL: &[ModuleRecord] = &[ TINYRUNTIME, TINYRUNTIME_NODEJS, TINYRUNTIME_PYTHON, + TINYMCP, ]; /// The record for `id`, if this build knows it.