Skip to main content
Two surfaces make up everything a paired device can reach: the capabilities you declare and the methods and events you expose. They are enforced in one place — the host — and a client cannot talk its way past either.

Declaring capabilities

Naming them

Capabilities are read by a human under time pressure, at the exact moment they are deciding whether to trust a device. Two rules follow:
  • Group by consequence, not by function. notes.delete is one capability even if three methods can delete. notes.rpcCall is not a capability, it is a bypass.
  • Split when the answer could differ. If someone might reasonably say yes to reading and no to writing, those are two capabilities.
Ten capabilities is a lot. If you are past that, the prompt has stopped being a decision and become a formality.

Policy: what the host will allow at all

capabilities says what exists. permissions says what may ever be granted, and it is evaluated before your approval hook runs — a bug in that hook cannot hand out more than the policy permits.
The defaults are already the conservative answer: auto-approval is capped at low, and every high-risk capability forces a human decision regardless of what pairing.approve returns.

Approving a pairing

A return of true grants everything requested; an array or { caps } grants a subset; false refuses. Compare the SAS digits on both screens — that comparison is the entire defense against a machine-in-the-middle during pairing, and skipping it silently gives that defense up. pairing.autoApprove: true exists for development. It is capped by maxAutoGrantRisk, so it cannot silently grant write access, but it should never reach production.

Exposing methods

Validate every input. Compile-time types say nothing about bytes arriving from another device. A method without inputSchema or validate is trusting a remote peer to be well-behaved.

The handler context

  • ctx.deviceId — which paired device is calling. Use it for per-device state.
  • ctx.requestId — correlates with the client’s request.
  • ctx.signal — aborts when the client cancels or the session drops. Long handlers should check it.
  • ctx.emitProgress(json) — progress events for the same request.
  • ctx.log — a logger pre-bound with device, method and request id.
idempotent: true is a promise you are making to the client: it may replay this call after a reconnect without asking you. Do not set it on anything that appends, charges, or sends.

Events

Events are broadcast to every connected device that holds the capability. There is no per-device event targeting — if a payload is not fit for every device that can subscribe, send it as an RPC result instead. Declare events before emitting them. Declaration is what carries the capability gate; an undeclared event has no gate to apply. A capability marked confirmEachUse: true is refused outright unless the host supplies a prompt:
"session" lasts until that device disconnects; "always" until the TTL expires or the grant is revoked. An unanswered prompt is a refusal, not a hang — which is why the timeout defaults to 60 seconds rather than infinity.

Calling from the client

A call the host refuses rejects with a capability_denied error rather than returning an empty result — see Error Codes. Handle it as a UI state (“this device is not allowed to do that”), not as a network failure.

Next

Permissions concept

The model behind the policy

API Reference

Every method and option

Production Checklist

Before other people run this