MCP · 14. Local versus hosted

Three tools are hosted-gated: issue_passport, sync_pull and sync_push. That is the entire hosted-only set, and it drops to two when CORECRUXD_AGENT_PASSPORTS=1. Everything else in the 118-tool catalogue is local, and a tool that is not in the tier table at all defaults to local.

This chapter is explanation. It exists because we make a claim about our own product, the free tier is never clipped to sell you the paid one; hosted-only tools are absent from the local token catalogue, and a reader is entitled to know precisely what that claim covers and where it stops. The verdict is it holds, with three caveats. The caveats are below, stated as plainly as the claim.

For the tools themselves, see chapter 12. For how tools/list is filtered, chapter 11, section 11.8.

14.0 In plain English

Software that comes in a free version and a paid one has a standing temptation, which is to make the free version worse than it needs to be so that the paid one looks necessary. The usual shape is a tool that appears in the menu, lets you click it, and then tells you to upgrade. This chapter exists to answer a fair question about our own product: does the free tier do that?

The claim we make is that it does not. Hosted-only tools are absent from the local catalogue rather than present and disabled, so a locally running daemon does not advertise capabilities it will refuse to perform. Three tools are hosted-gated in total, and anything not listed in the tier table defaults to local.

That is a claim about our commercial behaviour, not a technical contract, which is precisely why it needs stating carefully rather than asserted in a marketing sentence. This chapter is written as explanation and reaches a verdict rather than a specification: the claim holds, with three caveats, and the caveats are set out at the same volume as the claim in §14.5.

You will want this chapter when you are deciding whether to build on the local daemon and need to know what you are committing to, and when you are evaluating whether the hosted plane is worth paying for. §14.1 is the table; §14.4 is the wording we think is defensible; §14.5 is where a sceptical buyer should start, because it is written for them.

The thing people get wrong is reading "absent from the local catalogue" as a security boundary. It is not one, and §14.3 says so plainly: call-time enforcement fails open without an RCX router in front of it. Absence from the catalogue is a product-honesty property, meaning we do not dangle what we will not deliver. It is not a guarantee that a determined caller cannot reach a gated surface, and it was never intended to carry that weight.

14.1 The tier table

There are exactly two tiers in the code, Local and HostedGated (tool_surface.rs:10). There is no Pro tier and no Enterprise tier in this dimension.

The table itself, TOOL_SURFACE (tool_surface.rs:23), has 36 entries against a 118-tool catalogue. Unlisted tools default to Local:

// tool_surface.rs:208-213
pub fn tool_tier(name: &str) -> ToolTier {
    TOOL_SURFACE.iter().find(|entry| entry.name == name)
        .map_or(ToolTier::Local, |entry| entry.tier)   // unlisted ⇒ Local
}

That fallthrough is deliberate and tested, unknown_tools_default_local_for_backward_compatibility (tool_surface.rs:249).

The hosted-only set is a closed, literal set of three:

ToolSourceMarkerMoves with a flag?
issue_passporttool_surface.rs:191[hosted]Yes, becomes [local] under CORECRUXD_AGENT_PASSPORTS=1
sync_pulltool_surface.rs:196[hosted]No, pinned by test mod.rs:3355
sync_pushtool_surface.rs:201[hosted]No, same test

The other 33 explicit entries, and the roughly 82 tools absent from the table entirely, are [local]. CORECRUXD_AGENT_PASSPORTS is the only tier-moving flag in the codebase (mod.rs:2446, :2699).

14.2 Three artefacts, three different answers

"Is a hosted tool present?" has no single answer, because there are three different things a reader might mean by "the catalogue". This is the crux of the whole chapter.

ArtefactHosted tools present?Evidence
The RCX capability token's capability listNo, absent entirelymod.rs:2699; test mod.rs:3458
tools/list from a live shipped daemon, router present, free-local tokenNo, filtered outmod.rs:2490 plus crux-router/src/lib.rs:212; test mod.rs:3576
The static catalogue, list_tools() or list_tools_json()Yes: visible, [hosted]-marked, with _meta.crux.upgrade attachedmod.rs:2441, :2594; tests mod.rs:3342, :3433
tools/list where no RCX router is wired, embedders, testsYes, visible and annotatedmod.rs:2492

The exclusion in the token path is a single filter (mod.rs:2699):

let promoted_local = agent_passports_enabled && tool.name == "issue_passport";
if !promoted_local && !vaultcrux_local::tool_surface::is_local_tool(&tool.name) {
    return None;
}

There is a second, independent reason the same result holds. mint_free_local_token creates exactly one backend, named "local" (crux-router/src/lib.rs:456, :490). A free-local token contains no hosted.vaultcrux.com backend at all, so the router's backend selection returns nothing and the decision is CapabilityNotPermitted (crux-router/src/lib.rs:212). Even if a hosted capability string leaked into a free token, there would be no backend to route it to.

The shipped daemon always mints and attaches that token (main.rs:511, :1131). So for anyone running corecruxd, the production tools/list path is the filtered one.

14.3 Call-time enforcement

Filtering the list is not the guarantee. Refusing the call is. The enforcement point is enforce_rcx_tool_capability (dispatch.rs:641), called at dispatch.rs:480.

PropertyValue
JSON-RPC code-32030, CAPABILITY_DENIED (dispatch.rs:27)
MessageRCX capability denied: denied:capability_not_permitted (dispatch.rs:683)
data{reason_code, mode: "refused", token_id, token_hash, stamp: {header: "X-Crux-Mode"}, refusal_receipt, upgrade_hint} (dispatch.rs:664)
refusal_receipt.event_typercx.capability_token.call_refused.v1
upgrade_hint for hosted toolsnull

Test: tools_call_denied_by_rcx_router_returns_refusal_receipt (dispatch.rs:852).

The null upgrade_hint is worth understanding rather than fixing in your head. upsell.rs matches only the five metered service scopes, dense rerank, managed dense, and the three extraction scopes (upsell.rs:49). A denial on crux-mcp.sync_pull falls through to None (upsell.rs:59). A refused hosted tool tells you it was refused; it does not try to sell you anything at the point of refusal.

Note also that scope.rs is not involved in this decision. It handles agent-private fact and session prefixes only.

14.4 The verdict, in defensible wording

The following paragraph is exactly true of the code as it stands at commit 93b41a7. Use it verbatim rather than paraphrasing it.

The free and local tier's RCX capability token carries no hosted-only capability: rcx_local_capabilities() drops every HostedGated tool (mod.rs:2699), so issue_passport, sync_pull and sync_push (tool_surface.rs:191) never appear as capability strings in a free-local token, and that token contains no hosted.vaultcrux.com backend at all (crux-router/src/lib.rs:490). Because the shipped daemon always mints and attaches that token (main.rs:511, :1131), those tools are consequently filtered out of the tools/list response a local-tier agent receives, and are refused at call time with JSON-RPC -32030. Separately, the unfiltered static catalogue does list all three, prefixed [hosted] and annotated with _meta.crux.upgrade, deliberately visible but not callable.

14.5 Three caveats a buyer could reasonably raise

14.5.1 "Absent" applies to the token, not to the catalogue as a data structure

The static catalogue lists all three hosted tools, marked [hosted], with an explicit upgrade signpost in _meta.crux.upgrade. This is a deliberate, tested product decision (mod.rs:3426), not an oversight, and the code comment says so in as many words (mod.rs:2589):

hosted/metered tools are already listed (not hidden) on local installs

So: "hosted tools are invisible to free users" would overclaim. "Hosted tools are listed but not callable, and hold no capability in your local token" is exact. Use the second.

The design reasoning is defensible and worth stating rather than hiding. An agent that can see a tool it cannot call, with structured metadata saying why and what would change it, behaves better than an agent that discovers a capability gap by failing. The alternative, hiding the tools, trades a small marketing gain for a worse agent experience.

14.5.2 The hosted-only set is three tools, and two once the passport flag is on

Any marketing that implies a large withheld hosted surface is not supported by this table. Three tools out of 118, two with a flag flipped.

The commercial gate is elsewhere. It is the metered service scopes, dense rerank, managed dense embedding, and the three extraction scopes (upsell.rs:20), and those are not MCP tools at all. If you are evaluating what the paid tier adds, do not evaluate it by counting MCP tools; the answer will be "two" and it will be the wrong question.

14.5.3 Enforcement fails open when no RCX router is wired

enforce_rcx_tool_capability returns None, meaning allow, when there is no router (dispatch.rs:642):

let router = ctx.rcx_router.as_ref()?;

And McpContext::new_default sets the router to None (dispatch.rs:135, :173).

DeploymentHosted-tier enforcement
corecruxd, the shipped daemonEnforced. It always mints and attaches a token (main.rs:511)
An embedder of crux-mcp that skips with_rcx_routerNot enforced. Only the handlers' own passport-tier gates apply

For an embedder, the residual protection is the handlers themselves: sync_pull requires a basic-tier passport (sync.rs:89) and sync_push requires established (sync.rs:140). That is a real gate, but it is a different gate from the tier gate, and it is not the one this chapter is about.

If you embed crux-mcp directly, wire an RCX router. The default is permissive, and it is permissive silently.

14.6 One structural risk

TOOL_SURFACE is a 36-entry partial table against a 118-tool catalogue, with an unlisted-means-local fallthrough and no compile-time exhaustiveness check binding the two lists.

The consequence cuts one way and not the other, which is the useful thing to know:

  • The hosted set cannot silently grow. A new tool defaults to local, so nothing becomes paid by accident.
  • A newly added hosted tool cannot be gated unless someone remembers to add it to the table. If that is forgotten, the tool ships free.

The failure mode is therefore commercial rather than security-shaped, and it fails in the customer's favour. That is the right direction for it to fail, but it is not a guarantee, and no test currently asserts it.

Sources

All line references were verified at commit 93b41a7.