Reference: capability metadata
Package: @orpc-agent/core. The AgentMeta object is the declaration that turns an oRPC procedure into a capability. It lives in the procedure's ordinary oRPC metadata under the agent key and is validated when createCapabilityRegistry builds the registry.
Type
type AgentMeta = {
/** Model-facing explanation of what the capability does and when to use it. Required. */
description: string;
/** Deny-by-default exposure per surface. Only `true` exposes. Required. */
expose: Partial<Record<ExposureSurface, boolean>>;
/** What the capability does to the world. Required. Never inferred. */
sideEffect: "none" | "read" | "write" | "destructive" | "external";
/** Sensitivity of the operation, independent of sideEffect. Required. Never inferred. */
risk: "low" | "medium" | "high" | "critical";
/** Free-form labels for filtering and policy targeting. */
tags?: string[];
/** Per-capability execution timeout. Default: runtime `defaults.timeoutMs` (30 000). */
timeoutMs?: number;
/** Runtime-managed retries. Only effective under the eligibility rules (SI-11). */
retry?: {
maxAttempts: number; // additional attempts after the first
backoffMs?: number; // base delay, exponential; default 250
retryOn?: (error: CapabilityError) => boolean; // extra predicate over `retryable` errors
};
/** Declares that repeating the operation with the same input is safe.
Required (with retry config) before the runtime will retry a write. */
idempotent?: boolean;
/** Static approval gate. Conditional approval belongs in policies. */
approval?: {
required?: boolean;
type?: string; // e.g. "manager", "human-confirmation"
expiresInMs?: number; // default: runtime defaults.approvalExpiresInMs (900 000)
};
/** Redaction hooks. Both receive a read-only RedactionInfo (surface, capabilityId,
actor, context); one-argument hooks keep working. Pure and total. */
redact?: {
/** Maps validated output to its model-safe form. Applied by the runtime at stage 13. */
output?: (output: unknown, info: RedactionInfo) => unknown;
/** Maps the stored input to what approval surfaces display. Applied by the
application through redactApprovalInput. */
approvalInput?: (input: unknown, info: RedactionInfo) => unknown;
};
/** Capability-scoped policies, evaluated after runtime-level policies. */
policies?: AgentPolicy[];
/** Adapter-specific overrides. Never affects governance. */
adapters?: {
aiSdk?: { toolName?: string };
mcp?: { toolName?: string; annotations?: Record<string, unknown> };
};
};type ExposureSurface = "direct" | "aiSdk" | "mcp" | "workflow" | "test";Field semantics
description (required)
Written for the model: state what the capability does, when to use it, and any preconditions ("Search orders by customer email. Use after resolving the customer id."). This is prompt real estate — keep it under ~300 characters. Not a place for security notes; models don't enforce them.
expose (required)
The exposure map is the whole of SI-1. Absent surface = not exposed. expose: {} is valid and means "capability defined, reachable nowhere" (useful while staging a rollout). The registry does not reject it but inspect() flags it.
Exposure governs only the agent runtime path. The same procedure mounted in a normal oRPC router keeps serving your app's HTTP/RPC traffic regardless of expose — oRPC Agent adds a path, it never blocks existing ones.
sideEffect (required)
| Value | Meaning | Default consequences |
|---|---|---|
none | Pure computation | Retry-eligible |
read | Reads application state | Retry-eligible |
write | Creates/updates state | Never auto-retried unless idempotent: true + explicit retry |
destructive | Deletes or irreversibly changes state | Never auto-retried without explicit config; expect approval policies to target it |
external | Leaves the system (email, payment, third-party API) | Treated like write for retries; expect approval/confirmation policies |
risk (required)
Independent of sideEffect — a read of salary data is read/high. Risk drives nothing automatically; it exists so policies, approval routing, and audit filters have a stable, reviewed classification to target (req.capability.meta.risk === "critical"). Requiring it forces the classification conversation at definition time.
retry + idempotent
Full rules in security/idempotency-and-retries.md. Summary: runtime retries wrap pipeline stage 11 only, fire only on retryable errors, and only for none/read side effects — unless the capability declares both idempotent: true and a retry config, which is an explicit, reviewable statement.
approval
approval.required: true is the static form of requireApproval() — every invocation gates. Use policies for conditional gates (amount thresholds, role exceptions). Both routes converge in pipeline stage 8 and produce identical ApprovalRequests.
redact
redact.output runs after output validation (stage 13), before anything leaves the runtime (adapter results and any opt-in trace/audit payload). redact.approvalInput shapes what approval surfaces render — the application applies it through redactApprovalInput; the hash still binds the full validated input. Redaction functions must be pure and total (throwing fails the execution with EXECUTION_FAILED).
Both hooks receive a second argument, RedactionInfo — { surface, capabilityId, actor, context } — so one capability can return personal data in clear to an internal console on the aiSdk surface and redact it for external MCP clients:
redact: {
output: (o, { surface }) =>
surface === "mcp" ? { ...(o as Customer), manager: undefined } : o,
}The surface is the one the invocation was made on, taken from the authenticated frame — on a resumed approval, the surface recorded when the request was created — never from model input or request JSON (SI-2, SI-3). info is frozen and its actor is a frozen copy: a hook reads the frame and cannot write to it. A hook that ignores the second argument is unchanged (ADR-020).
adapters
Cosmetic/protocol hints only (tool naming, MCP annotations). Nothing here may change governance behavior — enforced by review, stated by design.
Declaring metadata
import { os } from "@orpc/server";
import { agentProcedure } from "@orpc-agent/core";
import * as z from "zod";
const base = os.$context<AppContext>();
const agentBase = agentProcedure(base);
export const searchOrders = agentBase
.meta({
agent: {
description: "Search orders by customer email or order number.",
expose: { aiSdk: true, mcp: true, direct: true },
sideEffect: "read",
risk: "low",
tags: ["orders"],
},
})
.input(z.object({ query: z.string().min(2), limit: z.number().int().max(50).default(10) }))
.output(z.object({ orders: z.array(OrderSummary) }))
.handler(async ({ input, context, signal }) => {
return { orders: await context.orders.search(input, { signal }) };
});Validation at registry build
createCapabilityRegistry validates every included procedure's metadata and throws a single aggregate error listing all problems:
- missing
description,expose,sideEffect, orrisk; - unknown surface keys in
expose; retryonwrite/destructive/externalwithoutidempotent: true;- adapter
toolNamecollisions after name mapping; timeoutMs/expiresInMsnon-positive.
Fail-fast at startup, never at first invocation.
Related
- Concepts: capabilities · Guides: defining capabilities, capability exposure
Contextual discovery
AgentMeta.discovery?: "discoverable" | "contextual" marks model presentation. Default: discoverable. Portable descriptors include the resolved value; the remote AI SDK adapter excludes contextual entries from its unbound tool list. A browser bridge may supply a bound presentation. This flag grants no authority and does not disable exposure or execution. Contextual model actions must still call an agent gateway; they must never switch to surface: "direct" to bypass approvals.