Skip to content

Plugin API reference

Look up the exact signature of anything an extension can call, the permission it needs, and whether the shipped type declarations know about it. For what each namespace is for, with worked examples, read The plugin API first — this page is the lookup layer that guide defers to.

Members are grouped by where the code runs:

  • ctx.api.* — the main-process half, reached through the ctx passed to activate(ctx). Declared as PluginAPI in @wamp/extension-sdk.
  • pluginAPI.* — the window half, imported from @wamp/plugin-api.

Every table carries an In types column. No means the member exists at runtime but is absent from the published declarations, so calling it is a compile error until you cast around it. Treat a No as unsupported: undeclared means unversioned, and it can change without notice.

The Permission noted on a namespace is the manifest permission that must be declared for the namespace to exist. Without it the member is undefined — not a thrown error — so guard or declare. See Permissions.

Four permission strings are not runtime gates: ai, filesystem, network, and terminal are disclosure labels shown to the user, and the host does not withhold those capabilities today. Read this column as a statement about which namespaces are present, not as a security boundary. Permissions constrain host APIs only; installed Node and UI code remains trusted code.

ctx.api.db, the old raw-SQLite handle, no longer exists. Raw SQL survives in exactly two places: the migrations array of a typed-data schema, and data.raw(sql, params).

ctx carries three members besides api.

Member In types Description
ctx.pluginId: string Yes This extension’s id — its directory name
ctx.pluginPath: string Yes Absolute path to the extension’s own directory
ctx.log Yes info / warn / error / debug, each (message, ...args)

Permission: none.

Member In types Description
complete(input, options?): Promise<string> Yes One-shot completion; resolves to the assistant text
stream(input, callbacks, options?): Promise<void> Yes The same completion, streamed through callbacks.onChunk
generateObject<T>(input, schema, options?): Promise<T> Yes Completion constrained to a JSON schema
completeWithUsage(input, options?) Yes complete plus { text, usage }
abort(requestId): void Yes Cancels an in-flight stream by the id onRequestStart gave

input is either a prompt string or a { role, content }[] array.

options is ExtensionAIOptions: model? (defaults to the user’s selected model), system?, maxTokens? (defaults to 4096), temperature?.

callbacks for stream:

Callback In types Description
onChunk(text) Yes Fires per streamed text fragment
onEnd(usage, stopReason?) Yes stopReason: 'max_tokens' means the answer was cut off, not finished
onError(error: Error) Yes Terminal failure
onRequestStart(requestId) Yes The id abort takes

Cancelling a main-process stream takes both members: keep the id onRequestStart hands over, then call abort(id). In the window, pluginAPI.ai.stream returns a cancel function instead.

There is no multi-turn session primitive. A chat-shaped UI keeps its own message history (component state, pluginAPI.storage, or typed data) and calls complete/stream once per turn with that history as input.

Permission: none.

Member In types Description
delegate(agentType, task, options?) Yes Runs an agent to completion and resolves with its result
getAvailable(): string[] Yes Agent ids currently registered

options: context?, todos?, skills?, modelOverride? (a tier name — fast, balanced, powerful — or a full model id). The result is { text, toolsUsed, stopReason, usage, costUsd }; costUsd is 0 for unpriced models. The window’s delegate additionally accepts outputSchema and returns object / schemaError, which the main-process declaration does not carry.

Permission: none.

Member In types Description
register(tool): Disposable Yes Registers a tool the assistant can call

tool fields: name, description, inputSchema (JSON Schema object), execute(input, ctx), plus optional category, group, display, and policy. execute returns a string or an array of content blocks ({ type: 'text', text } / { type: 'image', data, mediaType, dims? }). Tools sharing a group appear to the model as one grouped operation. Policy supports readOnly, mutates, parallelSafe, untrustedSource, egress, longRunning, compactable, and persistenceQuota; read-only tools run concurrently unless parallelSafe is false. Declare egress: 'url' when the call sends a request to the host its url argument names, or egress: 'any' when it can reach hosts it picks while running. Outside Full access the user then allows each host (or, for 'any', the tool) once per chat, and an unattended run is refused.

Tool names must match ^[a-zA-Z0-9_-]+$. A name containing a dot throws inside activate(), which faults the whole extension — so write notes_search, never notes.search. Schemas, display metadata, and policy flags are covered in Contributing tools.

The second argument to execute is typed unknown. The SDK publishes one narrow shape for it, ToolContext, and the runtime object has more.

Member In types Description
ctx.services?.client?.request(method, params?, opts?) Yes Reverse-RPC to the connected client; absent on a desktop host, which is the client
ctx.sendToolUse(name, input, status, result?, meta?) No Pushes an interim tool-use card to the UI; status is running, success, or error

Permission: none. Typed data is stored in the engine profile; remote renderer queries cross the engine transport.

Member In types Description
defineSchema(schema): Promise<DataAPI<S>> Yes Creates or migrates the tables and returns the typed query surface

The returned object has one property per table plus raw:

Member In types Description
findMany(args?): Promise<Row[]> Yes where, orderBy, limit, offset
findUnique(args): Promise<Row | null> Yes Single row by where
create(args): Promise<Row> Yes Inserts args.data and returns the stored row
update(args): Promise<{ changes }> Yes Applies args.data to rows matching args.where
delete(args): Promise<{ changes }> Yes Deletes rows matching args.where
count(args?): Promise<number> Yes Row count
raw<R>(sql, params?): Promise<R[]> Yes Raw SQL escape hatch; rows come back untyped

The predicate builder q (eq, ne, in, notIn, like, isNull, isNotNull, now, and the comparison operators) is imported from @wamp/extension-sdk. Schema shape, column types, and migrations are in Typed data.

Permission: none for all three.

Member In types Description
storage.get<T>(key, defaultValue?): Promise<T | undefined> Yes Per-extension key/value read
storage.set<T>(key, value): Promise<void> Yes Write
storage.delete(key): Promise<void> Yes Remove one key
storage.keys(): Promise<string[]> Yes Every key this extension has stored
secrets.set(key, value): Promise<void> Yes Encrypted write
secrets.get(key): Promise<string | null> Yes Encrypted read
secrets.delete(key): Promise<void> Yes Remove
secrets.has(key): Promise<boolean> Yes Existence check without decrypting
settings.get<T>(key): Promise<T | undefined> Yes The user’s override, falling back to the contributes.settings default
settings.onDidChange<T>(key, callback): Disposable Yes Observe writes to one setting in the current host process; dispose the returned subscription during deactivation
settings.set<T>(key, value): Promise<void> Yes Persists under settings:<key> in this extension’s storage

Permission: none for both.

Member In types Description
commands.register(cmd): Disposable Yes cmd carries the handler inside the descriptor
commands.execute(id, args?): Promise<unknown> Yes Invokes any registered command
commands.getAll(): Command[] Yes Every registered command
services.register<T>(id, impl): Disposable Yes Publishes a service under id
services.get<T>(id): T | undefined Yes Lookup that tolerates absence
services.require<T>(id): T Yes Lookup that throws when absent
services.onRegister<T>(id, cb): Disposable Yes Fires when a service appears
services.onUnregister(id, cb): Disposable Yes Fires when it goes away

A Command is { id, title, keybinding?, category?, scope? }, where scope is focused-view, focused-slot, global, or an app-specific string. Registering with no handler in either position throws.

Permission: none for both.

Member In types Description
ipc.handle(channel, handler): () => void Yes Answers pluginAPI.ipc.invoke from your UI half
ipc.broadcast(channel, payload): void Yes Pushes to every window; throws synchronously if the channel’s prefix is not a declared ipcNamespaces entry
events.emit(event, ...args): void Yes Deprecated; use ipc.broadcast for UI notifications

ipc.handle is generic in its argument tuple, so a typed handler such as (_e, req: SpawnRequest) => … infers without a cast.

Permission: none. These call Node directly with the path you pass, so they are not restricted to the workspace.

Member In types Description
readFile(path): Promise<string> Yes UTF-8 read
readFileBuffer(path): Promise<Buffer> Yes Binary read
writeFile(path, content): Promise<void> Yes UTF-8 write
exists(path): Promise<boolean> Yes Access check
readDir(path): Promise<string[]> Yes Entry names
mkdir(path): Promise<void> Yes Recursive create
copyFile(src, dest): Promise<void> Yes Copy
remove(path): Promise<void> Yes Delete
stat(path) Yes { size, isDirectory, isFile, mtime }
getDataPath(subdir?): string Yes This extension’s own data directory, created if missing
join, dirname, basename, extname Yes Path helpers, synchronous
Member Permission In types Description
dialog.openFile(options?): Promise<string[] | null> none Yes Native picker; options.directory picks folders, options.multiSelections allows several
dialog.saveFile(options?): Promise<string | null> none Yes Save picker with title, defaultPath, filters
notifications.show({ title, body, silent? }): void notifications Yes System notification; works with no window open
shell.onWillOpenExternal(handler): Disposable none Yes Claims a URL open before the OS browser sees it

An interceptor returns true to claim the URL. Handlers run in registration order across extensions, the first true wins, and a throw is treated as fall-through. Only user-initiated opens travel through this surface.

Permission: none, but navigation control, script injection, and request interception each need a matching entry in the manifest’s capabilities.

Member In types Description
create(options): WebviewHandle Yes options: id, html?, url?, enableScripts?, partition?, surface?, webRequestRewrite?

enableScripts defaults to true; pass false to disable page JavaScript. Native views always retain their page while hidden.

WebviewHandle:

Member In types Description
id Yes Readonly string
webview.asWebviewUri(localPath): string Yes Owner-bound URL for a file inside this extension
webview.cspSource Yes CSP source authorizing URLs returned by asWebviewUri
loadURL(url): Promise<void> Yes Navigate; resolves on load
goBack(), goForward(), reload(opts?) Yes History and refresh
executeScript(source, options?): Promise<unknown> Yes options.world is isolated or main
onWillNavigate(handler): Disposable Yes Return allow, deny, or { redirect }
onResourceRequest(handler): Disposable Yes Return { cancel?, redirect?, modifyHeaders? }
getState<T>(), setState<T>(state) Yes Per-webview state slot
setSurface(surface): void Yes Moves between visible and headless without reloading
reveal(): void Yes setSurface('visible') plus raise-to-front
setBounds(rect): void Yes Window pixels, not CSS pixels — multiply by devicePixelRatio
openDevTools(opts?): void Yes mode: bottom, right, undocked, detach
dispose(): void Yes Destroys the webview
Member Permission In types Description
http.route(method, path, handler): registration http-routes Yes Callable disposer with ready: Promise<void>; method is GET, POST, PUT, DELETE, or PATCH
http.baseUrl(): string http-routes Yes Authoritative after the first registration’s ready resolves

A route handler receives { method, path, params, query, body, headers } and returns { status?, body?, headers? }. Await registration.ready before publishing the URL or completing activation. Scheduled agent work is covered in Scheduled work.

All three are gated on the single process permission — every git operation is a git process spawn, so it grants nothing process does not already imply.

Member In types Description
process.spawn(command, args, options?): PluginChildProcess Yes options: cwd, env, stdio
process.killAll(): void Yes Kills every child this extension started
pty.spawn(options): PtyHandle Yes options: command, args?, cwd, env?, cols?, rows? (defaults 80×24)
pty.get(id): PtyHandle | undefined Yes undefined once the session has exited
pty.list(): PtyHandle[] Yes Live sessions this extension owns
git.isRepo(cwd): Promise<boolean> Yes Whether cwd is inside a repository
git.getRepoRoot(cwd): Promise<string> Yes Repository root for cwd
git.worktree.list(cwd): Promise<GitWorktree[]> Yes Every worktree of the repository
git.worktree.diff(ref, mainRepoCwd?): Promise<GitFileDiff[]> Yes Per-file status and line counts

PluginChildProcess exposes pid, stdin, stdout, stderr, kill(signal?), and on('exit' | 'error', handler).

PtyHandle exposes id, pid, write(data), resize(cols, rows), kill(signal?), and the disposable-returning onData(cb) and onExit(cb). Reach for pty whenever a human or a terminal UI is on the other end; process gives line-buffered pipes with no resize.

GitWorktree is { path, branch, head, isMain, lockReason? }. GitFileDiff is { path, status, oldPath?, additions, deletions, patch?, binary? }, where patch is populated on request rather than by list operations.

Permission: auth.identity. Outbound origins are declared separately as { "auth.outbound": [origin, …] }.

Member In types Description
getSession(): Promise<AuthSession | null> Yes Cached identity; null when signed out. Never round-trips
fetch(url, init?): Promise<AuthFetchResult> Yes Signed call to your own backend, forwarded through the main process
onChange(cb): () => void Yes cb(session, reason), where reason is logged-in, logged-out, token-refreshed, or session-expired

AuthSession is { user: { id, email, name? }, extensionId, audience, childTokenExpiresAt }. Your extension never sees WAMP’s own token; fetch attaches a child token whose audience is ext:<extensionId> and returns { status, data, headers }. It throws when the URL’s origin is not in the auth.outbound allowlist (loopback origins are auto-allowed in a dev build) or when nobody is signed in. See Sign in with WAMP.

Member Permission In types Description
tokens.onUsage(handler): () => void none Yes handler({ input_tokens, output_tokens, model?, conversationId? })
disposables.add(d): void none Yes Accepts a Disposable or a plain function
disposables.disposed: boolean none Yes Readonly; true after the host tears the extension down

Use ToolContext.workspacePath for tools and ExtensionIpcCallEvent.workspacePath for UI requests. Each identifies the project that invoked the operation.

These depend on services supplied by the host rather than permissions. Desktop supplies both; a headless core without those host services supplies neither. Guard their presence instead of inferring it from local versus remote mode.

Member In types Description
sessions.getActive(): ActiveSession | null Yes The session on screen; null on a fresh launch
sessions.getKnownIds(): readonly string[] | null Yes Every session that exists. null means the catalog has not loaded — which is not “no sessions”
sessions.onDidChange(cb): Disposable Yes Fires when the active session or the known-id set changes
clientAffordances.register(config): Disposable Yes Claims a wamp.client.<namespace>.* namespace

ActiveSession is { id, name, context }, where context is absent until the user picks a project. There is deliberately no session-deleted event: deletes have an undo window and can happen while the app is closed, so reconcile against getKnownIds() instead of listening.

ClientAffordanceConfig is { namespace, methods, rateLimit: { burst, perMinute }, singleFlight?, handle(submethod, params) }. Registering a namespace another extension already claimed throws.

pluginAPI is a closed interface: every member is required, so a typo is a compile error rather than a runtime undefined. Sensitive calls with declared permission boundaries are re-checked outside the window. pluginAPI has no MCP namespace; MCP server configuration lives in Settings. Skill package mutations share the host’s trusted renderer realm; they are not a per-extension authorization boundary. The engine still requires a validated preview plus its exact digest and applies the same trust, containment, and atomic-install policy to every caller.

pluginAPI.notify, ai, agents, skills, tasks

Section titled “pluginAPI.notify, ai, agents, skills, tasks”
Member In types Description
notify(message, type?): void Yes In-app toast; type is info, success, or error
notify.success(message), .error(message), .info(message) Yes The same, spelled per level
ai.complete(input, options?): Promise<string> Yes One-shot completion
ai.stream(input, callbacks, options?): () => void Yes Streams, and returns a cancel function
ai.generateObject<T>(input, schema, options?): Promise<T> Yes Schema-constrained completion
ai.listModels(): Promise<ListModelsResult> Yes { models, aliases } — the surface a model picker needs
agents.delegate(agentType, task, options?): Promise<DelegationResult> Yes options adds outputSchema; the result adds object and schemaError
agents.list(): Promise<string[]> Yes Agent ids available for delegation
agents.listEntries(): Promise<AgentEntry[]> Yes Full catalog with scope, triggers, tools, and skills
agents.save(payload): Promise<{ success, error? }> Yes Creates or overwrites a user- or workspace-scope agent
agents.delete(agentId): Promise<{ success, error? }> Yes Deletes a user- or workspace-scope agent
agents.listLoadErrors(): Promise<AgentLoadErrorEntry[]> Yes Agent files that currently fail to parse
agents.listScheduled(): Promise<ScheduledAgentEntry[]> Yes Cron-scheduled agents with next fire time
agents.onRegistryChanged(cb): () => void Yes Fires on any registry change
skills.list(): Promise<SkillEntry[]> Yes Every installed skill
skills.getDetails(qualifiedId): Promise<SkillDetails | null> Yes Exact manifest, body, and resource metadata, loaded on demand
skills.listLoadErrors(): Promise<SkillLoadError[]> Yes Invalid packages excluded from the usable catalog
skills.prepareInstall(registryId, skillId): Promise<PrepareInstallResult> Yes Read-only validation and full package preview with exact digest
skills.install(registryId, skillId, expectedDigest): Promise<SkillInstallResult> Yes Installs only the exact reviewed digest
skills.uninstall(registryId, skillId): Promise<SkillInstallResult> Yes Removes a registry-installed skill
skills.checkUpdates(skills): Promise<…> Yes Compares installed hashes against the registries
skills.listRegistries(): Promise<SkillRegistryEntry[]> Yes Configured registries
skills.setRegistryEnabled(id, enabled): Promise<void> Yes Persists the effective registry layer
skills.searchRegistries(query, maxResults?): Promise<SkillSearchResult> Yes { hits, errors }
skills.onChanged(callback): () => void Yes Install, uninstall, extension, and watched-file changes
tasks.enqueue(input): Promise<{ runId }> Yes runId is null when the requested agentId is not registered
tasks.list(runId): Promise<PluginTask[]> Yes Tasks for a run, in creation order
tasks.get(runId, id): Promise<PluginTask | null> Yes One task
tasks.subscribe(runId, callbacks): () => void Yes onCreated, onUpdated, onDeleted. Call the returned function or listeners leak

Neither listModels, skills, nor agents.listEntries has a main-process equivalent, so build pickers in the window. Agents and skills covers delegation and the registries.

Member In types Description
data.defineSchema(schema): DataAPI<S> Yes Not awaited here; awaited in main
storage.get<T>(key): Promise<T | null> Yes Per-extension read. Returns null, where main returns undefined
storage.set(key, value): Promise<void> Yes Write

The window’s storage has no delete or keys — do those from the main half.

Member In types Description
fs.read(path): Promise<string> Yes Named readFile in main
fs.write(path, content): Promise<void> Yes Named writeFile in main
fs.listDir(path): Promise<DirEntry[]> Yes { name, path, isDirectory } per entry
fs.exists(path): Promise<boolean> Yes Existence check
fs.stat(path): Promise<FileStat | null> Yes { type, size, mtimeMs, ctimeMs }, or null when absent or outside the workspace
fs.mkdir(path): Promise<void> Yes Create
fs.delete(path): Promise<void> Yes Named remove in main
fs.move(sourcePath, destPath): Promise<void> Yes No main-process equivalent
dialog.openFile(options?): Promise<string[]> Yes options: filters, multiple
dialog.openFolder(): Promise<string | null> Yes No main-process equivalent
dialog.saveFile(options?): Promise<string | null> Yes options: title, defaultPath, filters
http.fetch(url, options?): Promise<FetchResult> Yes Routed through the main process, so CORS does not apply. Returns { status, data, headers }

The window’s fs goes through the host’s file service and its path rules; the main-process fs does not. It also has no path helpers — join, dirname, basename, extname, and getDataPath exist only on ctx.api.fs.

Every pluginAPI namespace is present. An operation the host cannot serve rejects with an error whose code is host_unsupported; navigation.go and navigation.back throw it synchronously. Unavailable subscriptions return a no-op unsubscribe. A failed fs.exists or storage.get call rejects instead of reporting false or null.

Capability Desktop workspace Desktop Cloud-session realm Cloud web
notify, workspace.path, navigation params, ui.exitAppMode Available Available; workspace.path is null Available
navigation.go, navigation.back Available host_unsupported Available
fs.read, tools.call, ipc.invoke / ipc.subscribe Available Available Available
Other fs operations, workspace.pluginsPath, ui.showExtension Available host_unsupported host_unsupported
shell.openExternal Available Available host_unsupported
ai, agents, skills, auth.getSession / auth.fetch, http, storage, dialog Available host_unsupported host_unsupported
auth.resourceFetch Available when the viewer has a resource binding host_unsupported host_unsupported

pluginAPI.ui, navigation, shell, workspace, tools, events, auth

Section titled “pluginAPI.ui, navigation, shell, workspace, tools, events, auth”
Member In types Description
ui.showExtension(pluginId): Promise<void> Yes Surfaces another extension’s primary view
ui.exitAppMode(): void Yes Host-specific: closes a Cloud panel; does not close or navigate a Desktop app window
navigation.go(pageId, params?): void Yes Unknown ids no-op with a console warning
navigation.back(): void Yes Desktop restores the previous page and its params; Cloud closes the panel
navigation.params(): Record<string, unknown> Yes Params of the current page or panel
shell.openExternal(url): Promise<void> Yes Opens in the OS browser; a rejected scheme throws
workspace.path(): string | null Yes Workspace root, or null
workspace.pluginsPath(): Promise<string> Yes Where extensions are installed
tools.call(toolName, input?): Promise<unknown> Yes Invokes one of this extension’s own registered tools
events.on(event, callback): () => void Yes event is file.changed or ai.toolUse
auth.getSession(): Promise<AuthSession | null> Yes Same shape as the main-process member
auth.fetch(url, init?): Promise<AuthFetchResult> Yes, Desktop only For portable extensions, call the server half over pluginAPI.ipc and use ctx.api.auth.fetch there
auth.resourceFetch({ audience, path }, { signal }?): Promise<ResourceFetchResponse> Yes GET of a connected resource as the viewer, sent by WAMP Desktop; { status, headers, body }, never the token. Requires the audience in auth.resource; other hosts reject with ResourceFetchError host_unsupported. See Read your service from extension UI
auth.onChange(cb): () => void Yes Session lifecycle events

Never call window.alert, prompt, or confirm — they are blocked. Use <Dialog> from @wamp/ui; see Interface kit.

Member In types Description
ipc.invoke<T>(channel, ...args): Promise<T> Yes Request to your Node half; works over Desktop IPC or the Cloud engine connection
ipc.subscribe(channel, handler): () => void Yes Listen to a broadcast; call the returned function on cleanup

Declare the channel prefix in contributes.ipcNamespaces. A host without an IPC binding rejects invocation explicitly. Subscribe before requesting initial state when events can arrive during that request.

A UI bundle may export activate(host: ExtensionUIHost) to register renderer-side contributions. Import ExtensionUIHost and its dock, chat and tool types from @wamp/extension-sdk. This is how a bundle reaches tool-card rendering, chat blocks, new-chat destinations and dock tabs.

Member In types Description
host.ownerId: string Yes The extension’s own id
host.surface: 'workspace' | 'cloud-session' Yes Which shell mounted the host
host.tools.registerCardRenderer(toolName, component) Yes Draws the chat card for one tool’s results with your own React component. Keyed by the tool’s name
host.chat.registerBlockKind(kind) Yes Registers a chat-input block kind with a chip and a serializer
host.chat.insertBlock(block) Yes Inserts a block into the active chat draft
host.chat.setSessionDestinations(viewId, provider) Yes Publishes a creator for the active project’s new-chat destination picker
host.chat.clearSessionDestinations(viewId) Yes Removes that view’s destination creator
host.dock.setTabs(viewId, provider) Yes Publishes a session.dock view’s tabs into the dock’s tab row
host.dock.clearTabs(viewId): void Yes Removes the provider’s published tabs
host.dock.present(request) Yes Presents a resource through a view that advertises and validates it
host.dock.listViews() Yes Lists registered views and the resource kinds they accept
host.dock.canPresent(sessionId) Yes Whether the host can present a resource for that session

registerCardRenderer and registerBlockKind return disposables. The tab and session-destination setters and clearers return void. The host drops all of these registrations on unload, including after an activation throw. Return { dispose() } from activate for any other extension-owned resources; the same cleanup runs on a Cloud session switch.

Every host passes ownerId, surface and the whole of dock. Desktop’s workspace adds chat and tools; the Cloud web host and the desktop’s own Cloud session realm pass neither, and activate lazily when a panel or its workbench resources are requested. Guard host-specific namespaces before use — a host publishes a capability group whole or not at all, so host.chat && … is the check, and activate runs bare, where a missing member is a TypeError that costs the extension its panel.

surface is how a view avoids drawing a second copy of something the shell already draws: a cloud-session shell reviews that session’s changes and pull requests itself, so Git Workbench publishes only its history graph there. The Cloud host reports an activation throw but still attempts to mount the view; do not mistake a visible panel for successful provider registration.