Skip to content

The intent bus

Every finite request that crosses from one owner to another — plugin to host, CLI to host, agent to host — arrives as an intent: a named, versioned contract with a JSON Schema for its input and output, declared effects, and an explicit audience.

There are 51 canonical ones. All of them are listed, and that list is generated rather than written.

Naming an intent grants nothing

This is the sentence to keep. Authorization is a sequence of separate, fail-closed checks, and the name is only how you say which one you want:

CheckQuestion it answers
Manifest declarationdid this plugin declare it in intents.uses / provides?
Audiencemay this kind of caller — plugin, cli, agent — call it at all?
Capabilitydoes the manifest hold the permission the contract requires?
Scopeis this workspace / tab / pane one this caller may target?
Consenthas a policy-confirmed operation actually been confirmed?
Provider eligibilityis there a registered provider allowed to serve it?
Admissiondoes it fit the queue, payload and concurrency bounds?

Each is separate and each fails closed. There is no path where passing one implies another.

Discovery fails closed too

A caller that may not use a contract does not get to learn it exists.

sh
$ tenon-cli intent describe ui.toast.v1
{"code":"intent_not_found","message":"intent 'ui.toast.v1' is not callable by the CLI principal"}

$ tenon-cli intent describe totally.made.up.v1
{"code":"intent_not_found","message":"intent 'totally.made.up.v1' is not callable by the CLI principal"}

Those are the same answer, and that is the design. intent list returns the policy-filtered projection for that principal, not the catalog.

It has a real consequence for this site: the intent reference is generated from two sources, because a CLI-driven generator genuinely cannot see the plugin-only contracts. Rather than hiding that, the generated pages say which ones and why.

Audiences are exact

A core contract's audience is either {plugin, cli, agent} or {plugin}. There is no "everyone" and no generic app principal for built-in UI.

The second point matters: Tenon's own UI does not impersonate a plugin. Built-in Swift calls typed application services directly, because it shares one semantic owner with them. Public intent providers adapt to those same services. One semantic operation, one implementation, two ways in — never two competing implementations.

Effects are declared, not inferred

Every contract states what it does before it does it:

  • kindread, write, destructive
  • confirmationnever, policy, always
  • idempotency — whether repeating it is safe
  • external — whether it leaves the machine

confirmation: policy means a live interactive confirmation may be required. CLI and agent callers get no standing consent, so an unattended policy-confirmed operation expires rather than silently escalating. A deadline exceeded there is the system working correctly, not a bug to route around by raising the timeout.

Everything is bounded

Queues, payloads, lifetimes and generations all have limits. A deadline covers the whole thing — admission, confirmation, provider execution, settlement — and a request settles exactly once.

A slow provider may delay its own response. It may not block the socket accept loop, the built-in UI, or an unrelated provider: the accept and read loop runs on its own thread, framing and bounded JSON decode happen off the main actor, and UI effects hop to the main actor only for the minimal native operation.

A wait that takes 30 seconds does not make it a stream. terminal.wait.v1 observes asynchronously, settles once, and hands back no handle. When it times out it settles normally with met: false — "it did not finish in time" and "something went wrong" are different facts and are reported differently.

When an intent is the wrong answer

Intents are for finite cross-owner requests with one reply. Reaching for one elsewhere is the most common design mistake in a plugin:

NeedUse
finite request, one reply, another ownerINTENT
an immutable fact, no replyEVENT
many values, or a lifetime you ownRESOURCE
state the host renders or indexesCONTRIBUTION
your own settings, storage, logsSCOPED FACILITY
code inside your own pluginjust call the function

Do not self-send an intent to structure one plugin's code. Intents are a plugin's external contracts, not its module system. Choosing a mechanism is the full decision order.

The scoped-facility allowlist is closed

Exactly three finite plugin→host operations skip the intent path: tenon.settings, tenon.storage, and tenon.log. tenon.path.* is pure local string code and touches no filesystem.

Everything else defaults to INTENT. The list is closed, not a starting point — a new capability means a new contract, not a new shortcut.