docspluginsextensionscreating an extension

Creating an extension

An extension is a standalone package that adds a capability to any agent — host-side code with a declared config, secrets, tools, lifecycle, and an admin hook. Build one when a capability is worth reusing across agents.

defineExtension

An extension is a value returned by defineExtension(). It declares its schemas and lifecycle; create() builds the per-agent instance that carries the tools.

defineExtension
export const myExt = defineExtension({
  name: 'my-ext',
  configSchema: MyConfigSchema,
  secretsSchema: MySecretsSchema,
  create: (ctx) => new MyExtension(ctx),
  connect,  // optional admin hook
});
FieldTypeDescription
namestringRegistry key, and the config/ext/<name>/ folder name.
configSchemaZodType<TConfig>Behavioral policy, merged from the blueprint and operator overrides.
secretsSchemaZodType<TSecrets>Credentials, read from config/ext/<name>/secrets.json.
onServerStart? · onServerStop?(log) => Promise<void>Server-level shared resources (e.g. a subprocess pool). Run once.
create(ctx) => ExtensionInstanceBuild one instance per agent that enables the extension. The instance carries tools, handle, an optional promptSection, and onAgentStart/onAgentStop.
connect?(ctx) => Promise<Result>Admin credential check and resource discovery — see below.

Config & secrets

Two schemas, two files. configSchema validates behavioral policy; secretsSchema validates credentials. The operator's values land in config/ext/<name>/config.json and config/ext/<name>/secrets.json; the blueprint author declares the defaults in capabilities.json. See Configuring agents for the operator's-eye view of those files.

📖 JARGON
Locked by default. A bare value the author sets in capabilities.json is fixed — the operator can't override it. Wrap it as { unlocked: true, value } to let the operator change it in config.json. The framework strips enabled and channel before your schema sees the config.

ExtensionContext

create(ctx) receives the context — the validated config and secrets, plus the surfaces an extension is allowed to reach for:

MemberDescription
config · secretsMerged, validated config and credentials.
privateDirext/<name>/ — persistent extension state, never mounted into the container.
sharedDirshared/ext/<name>/ — mounted read-only at /shared/<name> for the agent.
hasChannelWhether the agent paired a dedicated channel for push.
deliver(text, opts?)Push a message into the agent (see deliver & push).
logStructured logger scoped to the extension.

Lifecycle

An extension runs at three scopes:

ScopeHooksWhen
ServeronServerStart · onServerStopOnce, around server start/stop — shared resources.
Agentcreate → onAgentStart → onAgentStopPer agent that enables it, at load and shutdown.
CallhandleEvery tool call.

Editing capabilities.json reloads the agent's extensions; editing a single config/ext/<name>/ file reloads just that one.

Tools & handle

Each tool is a ToolDefinition; handle dispatches every call. The tool layer is your policy boundary — parse the args, enforce scope, and return a result.

FieldTypeDescription
namestringThe MCP tool name, by convention {ext}__{action}.
descriptionstringWhat the tool does — the agent reads this.
schemaRecord<string, ZodTypeAny>Zod schemas for the tool's parameters.
approval?objectOptional human-approval gate — see below.

handle(toolName, args, call) is single dispatch by tool name. Use call for the per-conversation staging dirs, and return textResult(...).

Approval gates

A tool can require human approval before it runs. The agent calls it as usual; the participant gets an interactive prompt and the outcome arrives as a follow-up.

FieldTypeDescription
enabledbooleanWhether this call needs approval (resolved from config).
expiry?numberSeconds an approval stays valid (default 3600).
preview(args) => PreviewBuilds the human-facing summary and optional detail.
filter?(args, ctx) => 'approve' | 'skip' | 'block'Per-call decision. ctx.wasApproved(...) lets a call inherit trust from an earlier approval in the same conversation.

promptSection

An instance may contribute a promptSection — a string injected into the agent's system prompt every turn. Keep it short, and condition it on the config and hasChannel (for example, drop the subscription guidance when the agent has no channel to push to).

deliver & push

ctx.deliver(text, { replyTo? }) pushes a message into the agent — routed to the paired channel when one is configured — and returns the agent's first response. This is how a subscription or watch wakes the agent when something happens, rather than waiting to be asked.

connect

The optional connect hook powers the dashboard's Connect button: it validates the credentials and discovers what's reachable — folders, calendars, chats — in one call. Return the inventory in state, parsed through a Zod schema you export, so the admin UI can render it. An extension with no credentials to check (web-fetch) simply omits it.

connect
async function connect({ secrets }) {
  const folders = await probe(secrets);   // verify creds + discover
  return { ok: true, message: 'Connected.', state: { folders } };
}

Staging & storage

WhereRole
call.stagingDirFiles the extension writes for the agent to Read — per conversation, cleared when it ends.
call.stagingOutDirFiles the agent writes for the extension to pick up.
privateDirPersistent extension state; never mounted.
sharedDirRead-only mount the agent sees at /shared/<name>.

Package

An extension is a self-contained package under packages/ext-<name>/ — its own code, schemas, and a manual — so it can be reasoned about and shipped on its own.

packages/ext-my-ext/
  package.json        # @getcast/ext-my-ext
  src/index.ts        # the defineExtension() export
  manual/README.md    # mechanical reference (required)
  manual/SKILL.md     # behavioral skill (standard)

src/index.ts exports the defineExtension() result. The package peer-depends on @getcast/extension-schema and zod — the portable contract, free of server internals — with any protocol clients or parsers as direct dependencies.

Manual

The manual is how the extension explains itself to the rest of Cast — the reference each surface reads instead of its source. It's two files: README.md (required) carries the mechanical contract, and SKILL.md (standard) is behavioral guidance an author weaves into a blueprint when the agent picks up the extension.

README.md is organized into sections, each written for a different reader:

SectionWhat it's for
USAGEHow the agent should use the tools.
CONFIGThe behavioral settings and what each controls.
SECRETSThe credentials the extension needs.
CHANNELChannel pairing and push behavior, when the extension has it.
STORAGEWhere the extension keeps its state.
SECURITYRisk levels and how to compose a safe config.
ADMINThe build spec for the extension's admin page.
SERVICE APIPublic methods for using the extension from a service.

ADMIN is worth singling out: it specifies the fields the dashboard shows, their input types and help text, how the connect hook's discovered resources are surfaced, and how credentials are validated — so the admin page is built from the manual rather than reverse-engineered from the code. Driving the admin UI is one of the manual's jobs, not the whole point.

Registering

Import the package into the server entry, register it, and enable it on an agent. Registration is fail-fast — a duplicate name throws at startup.

// packages/cast/src/index.ts
import { registerExtension } from './extensions/registry.js';
import { myExt } from '@getcast/ext-my-ext';

registerExtension(myExt);

// blueprint/props/capabilities.json
{
  "extensions": {
    "my-ext": { "enabled": true, "channel": "inbox" }
  }
}

Service API

An extension can also be driven directly from an agent service rather than registered with the server — which is how a capability reaches one agent without changing the Cast runtime. To support that, expose public methods on the instance beyond the tool handlers, and document them in the manual's SERVICE API section. Those methods run below the approval gates that live in handle, so the calling service is the trusted party and applies its own policy. The using-side wiring is in Extensions.