Core concepts
Twelve terms, in the order they build on each other. Each section says what the thing is and why you care. Read once now; the rest of the documentation assumes these words.
Extension
Section titled “Extension”Why you care: it is the only unit of code the platform loads, so it is what you are always building.
An extension is a directory containing an extension.json manifest and
subdirectories for whatever it ships: ui/ for a React surface, main/ for
code that runs in the host process, agents/, skills/, commands/, hooks/,
mcp/. Discovery is by convention — you create only the directories you use.
The id is the directory name: kebab-case, starting with a letter. There is
no id field in the manifest.
An extension has up to two halves, both optional. The renderer half
(ui/index.tsx) runs in the app window and exports a views object. The main
half (main/activate.ts) runs in the host’s Node process and exports
activate(ctx) / deactivate(). A pure-UI app ships only ui/; a headless tool
pack ships only main/.
Manifest
Section titled “Manifest”Why you care: it is the whole declaration — identity, surfaces, and access — and it is validated, so a typo fails loudly instead of quietly.
extension.json is checked against a closed schema at install and at
activation. name and version are required; version is strict
MAJOR.MINOR.PATCH with no pre-release tags. Unknown fields inside
contributes.commands[], contributes.pages[], contributes.settings[], and
contributes.toolMetadata[] are rejected rather than ignored.
Two version fields do different jobs. compat.pluginApi is the range of the
plugin API you target: if the host’s version does not satisfy it, activation
refuses with a message naming both versions. engines.wamp is the
host-compatibility range shown to users in the catalog, and the marketplace
requires it before it will accept an upload.
App, and window ownership
Section titled “App, and window ownership”Why you care: “app” is not a different artifact — it is one field, so you can decide late.
Any page an extension contributes carries a presentation. The default,
"docked", renders your page inside WAMP’s own chrome alongside the assistant.
"app" gives your page the window: WAMP’s chrome is hidden and you draw your
own with AppShell from the interface kit, leaving via
AppShell.BackButton or pluginAPI.ui.exitAppMode().
{ "id": "tasks", "title": "Tasks", "context": "both", "presentation": "app" }Everything else — manifest, permissions, build, distribution — is identical. An app is an extension that owns its window — nothing more.
Contributions
Section titled “Contributions”Why you care: this is how your code becomes reachable; nothing you write shows up until it is contributed.
The contributes block declares what the extension adds to the host:
| Contribution | What it adds |
|---|---|
pages[] |
A sidebar entry and the surface behind it. context is "global", "project", or "both". |
views[] |
A component in a named slot: sidebar.primary, session.dock, statusbar.left, statusbar.right, chatInput.attachments, overlay.global. Each slot passes typed props. |
commands[] |
A palette entry with an optional keybinding and scope. You declare it here and register the handler at activation. |
agents[], skills |
Agent definitions and skills (below). |
settings[] |
Typed per-extension user settings with defaults. |
services[] |
A typed interface other extensions can require by id. |
mcpServers[], agentRuntimes[] |
A tool-providing process, or a foreign agent that can answer a chat turn. |
A dock tool is a session.dock view: a single panel bound to the active
session’s working directory — a file tree, a preview, a browser. It receives the
session id, the directory, and a requestFocus callback, and it must render a
labeled state for “no project chosen yet” and “still loading”.
Your main half does not run until something wakes it. activationEvents
declares what that is — onStartupFinished, onCommand:<id>, onView:<id>,
onTool:<id>, onAgent:<id>, and a few more. There is no eager *: startup
cost stays flat as the user installs more.
Permissions and capabilities
Section titled “Permissions and capabilities”Why you care: half of them are enforced and half are consent signals, and knowing which is which prevents both a broken build and a false sense of safety.
permissions is an array in the manifest: database, network, cron, ai,
terminal, filesystem, http-routes, notifications, process,
auth.identity, mcp, and the structured { "auth.outbound": [origins] }.
Enforced permissions are withheld when undeclared — the capability is
absent from the API your code receives, so ctx.api.cron is undefined and
calling it throws. That covers cron, http-routes, notifications,
process, auth.identity, auth.outbound, mcp, and the hosted cloud store.
Declarative permissions — ai, filesystem, database for local storage,
network, terminal — are shown to the user at install and describe what the
extension intends to touch, but the host does not withhold them at runtime
today. Declare them honestly; do not treat a missing declaration as a barrier.
capabilities is a separate array for narrower opt-ins, such as
webviews.navigate and webviews.executeScript.
The plugin API
Section titled “The plugin API”Why you care: it is the entire surface between your code and the platform, and it comes in two flavors depending on which half you are in.
In the main half, activate(ctx) receives a context whose ctx.api carries the
capability namespaces: ai, agents, data, cloud, cron, http, tools,
webviews, secrets, notifications, services, commands, ipc, events,
fs, process, pty, git, dialog, shell, tokens, storage,
settings, workspace, sessions, auth, disposables. Anything that returns
a disposable goes onto ctx.subscriptions and the host cleans it up for you:
ctx.subscriptions.push( ctx.api.cron.schedule('daily-digest', '0 9 * * *', runDigest),);In the renderer half you import a single object, and every call is awaited:
import { pluginAPI } from '@wamp/plugin-api';It is scoped to your extension — storage keys and tool names are namespaced for
you. Its surface is deliberately narrower: ai, agents, skills, tasks,
auth, data, cloud, fs, dialog, terminal, http, storage, shell,
ui, navigation, workspace, tools, events, mcp, and a callable
notify. The long-lived and security-sensitive namespaces — scheduling,
secrets, child processes, HTTP routes, webviews, services, commands — exist only
in main. To reach those from your UI, register a service both halves share, or
register a tool in main and call it with pluginAPI.tools.call(name, input).
Typed data
Section titled “Typed data”Why you care: it is the storage most apps want, and it is the one thing both halves of your extension can read with the same types.
You declare a schema once — conventionally shared/schema.ts — and hand it to
defineSchema from either half. Main and renderer get the same typed handle;
the renderer’s is a proxy over the process boundary.
const schema = { version: 1, tables: { books: { id: { type: 'text', primary: true }, title: { type: 'text', notNull: true }, status: { type: 'enum', values: ['want', 'reading', 'done'] as const }, added: { type: 'datetime', defaultNow: true }, }, },} as const;Column kinds are text, integer, real, boolean, datetime, json, and
enum. Queries take predicates (gte, in, like, isNull, …) with bare
values as shorthand for equality. Editing the schema later is safe in the
direction that matters: a new column is added to the existing table
automatically, while a change that would destroy stored rows fails loudly
instead. Under it is a per-extension SQLite database, and
data.raw(sql, params) is the escape hatch for a query the predicate DSL cannot
express.
Typed data is local to the machine. An app whose users sign in and expect their
records on a second device uses the hosted document store instead
(cloud.mine and cloud.shared), which needs the database permission.
Why you care: a tool is how the AI does something in your app rather than only talking about it.
A tool is a named function with a JSON-Schema input, registered at activation:
ctx.subscriptions.push( ctx.api.tools.register({ name: 'reading_list_add_book', description: 'Add a book to the reading list.', inputSchema: { type: 'object', properties: { title: { type: 'string' } }, required: ['title'] }, execute: async ({ title }) => data.books.create({ data: { id: `b-${Date.now()}`, title } }), }),);Names match [a-zA-Z0-9_-] and must not contain dots — major model providers
reject those, so an invalid name fails activation immediately. Once registered,
the tool is in the host’s registry: any agent whose definition allows it can
call it, including a foreign coding agent selected in the composer, which is
lent WAMP’s registry over MCP minus the file and terminal categories. Your own
UI narrows a conversation to your tools with tools: { include: ['reading_list_*'] }.
toolMetadata — an icon, a label, a category, and whether the tool is read-only
or long-running — controls how a call renders in the transcript.
Agents and skills
Section titled “Agents and skills”Why you care: an agent is a configured way of working that your app can invoke; a skill is knowledge loaded only when it is relevant.
An agent definition is a markdown file — agents/<id>.md — with YAML
frontmatter (name, description, tools, optionally a model and budget) and
the system prompt as the body. Definitions live in layers with fixed precedence
(builtin, then extension, then user, then workspace); a higher layer shadows a
lower one rather than replacing it, so uninstalling an extension reveals the
built-in agent underneath.
A skill is also a markdown file with frontmatter, but it is not an actor: it is instructional content an agent loads on demand when the task matches the skill’s description. Ship skills for the knowledge your app needs the assistant to have without paying for it in every conversation.
Your code can hand work to an agent and get a result back:
const { text } = await ctx.api.agents.delegate('explorer', 'Summarize this quarter');Call ctx.api.agents.getAvailable() — or pluginAPI.agents.list() — before
naming an agent: the set is dynamic.
Runs and sessions
Section titled “Runs and sessions”Why you care: these are the two units you will see in every log, budget, and piece of UI about work in progress.
A run is one execution of an agent loop. It has an id, a status
(running, completed, failed, cancelled, crashed), a stop reason, a
budget snapshot, accumulated token usage, and a trigger source that records
where it came from: chat, cron, webhook, delegation, one-shot,
external, or goal. Delegation makes child runs, so a piece of work is a tree
rather than a line. Runs are persisted, which is why one can be recovered after
a crash.
A session is a multi-turn conversation. In the shell, a chat session is
always bound to a directory — there is no floating conversation. In your code, a
session is an object you own: created with a conversationId, optionally
narrowed to your tools and extended with your own system prompt, emitting
streaming events, and persisting its history wherever you tell it to.
const { messages, send, isStreaming } = useAISession({ conversationId: 'reading-list.main', tools: { include: ['reading_list_*'] },});The relationship: sending into a session starts a run.
Products
Section titled “Products”Why you care: this is how an extension stops being something users install and becomes something they download.
A product is a brand file — product.json — that turns the platform into a
named binary: application name, icon, window, which extensions are bundled, the
user-data directory, the update feed, and optionally a rootPageId so the app
boots straight into your page instead of the standard shell. The user-data
directory is named after the product, which is why two products on one machine
never share state.
Nothing in your extension changes to be shipped this way. A product is configuration around the same artifact, and the same extension can also be published to the marketplace.
Accounts modes
Section titled “Accounts modes”Why you care: it decides who signs in to your product, and it is one line rather than an architecture.
A product declares accounts.mode:
| Mode | Who signs in | Notes |
|---|---|---|
wamp |
A WAMP account. | The default. Takes no other keys. |
none |
Nobody — the app opens straight into itself. | With an appSlug, each installation gets a stable anonymous identity and an AI credential; without one, the product is purely local and has no hosted AI. |
own |
Your users, who never see a WAMP login. | Requires an appSlug. signUp: false makes it invite-only. |
Your extension code does not branch on the mode. The shell owns sign-in, and what reaches your app is a session either way — which is what lets the same app ship all three ways unchanged.
Where to go next
Section titled “Where to go next”- Extensions and apps — the artifact in full.
- The manifest and Permissions — every key and every gate.
- The plugin API — namespace by namespace.
- Identity overview — accounts, organizations, and credentials as one picture.
- Choosing a path — marketplace, branded product, or your own backend.