Skip to content

Guide: capability exposure

Status: Implemented in v0.1. Concepts: ADR-004, SI-1/SI-2/SI-8.

Exposure answers one question per surface: is this capability reachable here at all? It is the outermost gate (pipeline stage 4) and the easiest one to reason about — a static map you can review in a diff.

The decision, per surface

ts
expose: {
  direct: true,     // your server code calls it via runtime.invoke
  aiSdk: true,      // your own model loop may call it
  mcp: false,       // external MCP clients may NOT
  // workflow, test: same idea; absent = denied
}

Ask per surface, in increasing order of caution:

SurfaceCallerAsk yourself
testYour CIUsually true — governance tests need reach. (The test runtime can also target other surfaces to test their exposure.)
directYour own server codeDoes non-agent code invoke this through the governed path? (Its normal oRPC router path is unaffected either way.)
workflowYour durable jobsWill steps replay? Is the handler idempotent enough for that world?
aiSdkYour model loop, your promptsWould I let a well-meaning intern with these prompts trigger this?
mcpExternal clients, other people's loopsWould I let a third-party integration trigger this? Highest bar.

The reference app's answers are a worked example: searches on aiSdk+mcp+direct; orders.refund on aiSdk+direct only; admin.deleteAccount not a capability at all (example).

Exposure ≠ visibility ≠ permission

Three different mechanisms, commonly confused:

  • Exposure (expose.mcp: false) — structural: nobody on that surface, ever, regardless of actor. Checked at stage 4.
  • Visibility (policy hide() at discovery) — conditional: this actor shouldn't see it listed; enforcement still happens at invoke (SI-2).
  • Permission (middleware / policy deny) — the actor may see it yet not be allowed to run it.

Use the weakest mechanism that states your intent: "never on MCP" is exposure; "only admins see it" is a discovery policy; "only admins run it" is middleware (+ optional policy backstop).

Staged rollout pattern

ts
// 1. Define with empty exposure — capability exists, reachable nowhere
expose: {}

// 2. Enable for tests; write governance tests
expose: { test: true }

// 3. Enable direct; wire into a UI action through the governed path (optional but
//    valuable: same audit trail as agents)
expose: { test: true, direct: true }

// 4. Enable your own loop
expose: { test: true, direct: true, aiSdk: true }

// 5. External surface last, if ever
expose: { test: true, direct: true, aiSdk: true, mcp: true }

Each step is a one-line diff — which is the review ritual working as intended.

Narrowed deployments

Exposure is per-capability; registries narrow whole deployments:

ts
// A read-only MCP endpoint, structurally incapable of writes
const mcpRegistry = capabilities.filter({ sideEffect: ["none", "read"] });
const mcpRuntime = createAgentRuntime({ registry: mcpRegistry, policies: mcpPolicies, audit });

Filtering removes capabilities from that runtime entirely (they're CAPABILITY_NOT_FOUND there); combine with per-capability expose for the full picture. Per-actor narrowing stays in policies — never fork registries per user (concepts/registry.md).

Auditing your surface area

Make the exposed set reviewable and diffable:

ts
// scripts/capability-report.ts — run in CI, commit the output
for (const c of capabilities.capabilities()) {
  console.log(
    `${c.id}\t${c.meta.sideEffect}/${c.meta.risk}\t` +
    Object.entries(c.meta.expose).filter(([, v]) => v).map(([s]) => s).join(","),
  );
}

A PR that flips mcp: false → true on a destructive capability should look exactly as loud in review as it is in consequence.

Anti-patterns

  • exposeAll-style helpers — don't build one; the framework deliberately has none.
  • Exposing "temporarily" for debugging — use the test surface and the test runtime instead.
  • Treating adapter filter options as exposure — they shape listings, not reachability (SI-2).
  • Encoding exposure in descriptions ("not for external use") — prose gates nothing.

Independent community project — not affiliated with or endorsed by the oRPC maintainers.