Reference

Plugins

Built-in plugins that extend the Routecraft runtime, and the full plugin API: the descriptor, the plugin context, the lifecycle and its faults, ports, hooks, steps and facets.

  1. 01
    llmPluginLanguage models.@routecraft/ai

    Configure provider keys, default models, and global LLM defaults for every agent and llm() call in the context.

  2. 02
    embeddingPluginVectors.@routecraft/ai

    Wire an embedding provider for the embedding() destination and downstream clustering with cosine().

  3. 03
    mcpPluginMCP server runtime.@routecraft/ai

    Expose mcp() capabilities over Model Context Protocol, with JWT, OAuth 2.1, and bearer-token verification built in.

  4. 04
    agentPluginAgent registry and harness.@routecraft/ai

    Register named agents, the tools they can call, and shared defaults like system prompt and principal context.

  5. 05
    acpPluginEditor protocol mount.@routecraft/ai

    Serve the Agent Client Protocol so a person can talk to this instance's agents from their editor, on the same server and behind the same wall as everything else.

  6. 06
    httpPluginHTTP routing.@routecraft/routecraft

    Expose routes over HTTP via the http() source, mounted as the catch-all surface on a named server; JWT, JWKS, or API-key auth at the plugin boundary.

  7. 07
    serversPluginNamed listeners.@routecraft/routecraft

    Declare the HTTP listeners the app binds, by name. HTTP, MCP, and custom plugins mount paths on a shared listener; a surface gets a dedicated port by declaring another named server.

  8. 08
    opsPluginHealth and readiness.@routecraft/routecraft

    Serve liveness, readiness, and operational health, mounted on a named server. Route lifecycle and circuit-breaker state are derived from events the framework already emits; indicators cover dependencies it cannot see.

  9. 09
    shellPluginshell() context defaults.@routecraft/os

    Set context-wide defaults for shell(): isolation tier, timeout, and output cap. The lowest of the three precedence layers, under the ROUTECRAFT_SHELL_ISOLATION operator override and the call site.

  10. 10
    remotesPluginRoutes of other instances.@routecraft/routecraft

    Name other running instances, and every dispatchable route they expose through the ops management API becomes a direct endpoint here, for direct(), forward(), agent tools and the local ops listing alike.

Note

Core adapter defaults (cron, direct) are set with dedicated CraftConfig fields. Each field installs the adapter's plugin for you, so you never list it in plugins. See Configuration and Merged Options.

How to write a plugin, with a worked example for each part, is on the Plugins guide. This page lists every field, member and fault.

First-class config keys

A config key installs the matching plugin with its options. Importing @routecraft/ai augments CraftConfig with keys for the AI plugins: setting llm, mcp, embedding, agent, or sessions is equivalent to listing the corresponding plugin in plugins (sessions maps to sessionsPlugin, documented under Configuration). The plugin's lifecycle and events are identical either way. A key and an explicit plugin for the same feature are two installations of one id and fail with RC1101.

Recommended for declarative configuration:

import { defineProject } from '@routecraft/routecraft'
import '@routecraft/ai' // augments CraftConfig

export default defineProject({
  llm: { providers: { openai: { apiKey: '...' } } },
  mcp: { clients: {} },
})

The factories remain available, for a plugin instance you build once and reuse, or for programmatic composition:

import { defineProject } from '@routecraft/routecraft'
import { llmPlugin, mcpPlugin } from '@routecraft/ai'

export default defineProject({
  plugins: [
    llmPlugin({ providers: { openai: { apiKey: '...' } } }),
    mcpPlugin({ clients: {} }),
  ],
})

defineProject

defineProject({ plugins, ...config }) declares a project and returns { craft, config, plugins }. craft.config.ts default-exports it, and craft start reads that export.

MemberTypeMeaning
craft()() => PreFromBuilderA route builder typed by exactly the installed plugins plus the default plugins. A step or facet of a plugin the project does not install is a compile error.
configCraftConfigThe configuration to start the application with, plugins included. Pass it to ContextBuilder.with() or testContext().with().
pluginsreadonly Plugin[]The plugins the project lists, as given.

A misspelled config key is a compile error, as it is with defineConfig. A config key that installs a plugin with steps or a facet (deferral) types the project's routes when it is set: the package registering the key merges its plugin type into ConfigKeyPlugins, so a third-party key gets the same typing as a first-party one. The root craft() export is typed by the plugins @routecraft/routecraft ships; a route that uses an uninstalled plugin's step refuses to start with RC1111, and reading an uninstalled plugin's facet fails with the same code.

A plain config object still loads: craft start accepts a default-exported defineProject(...), a named craftConfig export, or a default-exported config object, in that order. craft run <file> reads the file's named craftConfig export.

Plugin descriptor

definePlugin(descriptor) returns the descriptor unchanged, keeping its literal types.

FieldTypeMeaning
idstringDotted identity, unique per application: routecraft.deferral, acme.approvals. A duplicate is RC1101.
namespacestringThe prefix the plugin's facet, route options and events live under. Defaults to the last segment of id; unique per application (RC1102).
requiresPort[]Ports the plugin cannot run without. A missing provider is RC1104.
optionalPort[]Ports the plugin uses when present, read with lookup().
providesPort[]Ports the plugin provides in bind. Two providers of one port is RC1105.
replacesPort[]Ports whose other provider this plugin displaces. Each must also be in provides (RC1106). The displaced plugin still binds.
installsPlugin[]Plugins this one brings along, installed ahead of it and once per id, unless the application lists that id itself.
repeatablebooleanSeveral installs may coexist, named id#1, id#2, ... in list order. A repeatable plugin may not provide or replace a port, or declare hooks, points, steps or a facet. It binds ahead of every other plugin that uses the ports it uses, so a reader sees every contribution.
keepsAlivebooleanThe plugin owns a lifetime past the routes (a listener, a subscription): when every route has completed, the application keeps running until stopped.
hooksHooksHooks in the chain's slots, or at a point another plugin declares.
points{ name: string }[]Moments this plugin declares and invokes from its own steps, where other plugins add hooks.
stepsRecord<string, StepFactory>Route methods this plugin adds, by name. See steps.
facet(exchange) => viewWhat ex.<namespace> reads. See facets.
bind(c)(c: PluginContext) => void | Promise<void>Require, provide, observe, register routes. Runs in dependency order.
start(c)(c: PluginContext) => void | Promise<void>Begin work that needs running routes. Awaited.
stop(c, info)(c: PluginContext, info: StopInfo) => void | Promise<void>Release what bind and start acquired. Reverse order.

Plugin context

bind, start and stop receive a PluginContext. A plugin never receives the CraftContext.

MemberMeaning
id, namespaceThe plugin's own id and namespace.
loggerA child logger carrying the plugin id.
require(port)The provider of a port declared in requires or optional. Throws RC1108 for an undeclared port and RC1104 when an optional port has no provider.
lookup(port)Like require, but undefined when an optional port has no provider.
provide(port, value)Provide a port declared in provides. In bind only: RC1109 for an undeclared port, RC1110 after the application froze.
observe(event, handler)Subscribe to an event. Returns the unsubscribe function, for a subscription that ends before the plugin does; every subscription is released with the plugin at stop, so none needs tracking.
emit(event, details)Emit an event. Any name the EventDetailsMap declares is accepted, the framework's included, so a plugin emits under its own names (declared by augmenting EventDetailsMap) and never poses as the framework to telemetry or audit.
onDispose(fn)Register a release that runs at stop, after the plugin's own stop, in reverse registration order. Every one runs even when another throws, and they run when this plugin's own bind throws after registering them.
frozenTrue once the last bind returned. A plugin that collects contributions through its port refuses later ones with RC1110.
routes.register(...definitions)Add routes. In bind only (RC1110 afterwards).
routes.list(), routes.get(id)Read-only views of registered routes: id, definition, enabled, disabledReason.
routes.hooksOf(id)The hooks that apply to a route in the order they run, each as { slot, point, phase?, plugin, id }. Empty before the routes compile.
executionThe verbs that drive the application, below.

Execution

MemberMeaning
deliver(endpoint, body, headers?)Hand a body to the route listening on a direct() endpoint and resolve with its reply, typed unknown for the caller to narrow. RC5004 when nothing listens.
resume(request)Resume a parked exchange by its token on the plugin's own behalf, with no ingress door and no live principal. RC5052 when no plugin provides continuations.
sweep({ boot? })Run one pass of the kernel's sweep over parked exchanges: heal stale claims, purge settled records past retention, retire overdue ones through each route's error channel. The deferral plugin calls it on its cadence. Resolves with how many were retired.
capabilities()Discoverable capabilities of the enabled routes.
whenStarted()Resolves once every route signalled readiness and every start returned.
requestStop()Ask the application to stop. Returns at once; never await a stop from inside a lifecycle function.

Lifecycle

FunctionRunsThe routes areUse it for
bind(c)While the application is installed, in dependency order, before any route is registeredNot registered yetRequiring and providing ports, resolving config, opening resources, observing events
start(c)After every route has startedRunningWork that drives routes or needs them able to serve
stop(c, info)During shutdown, or when an install or start failed partwayStopping, stopped, or never startedReleasing what bind opened, stopping what start began

The stages an application goes through, and the fault each one raises:

StageWhat happensFaults
1. IdentityOne plugin per id, one per namespace, one token per port name.RC1101, RC1102, RC1103
2. ResolutionEvery required port resolves to exactly one provider.RC1104, RC1105, RC1106
3. OrderA topological sort over requires and optional; a repeatable plugin binds ahead of every other consumer of the ports it uses; ties keep list order. Default plugins come first.RC1107
4. BindEach plugin's bind runs in that order and must provide what it declared.RC1108, RC1109
5. FreezeNothing more is accepted: ports, routes, hooks.RC1110
6. CompileRoutes register; every hook is placed; every position and plugin step a route uses must have a provider.RC1111, RC1112, RC1113, RC1114, RC1115, RC1116
7. StartRoutes start and signal readiness, then each plugin's start.
8. StopReverse dependency order, consumers before providers; failures aggregated.

Stages 1 to 3 run before any plugin binds, so their faults leave nothing to release. Each lifecycle function is awaited before the next plugin's runs. Events plugin:binding / plugin:bound bracket bind, plugin:starting / plugin:started bracket start, and plugin:stopping / plugin:stopped bracket stop (see plugin events).

A throwing start fails context.start() with the original error, and every bound plugin is stopped in reverse order before it surfaces, including plugins whose start never ran. A failure during bind or route registration unwinds the same way: only plugins whose bind returned are stopped, and the original error surfaces unchanged even if a stop throws on the way out.

A stop that lands mid-boot waits for the lifecycle function still running before it stops that plugin, so a plugin always observes its function entered, settled, then stop. No lifecycle function runs once the stop walk has begun.

To abort a boot, throw. To stand the application down without failing the boot, call c.execution.requestStop(), which returns at once. A lifecycle function must never wait for the application's own stop: the stop waits for the function, and neither settles.

stop receives a second argument describing how far the application got:

FieldMeans
partialThis is not a fully started application: it never started (an install that failed partway, or an embedder that built and stopped without calling start()) or a start threw. Routes may not be registered and later plugins may never have bound.
startedThis plugin's own start returned. Always false for a plugin with no start, and during an install-failure unwind.

A plugin that only closes what bind opened can ignore both. A plugin that stops what start began reads started.

start is awaited, so the application is not ready until every start has resolved. Use it for startup work that finishes, and begin unbounded work (a poll loop, a subscription) there without awaiting it. A plugin descriptor may serve more than one application in a process, so per-run state is released with c.onDispose() or keyed by the PluginContext, never held in a closure slot.

Note

Waiting for a context to be ready

ctx.start() resolves when the context stops, not when it comes up: a context with an indefinite route (an HTTP server, a direct() endpoint) keeps running. Await ctx.whenStarted(), or c.execution.whenStarted() from a plugin, for readiness. It resolves once no route is still coming up and every start has finished, and rejects if a start or the config refuses. A single route failing to come up is not observable there, since the context keeps the others running.

Ports

port<T>(name) declares a port. The name is owner.capability@version in lowercase dotted segments; anything else is RC1103. Call it once at module scope and export the token: the token is the identity, and two tokens with one name in one application is RC1103.

Adapters read a port at runtime through the context they run in, with context.require(port) or context.lookup(port).

The ports the framework ships:

TokenNameProvided by
DIRECTroutecraft.direct@1routecraft.direct
RESILIENCEroutecraft.resilience@1routecraft.resilience
CACHEroutecraft.cache@1routecraft.cache
AUTHORITYroutecraft.principals@1routecraft.principals
ENFORCEMENTroutecraft.enforcement@1routecraft.auth
CONTINUATIONSroutecraft.continuations@1routecraft.deferral
WEB_INGRESSroutecraft.servers@1routecraft.servers
HTTProutecraft.http@1routecraft.http
OPSroutecraft.ops@1routecraft.ops
REMOTESroutecraft.remotes@1routecraft.remotes
CRON_DEFAULTSroutecraft.cron.defaults@1routecraft.cron
MAILroutecraft.mail@1routecraft.mail
CARDDAVroutecraft.carddav@1routecraft.carddav
AGENTSroutecraft.ai.agents@1routecraft.ai.agent (@routecraft/ai)
SESSION_STOREroutecraft.ai.session-store@1routecraft.ai.sessions (@routecraft/ai)
MCProutecraft.ai.mcp@1routecraft.ai.mcp (@routecraft/ai)
LLMroutecraft.ai.llm@1routecraft.ai.llm (@routecraft/ai)
EMBEDDINGroutecraft.ai.embedding@1routecraft.ai.embedding (@routecraft/ai)
SURFACESroutecraft.ai.surfaces@1routecraft.ai.surfaces (@routecraft/ai)
SHELLroutecraft.os.shell@1routecraft.os.shell (@routecraft/os)

RESILIENCE, CACHE and ENFORCEMENT fill the chain's route-scope positions. The default providers are exported as resilienceProvider, cacheProvider and enforcementProvider, so a replacement can delegate to them. A step-scope wrapper (.retry() and the rest called after .from()) resolves the same provider when the exchange runs, so a replacement fills both scopes (see Slots). A plugin that replaces a port is named once at boot, at warn for AUTHORITY and ENFORCEMENT.

AUTHORITY decides who an exchange acts for and whether to trust it. It provides mint(claims), brand(principal), isAuthentic(principal), restore(record), isRestored(principal) and read(exchange). authorityOf(exchangeOrContext) returns the application's authority. Every mint and every check goes through it, so a replacement decides both sides; delegate() and the ingress mounts take the authority explicitly rather than assuming the default. defaultAuthority is exported for a replacement that decorates it.

Default plugins

Installed in every application, ahead of its own plugins:

PluginFactoryProvidesSteps and facet
routecraft.directdirectPlugin()DIRECT
routecraft.resilienceresiliencePlugin()RESILIENCE
routecraft.cachecachePlugin()CACHE
routecraft.principalsprincipalsPlugin()AUTHORITY
routecraft.authauthPlugin()ENFORCEMENTsteps authenticate, delegate; facet ex.auth

An application plugin with the same id takes a default's place. A plugin that declares replaces for a default's port is selected over it while the default still binds. The deferral plugin (routecraft.deferral, installed by the deferral config key) adds the defer and resume steps and the ex.deferral facet.

Hooks

Slots

The chain around every route runs in a fixed order. Positions belong to the framework and hold one thing each; a plugin can replace what fills a position through its port, but cannot move, remove or add one. Slots sit between positions, and any number of plugins add hooks there.

In orderKindFilled by
errorslothooks; the route's own .error() runs first
beforeAuthslothooks
authorizepositionENFORCEMENT
afterAuthslothooks
parse, inputpositionsthe kernel; not replaceable
admittedslothooks
throttle, circuitBreaker, retrypositionsRESILIENCE
perAttemptslotwrappers
timeout, concurrencypositionsRESILIENCE
cacheCheckpositionCACHE
the pipelinethe route's steps
cacheStorepositionCACHE
exitslothooks, over completed exchanges only

SLOTS exports the slot names in order. A hook in a slot or point that does not exist is RC1112.

The resilience methods and .cache() placed after .from() are the same positions around one step. The wrapper resolves the same provider when the exchange runs, with scope: "step" and the step's label on the PositionRun ("route" and "route" at route scope), so a plugin replacing RESILIENCE or CACHE fills both scopes; throttle(options, scope) takes the scope, and the step-scope cache runs through CachePositions.wrap. A step wrapped in a method no installed plugin provides fails with RC1111 when it runs.

Phases

PhaseMayEnforced
observereadreturning anything but undefined is RC1115
mutatereturn { headers?, body? } to change the exchange, or nothingwriting an engine-owned header (routecraft.id, routecraft.route, routecraft.operation, routecraft.split_hierarchy) is RC1115. A body patch is applied as returned and never re-validated: in beforeAuth and afterAuth the route's .input() validates it afterwards, in admitted and exit it is trusted, so a hook there adds to the validated shape rather than replacing it, and a hook that must replace the body belongs in afterAuth
validatereturn nothing to allow, or refuse(reason, { kind? }) to refusereturning anything else is RC1115; a refusal fails the run with RC5068, and a door answers the caller with the kind (invalid, unauthenticated, forbidden by default, not_found, conflict, gone, rate_limited, unavailable) and the reason

A slot runs observe, then mutate, then validate. exit has no validate phase. In the error slot the phases are observe and mutate, and mutate is the deciding phase: a hook answers with a recovery body, recovery.drop(), recovery.defer(...) or recovery.rethrow(), or undefined to pass, and the first answer decides. Inside a phase, hooks run in the order the application lists the plugins. When two mutate hooks write the same header in one slot, the later wins and the framework warns once per route naming both.

perAttempt takes wrappers rather than phased hooks: { wrap(proceed, exchange, info) }. A wrapper surrounds every attempt of the route's work, inside retry and outside timeout; it may time, trace or guard an attempt and may throw, but does not change the exchange.

Hook fields

FieldApplies toMeaning
idallThe hook's name, addressed as pluginId/id by hooks.order and hooks.disable. Required, and stable across the plugin's releases: a hook without one is RC1117.
phaseall but perAttemptobserve, mutate or validate.
run(exchange, info)exchange slots and pointsThe hook.
run(error, exchange, info)errorThe error hook.
wrap(proceed, exchange, info)perAttemptThe wrapper.
routesallOnly these route ids. Combined with tags as alternatives.
tagsallOnly routes carrying one of these tags.
runsallThe run kinds the hook applies to: normal, resume, debounce, errorChannel. Defaults to normal only, except in error, which defaults to every kind. beforeAuth, afterAuth and admitted see admissions only: a normal run, and an admission resume (a park raised before the route admitted the exchange), which runs afterAuth and admitted around the authorize it re-runs. An admission resume is an admission, so those slots see it as kind normal and a hook keeping the default runs on it; the error slot, points and perAttempt see the same run as kind resume.
writesmutateThe headers the hook writes, so a conflict is reported at start rather than on the first request.
mayDefererrorThe hook may answer recovery.defer(). Makes every route it applies to deferrable, so the application refuses to start without a deferral runtime (RC5052).
schemaerrorWhat a resume payload must satisfy when the hook parks with recovery.defer(). Read back live at the resume door and validated there (RC5049); a schema that changed or disappeared under a parked exchange refuses the resume (RC5048).

info carries routeId, tags, slot and kind. An error hook's info also carries execution (1 on the first run, 2 once the exchange is a resumed continuation) and forward, which sends to a direct() endpoint under the failing exchange's principal.

An error hook that throws is reported as route:error-handler:failed with scope: "slot" and the hook's id under hook, and the slot continues to the next hook. A failure an error hook decides reaches neither context:error nor route:exchange:failed.

Points

A plugin may declare points: [{ name: 'approvals.decided' }] and run the hooks other plugins place there from its own step, with ctx.invoke(name, exchange) in a step's execute. Another plugin places a hook there under hooks.points: hooks: { points: { 'approvals.decided': { id: 'recordDecision', phase: 'observe', run } } }. Point names are dotted under the declaring plugin's namespace; two plugins declaring one point, or a point named after a slot, is RC1113.

Hooks config

The application settles conflicts in config, under hooks:

KeyTypeMeaning
orderRecord<"slot/phase", string[]>The exact order of one phase of one slot, by hook id (pluginId/id). Hooks it does not name run after the named ones, in plugin order.
disablestring[]Hooks switched off, by id. Slots only; a position is never disabled.

A name in order or disable that matches no installed hook is RC1112.

Steps

Each entry of a plugin's steps becomes a builder method of that name. The entry takes the method's arguments and returns a step built with step<In, Out>().

APIMeaning
step<In, Out>((exchange, ctx) => body)A step that replaces the body with what the function returns. The method exists only on a route whose body is assignable to In, and leaves the route at Out.
step<In, Out>(rawStep)Wraps a step written against Step directly, for one that drops, branches or defers rather than continuing.
BodyA placeholder for the body at the call site. step<Body, Body> with (fn: (body: Body) => ...) in the factory's arguments gives a body-preserving method whose callback is typed at the route's current body.
StepMethods<S, This>An interface a plugin merges its method types into, under its namespace, for a method generic at the call site. The steps entry of the same name still provides the runtime. BuilderState, Retyped and SetBody are exported for writing these.
FacetTypes<S>An interface a plugin merges its facet type into, under its namespace, when the facet's type depends on the route's state.

A route that uses a plugin's step refuses to start unless the plugin is installed (RC1111). Two plugins declaring one step, or a step named after a builder method, is RC1116.

Facets

A plugin's facet is readable as ex.<namespace> in route callables, typed in routes built with the project's craft(). It is computed from the exchange on every read and never stored. A facet named after an exchange field (id, headers, body, logger, context) or method is RC1114. Reading a facet in an application that does not install its plugin is RC1111.

FacetPluginLibrary equivalent
ex.auth.principalroutecraft.authprincipalOf(exchange)
ex.deferralroutecraft.deferraldeferralOf(exchange)

Code that holds a plain Exchange rather than a route's typed one (an adapter, a hook, a helper) uses the library function.


Plugins guide

Write a plugin: ports, hooks, steps and facets with worked examples.

Configuration

Every craft.config.ts field, including the first-class keys that stand in for plugins.

Errors

RC1101 to RC1117 and RC5068, the plugin faults.

Agents and skills

Give a project a model, define an agent, and grant it capabilities as tools.

Previous
Runtime