Skip to content

refactor(skills,mcp): move skills and MCP machinery onto tinyskills and tinymcp (#2576) - #2580

Merged
oxoxDev merged 53 commits into
tinyhumansai:mainfrom
oxoxDev:feat/2576-skills-mcp-onto-tiny-libs
Oct 9, 2026
Merged

oxoxDev merged 53 commits into
tinyhumansai:mainfrom
oxoxDev:feat/2576-skills-mcp-onto-tiny-libs

Conversation

@oxoxDev

@oxoxDev oxoxDev commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Moves OpenCompany's skills and MCP machinery onto the shared tinyskills (v0.2.8) and tinymcp (v0.4.0) libraries and deletes the host copies. This is step 1 toward many isolated agents on one shared OpenHuman runtime.

Closes #2576

Skills

  • SKILL.md parse/render, slug rules, document digest, scanner, archive reader and skill-tree materialization now come from tinyskills; OpenCompany's copies are gone.
  • The six copies of "deltas + global disables + shared registry + resolve" collapse into one load_skill_set service. resolve takes the baseline and library as explicit inputs.
  • A SkillLibrary source seam resolves the shared skill library per host. The library ships as packaged files beside the desktop app and in containers, which fixes the empty shared registry on desktop and provisioned tenants.

MCP

  • The credential scrubber moves out of MCP probing into crate::redact.
  • MCP host code is gathered under crate::mcp, with one per-agent seam (resolve_for_agent -> AgentMcp) that feeds each agent's AgentSpec.
  • The unreachable OcMcpCallTool wrapper is removed; the MCP tool scope it fed is kept and pinned by tests.
  • MCP call failures and per-call metering are restored from tinymcp's structured call outcome. Recorded failures scrub every declared server's credentials from the server name, tool name and message.
  • Console OAuth runs through tinymcp's OAuthFlow and registers its client as "OpenCompany". mcp.json is read through tinymcp's config document.

Pins

API Or Behavior Changes

  • Shared skill registry is no longer empty on desktop and provisioned tenants (109 library skills load on the serve host).
  • MCP failure notes and usage rows for MCP calls are back.
  • OAuth client name: dynamic client registration sends client_name = "OpenCompany", as before the tinymcp swap (tinymcp's default is "TinyMCP").
  • Rendered SKILL.md:
    • A category that is blank after trimming is now left out instead of written as an empty line.
    • extra_frontmatter lines (for example license:, allowed-tools:) are now kept; before they were dropped.
    • Both change the bytes, and so the digest, of newly rendered documents that hit those cases. Documents already stored keep their bytes. No shipped skill is affected: all 111 shipped skills render with the same digest as main (tests/snapshots/skill-pins.txt, unchanged).
  • No other user-visible change. The frontend is byte-identical to main.

Tests

  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings (with --locked), plus -p opencompany-core --no-deps --features openhuman,mcp,composio,acp --all-targets
  • N/A: cargo build --all-targets was not run separately; cargo check --locked --all-features --all-targets and the test builds below cover the same targets
  • cargo test, scoped:
    • cargo test --locked -p opencompany-core --lib: 5968 passed, 1 ignored
    • cargo test --locked -p opencompany-core --features openhuman --tests
    • cargo test --locked --manifest-path crates/opencompany-app/Cargo.toml
    • bash scripts/ci/assert-no-duplicated-openhuman.sh, assert-feature-lanes, assert-desktop-features, assert-integration-targets-run.sh
  • New tests:
    • every shipped skill's parse/render and source digest is pinned;
    • read tools after a tree rebuild;
    • a file over 16 MiB fails materialization with no partial skill;
    • a missing workspace is created;
    • per-agent MCP block on a live harness turn, and which agents get the MCP bridge tools;
    • call outcomes reach metering and the failure drain;
    • a credential typed as a server or tool name is scrubbed;
    • the console OAuth registration body carries client_name = "OpenCompany";
    • a blank category is left out of rendered SKILL.md.

Manual verification (staging, serve host + console):

  • Skill registry renders.
  • Registry install materializes on disk with a pin equal to main's digest.
  • Authored skill renders.
  • Archive upload limits behave as on main.
  • MCP:
    • add, plus malformed mcp.json errors in the console editor, the config API and at boot;
    • a deepwiki tool call answers in chat over staging inference, with a usage row;
    • an agent without the grant sees no MCP tools;
    • a forced failure shows a failure note, and the bearer is never logged or rendered;
    • a console OAuth sign-in against a DCR server completes.
  • Desktop skill library is verified through the app's tests; the desktop window was not launched.

Documentation

  • Module docs for crate::mcp and the skills sources are updated in place: src/mcp map, call outcomes, console OAuth, the bundle reader, the library sources and the shared loader, and the tinyskills calls behind parse, scan, upload and materialize.
  • References to the removed MCP wrapper are dropped.

oxoxDev added 30 commits October 7, 2026 14:44
…humansai#2576)

scrub, redact and SCRUB_MAX_BYTES are used by Composio, search, the
turn settle path and the OAuth callback, not only MCP. Moving them to an
ungated crate::redact lets the MCP code move without dragging every
non-MCP caller along, and runs their tests in the default lane. Bodies
and tests move verbatim.
…rate::mcp (tinyhumansai#2576)

company/mcp*.rs become mcp/decl/ (declarations, mcp.json, endpoint,
server info, families) and mcp/policy/. Rename only: both stay ungated
so their tests keep running in the default lane, and company::mcp stays
addressable through a re-export.
…inyhumansai#2576)

decl/mod.rs was 1005 lines, over the source cap. Storage and resolution
move to decl/store.rs, validation and normalization to decl/validate.rs,
re-exported so every existing path still resolves. No code changes.
Rename only. The probe, failure classifier and McpFailureQueue keep
their bodies; callers are re-pointed from harness::mcp_probe.
tinyhumansai#2576)

harness/built_in/mcp.rs splits along its existing seam: the per-agent
exposure (attachments, briefs, registry tools) becomes mcp/agent/, the
company-scoped registry store becomes mcp/runtime.rs, each with its own
tests. Rename only; callers are re-pointed from harness::mcp.
…yhumansai#2576)

resolve_for_agent folds the company's declarations, the agent's
effective grants and the registry store into an AgentMcp that answers
which servers ride on the spec, which registry tools go on the belt and
what the persona brief says. build_agent_with_model now asks it instead
of carrying three separate blocks. Inputs and outputs are unchanged: a
dump of native names, tool scope, shared belt, attachments and system
prompt across ten grant/server shapes is byte-identical before and
after.
OPENHUMAN_NATIVE_TOOLS names mcp_list_tools and mcp_call_tool, and the
belt drops those names before it reaches the runtime, so the
McpListToolsTool and OcMcpCallTool pushed for a declared-server agent
never ran: every call went through OpenHuman's own mcp_call_tool over
the servers attached to the spec, whose deny list carries the per-agent
blocks. Removed with it: registry_for_agent, granted_secrets,
granted_policies, McpMetering, required_string_arg and
McpToolPolicySet, none of which had another caller.

The two bridge names now come from AgentMcp::native_tool_names under
the same condition registry_for_agent used, so the ToolScopeSpec each
agent is registered with is unchanged; the ten-shape scope dump is
byte-identical before and after. Credential-reflection canaries move
onto the native mcp_call_tool, the path agents actually take. Per-call
failure surfacing and OauthCall metering were never live on this path
and return with the tinymcp call-outcome observer.
)

Doc comments that named OcMcpCallTool or registry_for_agent as the
producer or the gate now name what actually decides: resolve_for_agent
and the native mcp_call_tool. Comment-only.
…nsai#2576)

mcp_list_tools and mcp_call_tool are in an agent's native scope only
when an enabled server its grants name is declared, and never on its
belt; a wildcard grant attaches every enabled server and a named one
only its own.
…ai#2576)

Drives the real pool and hosted provider against a scripted model and a
loopback MCP server. With the writer blocked from one tool, its native
mcp_call_tool is refused and the server sees no call, another tool on
the same server succeeds, and a teammate the rule does not name still
reaches the blocked tool. Clearing the block moves the MCP fingerprint
and the writer's next call succeeds on the same pool.
…#2576)

The mcp feature lane filtered to build, app::types and hive tests, so
the mcp-gated tests under crate::mcp were compiled by the all-features
check and run by nothing.
tinyhumansai#2576)

A golden snapshot of all 111 SKILL.md files under companies/ (108 bundle
skills plus the 3 global baseline skills): the digest of
render(parse(src)), the source digest a registry install pins, and the
digest of the frontmatter lines the parser keeps aside. Generated on
main 7af9e56 so the parser and digest swaps that follow cannot shift
an existing install's pin unnoticed. Re-bless with BLESS_SKILL_PINS=1.
…pe (tinyhumansai#2576)

The per-agent skill narrowing lived in runtime::builder, so the company
layer's skill_effective and skill_scope called up into the runtime to
reach it. It now lives beside the per-skill inversion it mirrors, with
its tests; behaviour is unchanged.
…mansai#2576)

skill_effective::resolve read the global baseline from crate::globals
itself and joined skills/ onto a company source dir. It now takes a
SkillLayers { baseline, bundle_root, library } and reads nothing ambient,
so the same fold can serve an owner whose layers differ.

company::skill_set names a company's layers (global baseline, the bundle
under its source dir, the shared library) in one place; every reader goes
through resolve_company / resolve_company_for_agent. Output unchanged.
…nyhumansai#2576)

Five readers (the REST list, slug collision check, document reader,
GraphQL Company.skills and the teammate picker) each listed the deltas,
appended the [globals].disable synthesized disables, fetched the shared
library and resolved. They now call load_runtime_skill_set; the harness
reads its deltas through load_skill_deltas, keeping its own disable
source (the manifest) and the delta fingerprint over the same rows.

SkillOwner names whose deltas are read so an agent key can join it
later. The reader-agreement test now drives each loader against a real
delta store and checks REST, GraphQL, the picker, the document reader
and the materialized tree report one set.
…inyhumansai#2576)

skill_md assembled a document by hand with a second copy of the
renderer's one-line collapse. It now builds a SkillDoc (no version, body
plus the trailing newline it always added) and renders it; a test pins
the output byte for byte against the old assembly.
…#2576)

The shared library was a skills_root path plus a OnceLock cache on
AppState, set only by serve from its --company checkout. It is now a
company::skill_library::SkillLibrary: DirLibrary (the same load, cache
and Config-error recast) or NoLibrary, picked by for_host in the order
explicit dir, OPENCOMPANY_SKILL_LIBRARY, packaged copy, none. A named
directory that is missing still fails; a packaged copy is served only
when present.

AppState holds the library and shared_skill_registry() delegates to it.
RuntimeBuilder::with_skills_registry takes the library and snapshots it
into the harness at build. serve, the desktop builder and provisioning
call checked_skill_library(), so a broken library still fails boot or
500s a provision. The desktop host now resolves the library too (env
only until the packaged copy is wired), so the parity exemption for
state.with_skills_root is gone.
The desktop served no shared skill library: a packaged install has no
checkout, so the registry tab was empty and every install fell back to a
client-authored stub. The bundle now carries every company's skills/ as
Tauri resources (layout preserved, read from disk, not compiled in),
the shell resolves the resource directory before it starts its hosts,
and each embedded host serves that copy unless OPENCOMPANY_SKILL_LIBRARY
names another.

With a library present, installing a slug the library does not serve is
a 404 rather than a stub. Tests boot an embedded host over the packaged
layout and check the registry lists it, an install snapshots the
library's document, an unknown slug 404s, and the bundle's resource
pattern lands where the host looks.
…inyhumansai#2576)

The image sets OPENCOMPANY_SKILL_LIBRARY=/app/companies so a tenant
provisioned with no company still serves the library it ships, and
desktop-dev.sh points the shell at the checkout's companies/ so local
edits are what it lists.
…nyhumansai#2576)

Probes OpenHuman's read_skill_resource across in-place
re-materializations of one agent's skill tree: an edited resource is
read fresh and list/describe see a newly added skill, but
read_skill_resource resolves skills through OpenHuman's process-wide
metadata cache, which nothing invalidates for this tree, so an added
skill's resources are not found until restart and a removed one fails
on its missing directory. Recorded as-is for an upstream fix; not
patched here.
…yhumansai#2576)

The shared library section names the resolution order (explicit,
OPENCOMPANY_SKILL_LIBRARY, packaged, none) and what fails boot; install
case 3 now applies only to a host naming no library. The fold's explicit
layers, the skill_set loaders, the moved scope derivation, the golden
pin snapshot and the read_skill_resource stale-scan gap are recorded.
The bundle's mcp.json is read in the default lane, and the parser moves
onto tinymcp's config_doc. Same vendored path as before, so the lockfile
is unchanged.
…ai#2576)

The bundle file is now parsed by config_doc::parse_with in lenient mode,
with endpoint, readOnlyTools, authSecret and $comment registered as host
fields. OC keeps only what is particular to a committed bundle: the
endpoint spelling, the shared validator, and refusing a credential in the
query string or an inline headers block.

An unknown entry field now drops that entry and is reported, where serde's
deny_unknown_fields used to refuse the whole file.
…nsai#2576)

OcMcpRegistryScopedTool now attaches tinymcp's McpCallOutcome to every
mcp_registry_tool_call result: answered when the install replied, failed
with the error's wire code when the vendored tool reports a string error
body, and ToolNotAllowed / InvalidArguments on its own refusals. The text
classifier is pinned against tinymcp's own error renderings.
…ansai#2576)

mcp::observe reads the McpCallOutcome that tinymcp's mcp_call_tool (and
the registry call tool) attach to their results, as OpenHuman forwards it
on ToolCallCompleted / SubagentToolCallCompleted. An answered call records
one OauthCall sample, as the removed OcMcpCallTool did. A call that failed
before the server answered becomes a scrubbed McpFailure, classified by the
new probe::classify_call_error; refusals (ToolNotAllowed, bad arguments)
are left to the agent's result, as before.

Driven by real bridge results against loopback servers: answered, remote
isError, blocked, 401 with and without a credential or OAuth challenge,
unreachable, JSON-RPC error, reflected-secret scrubbing.
…humansai#2576)

HarnessDeps.mcp_failures is now the company's McpCallObserver. Each agent's
blueprint carries an AgentMcpObserver bound to its company, id, meter and
the declared servers; CompanyAgent observes the progress stream after every
turn, and the hive settle path does the same for coordinator turns. The
brain and settle drains, the clear-before-turn calls, the bubble steps and
the McpCallFailed journal path are unchanged.

McpFailureQueue is deleted; nothing had pushed onto it since OcMcpCallTool
was removed. The confined copilot agent gets an inert observer.
…inyhumansai#2576)

A live harness turn against a loopback MCP server meters the answered call
as one OauthCall and records nothing; a call to a server answering 401
lands on deps.mcp_failures as credential_required and is not metered. The
registry-scoped tool's answered, refused and failed calls read the same
way through the observer.
…ansai#2576)

Discovery, dynamic client registration, PKCE, the parked pending state,
the code exchange and the refresh now come from tinymcp's OAuthFlow with
require_public_endpoints on, so every refresh re-applies the endpoint guard
(OAuthFlow::refresh, not the free refresh_if_expired). The guard is
tinymcp's, a superset of the ranges OC refused, and pins the client to the
addresses it checked.

OC keeps the host side: the company/server id a redirect resolves back to,
and MaterialStore, which maps the flow's credential map onto the stored
AuthMaterial::OAuth so existing tokens keep working and the callback still
stores through store_auth. AppState holds the flow instead of its own
pending map. Routes, status codes and error variants are unchanged;
supports_console_oauth keeps its own discovery because OAuthFlow::detect
reads a discovery failure as a static token, where the probe must leave the
Sign in button alone.

Deleted: OC's PKCE, DCR, authorize-URL builder, token exchange and parse,
SSRF guard, refresh, needs_refresh, PendingOAuth/OAuthBegin and the
AppState park/take/TTL code.
oxoxDev added 12 commits October 7, 2026 14:44
…humansai#2576)

skill_digest was the same lowercase-hex SHA-256 of one rendered document
that tinyskills now exports as document_digest. Call sites use it
directly; the OC function and its known-answer tests are deleted.
…inyhumansai#2576)

slugify, validate_slug, validate_slug_shape and valid_slug now run on
tinyskills SlugRules (Separator punctuation, truncation, reserved routes,
`skill` fallback), and the frontmatter-size and description-length
checks on check_frontmatter_size / validate_description_chars. OC keeps
its operator-facing sentences; tests pin them and compare slugify with
the previous derivation.
…ai#2576)

company::skill_scan was the scanner and catalogue sanitizer tinyskills
now owns, check for check. The write plane scans SkillDoc::scan_document
and the harness renders catalogue text with tinyskills'
sanitize_catalogue_text; the OC module and its duplicated tests are
deleted. The shipped-bundles-scan-clean test stays, beside the other
shipped-bundle tests.
…archive (tinyhumansai#2576)

skill_upload's zip reader (entry and byte ceilings, path, link and
nesting checks, macOS metadata, single-root and SKILL.md lookup) was the
reader tinyskills now ships behind its `archive` feature, which
opencompany-core enables. OC maps each ArchiveError to the sentence the
upload dialog showed before and still refuses bundled extras by name.

Entries prefixed `./` are now read as the same paths, which is the
accepted upload behaviour; a test pins it. The desktop lockfile also
gains the tinyskills edge the vendor pin commit left out.
…humansai#2576)

EffectiveSkills::materialize hands its enabled entries to
tinyskills::materialize_tree instead of clearing the scratch tree and
copying bundles with its own copy_dir_recursive, which is deleted.

tinyskills is stricter in three ways, each pinned by a test: a bundle
nested more than 32 directories deep fails the materialization; a
symlinked skills root is unlinked rather than followed; a bundle whose
directory name is not a safe path segment is now left out of the tree
and the catalogue (with a warning) instead of being copied.
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 190 billable files and costs up to $47.50.

View limit details

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8153c4d4-6415-4407-bdc9-25df7fcb1ae6
📥 Commits

Reviewing files that changed from the base of the PR and between ebf61ea and 7631505.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • crates/opencompany-app/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (190)
  • crates/opencompany-app/src/embedded.rs
  • crates/opencompany-app/src/embedded_skill_library_tests.rs
  • crates/opencompany-app/src/lib.rs
  • crates/opencompany-app/src/local.rs
  • crates/opencompany-app/tauri.conf.json
  • crates/opencompany-core/Cargo.toml
  • crates/opencompany-core/examples/live_company_turn.rs
  • crates/opencompany-core/src/app/types.rs
  • crates/opencompany-core/src/app/types_tests.rs
  • crates/opencompany-core/src/bin/opencompany.rs
  • crates/opencompany-core/src/company/composio_probe.rs
  • crates/opencompany-core/src/company/manifest.rs
  • crates/opencompany-core/src/company/mcp.rs
  • crates/opencompany-core/src/company/mcp_file.rs
  • crates/opencompany-core/src/company/mcp_oauth.rs
  • crates/opencompany-core/src/company/mcp_oauth_fixture_tests.rs
  • crates/opencompany-core/src/company/mcp_oauth_tests.rs
  • crates/opencompany-core/src/company/mod.rs
  • crates/opencompany-core/src/company/runtime.rs
  • crates/opencompany-core/src/company/skill_effective.rs
  • crates/opencompany-core/src/company/skill_effective/skill_effective_layers_tests.rs
  • crates/opencompany-core/src/company/skill_effective/skill_effective_tests.rs
  • crates/opencompany-core/src/company/skill_file.rs
  • crates/opencompany-core/src/company/skill_file_pins_tests.rs
  • crates/opencompany-core/src/company/skill_file_tests.rs
  • crates/opencompany-core/src/company/skill_library.rs
  • crates/opencompany-core/src/company/skill_library_tests.rs
  • crates/opencompany-core/src/company/skill_provenance.rs
  • crates/opencompany-core/src/company/skill_provenance_effective_tests.rs
  • crates/opencompany-core/src/company/skill_provenance_tests.rs
  • crates/opencompany-core/src/company/skill_scan.rs
  • crates/opencompany-core/src/company/skill_scan_tests.rs
  • crates/opencompany-core/src/company/skill_scope.rs
  • crates/opencompany-core/src/company/skill_scope_effective_tests.rs
  • crates/opencompany-core/src/company/skill_set.rs
  • crates/opencompany-core/src/company/skill_upload.rs
  • crates/opencompany-core/src/company/skill_upload_tests.rs
  • crates/opencompany-core/src/company/skill_validate.rs
  • crates/opencompany-core/src/company/skill_validate_tests.rs
  • crates/opencompany-core/src/desktop.rs
  • crates/opencompany-core/src/globals/mod.rs
  • crates/opencompany-core/src/harness/built_in/brain.rs
  • crates/opencompany-core/src/harness/built_in/brain_tests_part6.rs
  • crates/opencompany-core/src/harness/built_in/brain_tests_support1.rs
  • crates/opencompany-core/src/harness/built_in/brain_tests_support2.rs
  • crates/opencompany-core/src/harness/built_in/brain_tests_support3.rs
  • crates/opencompany-core/src/harness/built_in/brain_tests_support4.rs
  • crates/opencompany-core/src/harness/built_in/build.rs
  • crates/opencompany-core/src/harness/built_in/build_tests.rs
  • crates/opencompany-core/src/harness/built_in/build_tests_part3.rs
  • crates/opencompany-core/src/harness/built_in/built_in_test_fixtures.rs
  • crates/opencompany-core/src/harness/built_in/built_in_test_fixtures_2.rs
  • crates/opencompany-core/src/harness/built_in/built_in_tests_part02.rs
  • crates/opencompany-core/src/harness/built_in/built_in_tests_part04.rs
  • crates/opencompany-core/src/harness/built_in/built_in_tests_part05.rs
  • crates/opencompany-core/src/harness/built_in/built_in_tests_part06.rs
  • crates/opencompany-core/src/harness/built_in/built_in_tests_part07.rs
  • crates/opencompany-core/src/harness/built_in/composio.rs
  • crates/opencompany-core/src/harness/built_in/composio_catalog.rs
  • crates/opencompany-core/src/harness/built_in/composio_catalog_tests_part1.rs
  • crates/opencompany-core/src/harness/built_in/composio_turn_tests.rs
  • crates/opencompany-core/src/harness/built_in/confine.rs
  • crates/opencompany-core/src/harness/built_in/hive_hooks/settle.rs
  • crates/opencompany-core/src/harness/built_in/iteration_cap_turn_tests.rs
  • crates/opencompany-core/src/harness/built_in/mcp.rs
  • crates/opencompany-core/src/harness/built_in/mcp_agent_policy_tests.rs
  • crates/opencompany-core/src/harness/built_in/mcp_blocked_tests.rs
  • crates/opencompany-core/src/harness/built_in/mcp_policy_freshness_tests.rs
  • crates/opencompany-core/src/harness/built_in/mcp_reads_tests.rs
  • crates/opencompany-core/src/harness/built_in/mod.rs
  • crates/opencompany-core/src/harness/built_in/native_salvage_turn_tests.rs
  • crates/opencompany-core/src/harness/built_in/policy.rs
  • crates/opencompany-core/src/harness/built_in/publish.rs
  • crates/opencompany-core/src/harness/built_in/publish_turn_helpers_tests.rs
  • crates/opencompany-core/src/harness/built_in/search.rs
  • crates/opencompany-core/src/harness/built_in/search_turn_tests.rs
  • crates/opencompany-core/src/harness/built_in/skills.rs
  • crates/opencompany-core/src/harness/built_in/skills_catalogue_tests.rs
  • crates/opencompany-core/src/harness/built_in/skills_materialize_tests.rs
  • crates/opencompany-core/src/harness/built_in/skills_stale_read_tests.rs
  • crates/opencompany-core/src/harness/built_in/skills_tests.rs
  • crates/opencompany-core/src/harness/built_in/workflow_build/workflow_build_pass_tests_1.rs
  • crates/opencompany-core/src/harness/built_in/workspace_provision_turn_tests.rs
  • crates/opencompany-core/src/harness/built_in/workspace_turn_helpers_tests.rs
  • crates/opencompany-core/src/harness/cap_publish_tests.rs
  • crates/opencompany-core/src/harness/cap_turn_tests.rs
  • crates/opencompany-core/src/harness/spend_halt_turn_test_fixtures.rs
  • crates/opencompany-core/src/lib.rs
  • crates/opencompany-core/src/mcp/README.md
  • crates/opencompany-core/src/mcp/agent/README.md
  • crates/opencompany-core/src/mcp/agent/agent_blocked_tests.rs
  • crates/opencompany-core/src/mcp/agent/agent_tests.rs
  • crates/opencompany-core/src/mcp/agent/agent_turn_tests.rs
  • crates/opencompany-core/src/mcp/agent/mod.rs
  • crates/opencompany-core/src/mcp/agent/registry_list.rs
  • crates/opencompany-core/src/mcp/agent/registry_list_tests.rs
  • crates/opencompany-core/src/mcp/agent/registry_outcome.rs
  • crates/opencompany-core/src/mcp/agent/registry_outcome_tests.rs
  • crates/opencompany-core/src/mcp/agent/registry_scoped.rs
  • crates/opencompany-core/src/mcp/agent/registry_scoped_tests.rs
  • crates/opencompany-core/src/mcp/agent/resolve.rs
  • crates/opencompany-core/src/mcp/agent/resolve_tests.rs
  • crates/opencompany-core/src/mcp/decl/README.md
  • crates/opencompany-core/src/mcp/decl/decl_store_tests.rs
  • crates/opencompany-core/src/mcp/decl/decl_tests.rs
  • crates/opencompany-core/src/mcp/decl/endpoint.rs
  • crates/opencompany-core/src/mcp/decl/families.rs
  • crates/opencompany-core/src/mcp/decl/families_agent_policy_tests.rs
  • crates/opencompany-core/src/mcp/decl/families_tests.rs
  • crates/opencompany-core/src/mcp/decl/file.rs
  • crates/opencompany-core/src/mcp/decl/file_tests.rs
  • crates/opencompany-core/src/mcp/decl/mod.rs
  • crates/opencompany-core/src/mcp/decl/server_info.rs
  • crates/opencompany-core/src/mcp/decl/server_info_tests.rs
  • crates/opencompany-core/src/mcp/decl/store.rs
  • crates/opencompany-core/src/mcp/decl/validate.rs
  • crates/opencompany-core/src/mcp/mod.rs
  • crates/opencompany-core/src/mcp/observe.rs
  • crates/opencompany-core/src/mcp/observe_tests.rs
  • crates/opencompany-core/src/mcp/policy/README.md
  • crates/opencompany-core/src/mcp/policy/agent.rs
  • crates/opencompany-core/src/mcp/policy/agent_tests.rs
  • crates/opencompany-core/src/mcp/policy/mod.rs
  • crates/opencompany-core/src/mcp/policy/policy_reset_tests.rs
  • crates/opencompany-core/src/mcp/policy/policy_tests.rs
  • crates/opencompany-core/src/mcp/probe.rs
  • crates/opencompany-core/src/mcp/probe_tests.rs
  • crates/opencompany-core/src/mcp/runtime.rs
  • crates/opencompany-core/src/mcp/runtime_tests.rs
  • crates/opencompany-core/src/policy/consequence.rs
  • crates/opencompany-core/src/ports/types.rs
  • crates/opencompany-core/src/ports/types_skill_events_tests.rs
  • crates/opencompany-core/src/redact.rs
  • crates/opencompany-core/src/redact_tests.rs
  • crates/opencompany-core/src/runtime/builder.rs
  • crates/opencompany-core/src/runtime/handover.rs
  • crates/opencompany-core/src/runtime/tools.rs
  • crates/opencompany-core/src/server/graphql/graphql_test_group_3.rs
  • crates/opencompany-core/src/server/graphql/skills.rs
  • crates/opencompany-core/src/server/graphql/skills_drift_tests.rs
  • crates/opencompany-core/src/server/mcp_oauth.rs
  • crates/opencompany-core/src/server/mcp_oauth_tests.rs
  • crates/opencompany-core/src/server/operator.rs
  • crates/opencompany-core/src/server/operator_test_group_2.rs
  • crates/opencompany-core/src/server/ops/capabilities.rs
  • crates/opencompany-core/src/server/ops/mcp.rs
  • crates/opencompany-core/src/server/ops/mcp_registry.rs
  • crates/opencompany-core/src/server/ops/mcp_registry/catalogue.rs
  • crates/opencompany-core/src/server/ops/mcp_registry/wired.rs
  • crates/opencompany-core/src/server/ops/mcp_tool_policy.rs
  • crates/opencompany-core/src/server/ops/mcp_tool_policy_agent_tests.rs
  • crates/opencompany-core/src/server/ops/mcp_tool_policy_route_tests.rs
  • crates/opencompany-core/src/server/ops/mcp_tool_policy_tests.rs
  • crates/opencompany-core/src/server/ops/skills.rs
  • crates/opencompany-core/src/server/ops/skills/doc.rs
  • crates/opencompany-core/src/server/ops/skills/doc_tests.rs
  • crates/opencompany-core/src/server/ops/skills/journal.rs
  • crates/opencompany-core/src/server/ops/skills/update.rs
  • crates/opencompany-core/src/server/ops/skills/update_tests.rs
  • crates/opencompany-core/src/server/ops/skills/upload_tests.rs
  • crates/opencompany-core/src/server/ops/skills/vet.rs
  • crates/opencompany-core/src/server/ops/skills_scan_tests.rs
  • crates/opencompany-core/src/server/ops/skills_skill_md_frontmatter_resists_tests.rs
  • crates/opencompany-core/src/server/ops/team_agent.rs
  • crates/opencompany-core/src/server/ops/team_mcp.rs
  • crates/opencompany-core/src/server/ops/team_mcp_tests.rs
  • crates/opencompany-core/src/server/ops/write_mcp_manifest_tests.rs
  • crates/opencompany-core/src/server/ops/write_skills_drift_tests.rs
  • crates/opencompany-core/src/server/ops/write_skills_install_persists_the_tests.rs
  • crates/opencompany-core/src/server/ops/write_skills_journal_tests.rs
  • crates/opencompany-core/src/server/ops/write_test_support.rs
  • crates/opencompany-core/src/server/provision.rs
  • crates/opencompany-core/src/workflows/gated_tool_turn_tests.rs
  • crates/opencompany-core/src/workflows/runner_capped_halt_tests.rs
  • crates/opencompany-core/tests/desktop_serve_parity.rs
  • crates/opencompany-core/tests/snapshots/skill-pins.txt
  • deploy/Dockerfile
  • docs/modules/mcp-registry.md
  • docs/modules/mcp.md
  • docs/modules/skills.md
  • docs/spec/runtime/api-skill-authoring.md
  • docs/spec/runtime/globals.md
  • docs/spec/runtime/harnesses.md
  • docs/spec/runtime/ports-state.md
  • docs/spec/runtime/tools.md
  • scripts/ci/feature-lanes.txt
  • scripts/ci/lanes/lanes-plan.mjs
  • scripts/desktop-dev.sh
  • vendor/openhuman
  • vendor/tinyhivemind
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@tinysweeper

tinysweeper Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

⚠️ Review failed for 7631505fa200. the review of #2580 did not finish within 900s

@oxoxDev

oxoxDev commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai can you review this now?

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

@oxoxDev, I’ll start the review of #2580 now.

⚠️ Action not completed

Review skipped: 187 files exceed the limit of 100.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@sanil-23 sanil-23 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff against the vendored tinymcp v0.4.0 and tinyskills v0.2.8 sources at the PR's pins. The skills port looks faithful (limits, Mac-metadata handling, reserved-slug suffixing, BOM handling all match the deleted host code), and the scrubbing invariant the old OcMcpCallTool carried is now covered by tinymcp's SecretScrubber plus the observer on recorded failures. I also checked that a delegated turn's MCP calls are observed once (OpenHuman emits either ToolCallCompleted or SubagentToolCallCompleted, never both).

Requesting changes for four correctness issues in the console OAuth swap and the new call observer, inline below. The other five comments (error-prose coupling, two efficiency items, a dead helper, missing per-directory READMEs) are take-or-leave.

Fix before merge

  1. begin_error reports a transient protected-resource metadata fetch failure as "does not advertise DCR, paste a token" (tinymcp uses AuthDiscovery for both).
  2. The observer classifies every registry-install 401 as credential-required because auth_configured is looked up by declared name and registry outcomes carry a server_id.
  3. A server name containing / breaks the callback's company lookup now that company and server share one slash-separated id.
  4. A replayed or raced callback renders a 502 "token exchange failed" instead of the 400 "Sign-in expired" page.

|| v6.is_multicast()
|| (v6.segments()[0] & 0xfe00) == 0xfc00 // unique-local fc00::/7
|| (v6.segments()[0] & 0xffc0) == 0xfe80 // link-local fe80::/10
tinymcp::Error::AuthDiscovery { .. } => OpenCompanyError::InvalidRequest(format!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transient discovery failure reported as "no DCR". tinymcp's transport also returns Error::AuthDiscovery when fetching the protected-resource metadata fails (transport/http/mod.rs wraps the fetch error as AuthDiscovery { detail: "fetching protected-resource metadata: …" }), and OAuthFlow::begin propagates it with ?. This arm turns that into the 400 "requires OAuth but does not advertise dynamic client registration — paste a static API token".

Scenario: the server's /.well-known/oauth-protected-resource briefly returns 503; the operator is told the server lacks DCR and to paste a token, for a server that signs in fine a minute later. Before the swap this was Harness("oauth discovery failed: …").

Suggest matching on the detail prefix ("fetching protected-resource metadata" → Harness) so only the genuine no-usable-authorization-server case gets this message, or have tinymcp return Transport/Http for the fetch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 52d4240: an AuthDiscovery whose detail starts with fetching protected-resource metadata now maps to Harness("oauth discovery failed: ..."); only the no-usable-authorization-server case keeps the "paste a token" message. Regression test serves a 503 on the metadata route.

registration — paste a static API token in its credential field instead."
)),
tinymcp::Error::MalformedResponse { detail }
if detail.contains("endpoint refused")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status code keyed on tinymcp's message prose. The 400-vs-500 decision here depends on substrings of MalformedResponse.detail ("does not require authorization", "endpoint refused", "endpoint has no host", starts_with("invalid ")). A reworded message in a later tinymcp bump compiles fine and silently turns an actionable 400 into a 500 "oauth sign-in could not start".

Since tinymcp is vendored and in-house, the right-depth fix is typed variants (e.g. Error::NoAuthorizationRequired, Error::EndpointRefused) matched here instead of prose. Non-blocking.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly: tinymcp v0.4.0 has no typed variant for these (flow.rs and endpoint_guard.rs build them with Error::malformed(..), so MalformedResponse { detail } is all there is), so the arms still match prose. Fixed in be92b1a by naming each fragment as a constant and adding a test that include_str!s the tinymcp sources and fails if any fragment stops appearing there, so a reword breaks CI rather than turning a 400 into a 500. Typed variants (NoAuthorizationRequired, EndpointRefused) would still be the right fix on the tinymcp side.

/// scrubbed with every declared server's credentials, like the message.
fn failure(&self, outcome: &McpCallOutcome, output: &str) -> Option<McpFailure> {
let error = outcome.error.as_ref()?;
let decl = self.servers.iter().find(|decl| decl.name == outcome.server);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registry-install 401 is always classified as credential-required. auth_configured is derived by matching outcome.server against the declared servers' names, but for a mcp_registry_tool_call outcome outcome.server is the registry server_id, which never matches a declared name, so this is always false for registry installs.

Scenario: a directory install with stored env credentials gets a 401 because its token was revoked. call_error_from_text → UNAUTHORIZED, classify_call_error → status_kind(401, false) → CredentialRequired, and the operator bubble says "MCP server '' needs a credential. Add its API token …" even though one is configured and the real state is token-rejected. Registry calls were not classified at all before this PR, so this is a new misleading note.

Suggest resolving configured-ness for registry ids through McpRuntime (the install's env-key names), or emitting no auth hint for registry outcomes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ea9b84d: the observer now looks up configured-ness as Option<bool>; a server it cannot match to a declaration (a registry install id) gets None, and classify_call_error maps a 401 with unknown auth to the generic error class with no auth hint instead of credential_required. Regression test uses a registry-shaped 401 outcome; the existing registry observer test that encoded the old status is updated.

(verifier, challenge)
/// The company and server a [`server_id`] names.
pub fn split_server_id(server_id: &str) -> Option<(CompanyId, String)> {
let (company, server) = server_id.rsplit_once(SERVER_ID_SEPARATOR)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A server name containing / breaks callback resolution. The pending authorization is parked under "{company}/{server_name}" and recovered with rsplit_once('/'), but validate_one does not forbid / in a server name.

Scenario: operator adds a runtime server named acme/docs (accepted), opens /mcp/servers/acme%2Fdocs/oauth/start (axum decodes the segment to acme/docs). server_id becomes cid/acme/docs; the callback's split_server_id yields company cid/acme, server docs; state.registry().get misses and the tab shows 404 "unknown company". Before the PR PendingOAuth carried company_id and server_name as separate fields, so this worked.

Either reject / in validate_one or pick a separator no company id or server name can contain.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2722465: the company half of the server id is escaped (% and /) and the id is split on the first /, so any server name round-trips, including acme/docs. Test covers slashes and percent signs in both halves.

let status = resp.status();
let json: Value = resp.json().await.map_err(|e| {
OpenCompanyError::Harness(format!("client registration returned non-JSON: {e}"))
.map_err(|error| OpenCompanyError::Harness(format!("token request failed: {error}")))?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unknown/expired state is rendered as a 502 exchange failure. Every OAuthFlow::complete error, including tinymcp's MalformedResponse("unknown or expired oauth state"), becomes Harness("token request failed: …"), so the callback route renders the 502 "token exchange failed" page.

Scenario: the browser (or a double-click) delivers the same callback twice; both pass pending_server(cb_state), the first exchanges and consumes the entry, the second hits this arm and the operator sees a 502 and a warn log even though the sign-in succeeded. take_oauth used to route this to the 400 "Sign-in expired" branch; map that specific error there.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 411cad1: complete maps tinymcp's unknown or expired oauth state to InvalidRequest, and the callback renders that as the 400 "Sign-in expired" page; other exchange errors stay a 502. The prose is pinned to the tinymcp source by the same coupling test as above. Tested at the response-mapping level (a raced callback itself needs two concurrent requests, so the mapping is what is asserted).

company.clone(),
manifest_agent.id.clone(),
deps.meter.clone(),
deps.mcp_servers.clone(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every agent clones the whole credentialed decl list. deps.mcp_servers.clone() copies every declared server — resolved AuthMaterial, tool_policies, tool_inventory — into each AgentBlueprint's observer, where only the name, auth.is_configured() and secret_values() are read. N agents × M servers full copies per roster build and per MCP-freshness rebuild, each long-lived in a CompanyAgent; the old path kept one Vec<String> of granted secrets per agent.

Build one Arc<[McpServerDecl]> (or a slim (name, configured, secrets) list) once per HarnessDeps and share it. Non-blocking.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e216f8c: observers no longer hold the decls. The company's McpCallObserver keeps one slim (name, configured) + secrets snapshot and every agent built over unchanged servers gets the same Arc; a change in the servers replaces it. Test asserts sharing and replacement.

.filter(|decl| decl.enabled && grants_cover_server(grants, &decl.name))
.cloned()
.collect();
let declared_wired = !granted.is_empty() && !registry_from_decls(&granted).is_empty();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A full registry is built only to test is_empty(). registry_from_decls(&granted) assembles an oh::mcp::host::static_registry for every granted server and drops it; embed_servers() then re-runs the same grant filter over decls.

If the check is guarding against an unbuildable set, compute granted once, keep it on AgentMcp for embed_servers, and either reuse the registry or test buildability without constructing it. Non-blocking.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly, in acd3c67: granted is computed once, kept on AgentMcp, and embed_servers reuses it, so the grant filter no longer runs twice and the decls are borrowed instead of cloned. I kept the is_empty() check on a built registry on purpose: static_registry degrades an unbuildable set (e.g. an invalid header-auth name) to empty, and that is the only cheap way to keep such a server from being wired; there is no buildability test short of constructing it.


/// A company's effective skill set: the global baseline, the bundle under
/// `source_dir`, and `library`, folded with `deltas`.
pub fn resolve_company(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skill_set::resolve_company has no production caller. Every reader goes through load_runtime_skill_set, the harness through resolve_company_for_agent; the resolve_company GraphQL calls is server::graphql::skills::resolve_company, a different function. Two same-named resolve entry points with one dead is the drift this PR's "six copies collapse into one" aims to remove. Drop it or make resolve_company_for_agent delegate to it. Non-blocking.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 03d727a: resolve_company is #[cfg(test)] now (its only callers are tests); resolve_company_for_agent is the one production entry point.

use crate::company::mcp::{AuthMaterial, McpServerDecl};
use crate::runtime::tools::grants_cover_server;

mod registry_list;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No README.md in the new src/mcp/{agent,decl,policy}/ directories. CLAUDE.md ("Project Structure & Module Organization"): "Every source directory carries a README.md describing what lives there, file by file." src/mcp/README.md has the table, but the three subdirectories carry none; src/harness/built_in/hive_hooks/README.md is the precedent for a sibling subdirectory. Either add them or move the file-by-file rows down. Non-blocking.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7631505: added README.md to mcp/decl, mcp/policy and mcp/agent with the file-by-file tables, and mcp/README.md now points at them.

@sanil-23 sanil-23 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 7631505. All nine items from the first pass are addressed, each with a regression test, and I checked the fixes against the delta rather than the replies alone:

  • AuthDiscovery from a failed metadata fetch is a 500 "discovery failed" again; only the no-usable-authorization-server case says "paste a token".
  • A registry-install 401 gets no credential hint (Option<bool> configured-ness, call_status_kind).
  • The company half of the sign-in id is percent-escaped and split on the first /, so any server name round-trips.
  • An unknown or consumed state renders the 400 "Sign-in expired" page; other exchange errors stay 502.
  • The matched tinymcp prose is pinned by the include_str! test, so a reword fails CI instead of silently changing a status. Typed variants remain the right tinymcp-side follow-up.
  • One shared (name, configured) + secrets snapshot per company observer; granted computed once as borrowed refs; the registry-emptiness check kept for the unbuildable-set reason, which holds.
  • skill_set::resolve_company is test-only; the three src/mcp/ subdirectories have READMEs.

CI is green on every lane at this head. One non-blocking nit inline on observe.rs.

/// What classifying and scrubbing a failure needs of the declared servers: who
/// is configured, and every credential value to scrub. Held once per company
/// observer, not per agent.
#[derive(Debug, PartialEq, Eq)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (non-blocking): Debug over every credential value. ObservedServers.secrets is every declared server's bearer, header, query-param and OAuth token value, and this derive prints them all in the clear on {:?}. Nothing formats it today (AgentMcpObserver and McpCallObserver derive no Debug), so it is a footgun rather than a leak: the first #[derive(Debug)] added to AgentMcpObserver, or a {:?} in a tracing line or an assert_eq! on the snapshot, dumps the company's MCP credentials into logs or test output.

share_servers only needs PartialEq. Drop Debug, or give it a redacting impl the way tinymcp's OAuthBundle and this crate's CompanyAgent do.

@oxoxDev
oxoxDev merged commit c589a64 into tinyhumansai:main Oct 9, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move skills and MCP machinery onto tinyskills and tinymcp

2 participants