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.
export const myExt = defineExtension({
name: 'my-ext',
configSchema: MyConfigSchema,
secretsSchema: MySecretsSchema,
create: (ctx) => new MyExtension(ctx),
connect, // optional admin hook
});| Field | Type | Description |
|---|---|---|
| name | string | Registry key, and the config/ext/<name>/ folder name. |
| configSchema | ZodType<TConfig> | Behavioral policy, merged from the blueprint and operator overrides. |
| secretsSchema | ZodType<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) => ExtensionInstance | Build 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.
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:
| Member | Description |
|---|---|
| config · secrets | Merged, validated config and credentials. |
| privateDir | ext/<name>/ — persistent extension state, never mounted into the container. |
| sharedDir | shared/ext/<name>/ — mounted read-only at /shared/<name> for the agent. |
| hasChannel | Whether the agent paired a dedicated channel for push. |
| deliver(text, opts?) | Push a message into the agent (see deliver & push). |
| log | Structured logger scoped to the extension. |
Lifecycle
An extension runs at three scopes:
| Scope | Hooks | When |
|---|---|---|
| Server | onServerStart · onServerStop | Once, around server start/stop — shared resources. |
| Agent | create → onAgentStart → onAgentStop | Per agent that enables it, at load and shutdown. |
| Call | handle | Every 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.
| Field | Type | Description |
|---|---|---|
| name | string | The MCP tool name, by convention {ext}__{action}. |
| description | string | What the tool does — the agent reads this. |
| schema | Record<string, ZodTypeAny> | Zod schemas for the tool's parameters. |
| approval? | object | Optional 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.
| Field | Type | Description |
|---|---|---|
| enabled | boolean | Whether this call needs approval (resolved from config). |
| expiry? | number | Seconds an approval stays valid (default 3600). |
| preview | (args) => Preview | Builds 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.
async function connect({ secrets }) {
const folders = await probe(secrets); // verify creds + discover
return { ok: true, message: 'Connected.', state: { folders } };
}Staging & storage
| Where | Role |
|---|---|
| call.stagingDir | Files the extension writes for the agent to Read — per conversation, cleared when it ends. |
| call.stagingOutDir | Files the agent writes for the extension to pick up. |
| privateDir | Persistent extension state; never mounted. |
| sharedDir | Read-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:
| Section | What it's for |
|---|---|
| USAGE | How the agent should use the tools. |
| CONFIG | The behavioral settings and what each controls. |
| SECRETS | The credentials the extension needs. |
| CHANNEL | Channel pairing and push behavior, when the extension has it. |
| STORAGE | Where the extension keeps its state. |
| SECURITY | Risk levels and how to compose a safe config. |
| ADMIN | The build spec for the extension's admin page. |
| SERVICE API | Public 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.