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 consent signals 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.

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 five 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.storage: PluginStorage Yes The same object as ctx.api.storage
ctx.log Yes info / warn / error / debug, each (message, ...args)
ctx.subscriptions: { dispose }[] Yes Array of disposables the host tears down on deactivation

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
createSession(opts): AISession Yes Multi-turn session owned by this extension
chat(input, options?): Promise<string> No Behaves exactly like complete
chatWithUsage(input, options?) No chat plus { text, usage }
completeWithUsage(input, options?) No complete plus { text, usage }
abort(requestId): void No Cancels an in-flight stream

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) No The id abort takes

Cancelling a main-process stream therefore needs two undeclared members. In the window, pluginAPI.ai.stream returns a cancel function instead, which is declared.

CreateAISessionOptionsagent is the only required field.

Field Description
agent: string Agent definition id; sets the base prompt and tool set
conversationId?: string Stable id. Generated when omitted
systemPromptExtension?: string Appended to the agent’s base system prompt
tools?: { include?, exclude? } Name filters. Exact match, except a trailing * which matches a prefix
model?: string Overrides model resolution
maxTurns?: number Per-send turn cap
maxTokens?: number Output token cap per turn
persistence?: AISessionHistoryPersistor load(), replace(history), optional appendHint(msg)
hooks?: AISessionHooks beforeCall, onUsage, afterTurn

The window’s pluginAPI.ai.createSession accepts the same object minus persistence and hooks.

Member In types Description
id, extensionId Yes Readonly strings
isBusy, isDestroyed Yes Readonly booleans
events.on(event, handler): () => void Yes event is chunk, tool_use, turn_end, or error
send(input, opts?): Promise<AISessionTurnResult> Yes Runs a turn. Throws when another send is in flight
appendContext(text, opts?): Promise<AISessionMessage> Yes Adds a model-visible transcript entry with no model call
cancel(): void Yes Aborts the in-flight send; no-op otherwise
fork(newId, persistor?): AISession Yes Independent session with a copy of the history
destroy(): Promise<void> Yes Idempotent teardown
history(): AISessionMessage[] Yes Fresh copy of the current history

send accepts a string or { content: AISessionContentBlock[] }, plus { hidden } in opts for grounding the model sees but the UI does not. AISessionTurnResult carries text, toolsUsed, usage, stopReason, turns, and history. Full treatment in AI sessions.

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, display, and policy. execute returns a string or an array of content blocks ({ type: 'text', text } / { type: 'image', data, mediaType, dims? }).

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. The local typed store is ungated — the database permission withholds cloud, the store whose records leave the machine, not this one.

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, take, skip
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: database.

cloud.mine is private to the signed-in end user and follows them across devices. cloud.shared is one copy that every signed-in user of the app can read and write. Both expose the same four operations.

Member In types Description
get<T>(collection, key): Promise<T | null> Yes null when the key does not exist
set<T>(collection, key, value): Promise<void> Yes Create or overwrite; undefined is not storable
delete(collection, key): Promise<void> Yes Idempotent
list<T>(collection): Promise<CloudRecord<T>[]> Yes The whole collection; the host follows the server’s cursor to the end

Collection and key names match [A-Za-z0-9._-]{1,128}. Values are anything JSON.stringify accepts, up to 256 KB serialized. The namespace also requires a product with an app backend configured; in a build without one, every call throws a message saying so. The window’s pluginAPI.cloud calls the same service, and the main process re-checks the database permission on each operation — a modified bundle must not reach the app’s backend without it. Backend setup is documented at docs.cloud.vampikez.fun.

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.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.register(cmd, handler): Disposable Yes Older two-argument shape; still accepted
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 window.electronAPI.extensionIPC.invoke from your window half
ipc.broadcast(channel, payload): void Yes Pushes to every window; throws synchronously if the channel’s prefix is not a declared ipcNamespaces entry
ipc.subscribe(channel, handler): Disposable Yes Listens on an extension-owned channel from the main half
ipc.send(channel, ...args): void Yes Legacy un-namespaced send; prefer broadcast
events.on(event, handler): () => void Yes In-process subscribe on plugin:<id>:<event>
events.emit(event, ...args): void Yes Emits in-process and fans out to every window

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?, retainContextWhenHidden?, partition?, surface?, webRequestRewrite?

WebviewHandle:

Member In types Description
id Yes Readonly string
webview.postMessage(message): void Yes Send into the page
webview.onDidReceiveMessage(handler): Disposable Yes Receive from the page
webview.asWebviewUri(localPath): string Yes Returns wamp-webview://<extensionId>/<path>
webview.cspSource Yes Readonly per-webview CSP source
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
cron.schedule(id, interval, handler): CronHandle cron Yes Repeating job; the handle is a disposable that removes the entry
cron.runOnce(id, handler): Promise<void> cron Yes Runs once, tracked under id
cron.cancel(id): void cron Yes Removes an entry by id
cron.list(): CronJobInfo[] cron Yes { id, interval, lastRun, nextRun, status, lastError? }
http.route(method, path, handler): () => void http-routes Yes method is GET, POST, PUT, DELETE, or PATCH
http.baseUrl(): string http-routes Yes The base URL to hand to whoever calls your route

A route handler receives { method, path, params, query, body, headers } and returns { status?, body?, headers? }. Both namespaces are 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.create(cwd, opts): Promise<GitWorktree> Yes opts: branch, baseRef?, path?
git.worktree.remove(cwd, ref, opts?): Promise<void> Yes opts: deleteBranch?, force?
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? })
workspace.getWorkspacePath(): string | null none Yes The open workspace root, or null
workspace.getOpenFiles(): string[] none Yes Declared, but the current host always returns an empty array
workspace.openFile(path): Promise<void> none Yes Declared, but the current host resolves without opening anything
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

The canonical path for workspace access is ctx.api.services.require('workspace'); ctx.api.workspace is a convenience pass-through over that service.

These two are gated on the host rather than on a permission. Both are absent on a headless core, and clientAffordances is also absent in pure-local 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. It is ungated apart from two namespaces whose calls are re-checked in the main process — cloud needs database, and mcp needs mcp. Declaring them in the manifest is not optional; the window bundle is a convenience layer, and main decides.

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.createSession(opts): Promise<AISession> Yes Awaited here; synchronous in main
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 and webhook agents with next fire time
agents.onRegistryChanged(cb): () => void Yes Fires on any registry change
skills.list(): Promise<SkillEntry[]> Yes Every installed skill
skills.install(registryId, skillId, options?): Promise<SkillInstallResult> Yes options.allowUntrusted for an untrusted registry
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.searchRegistries(query, maxResults?): Promise<SkillSearchResult> Yes { hits, errors }
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.approveDrafts(runId): Promise<PluginTask[]> Yes Promotes drafts to pending and spawns a worker run
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
cloud.mine, cloud.shared Yes The same four operations as ctx.api.cloud; needs database
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
terminal.exec(command, args, opts?): Promise<ExecResult> Yes An argv array, not a shell string. Returns { stdout, stderr, code }
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.

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

Section titled “pluginAPI.ui, navigation, shell, workspace, tools, events, mcp, auth”
Member In types Description
ui.showExtension(pluginId): Promise<void> Yes Surfaces another extension’s primary view
ui.exitAppMode(): void Yes Leaves a presentation: 'app' page for the previous docked view
navigation.go(pageId, params?): void Yes Unknown ids no-op with a console warning
navigation.back(): void Yes Pops the navigation history
navigation.params(): Record<string, unknown> Yes Params from the most recent go
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:tool-use
mcp.listServers(): Promise<McpServerInfo[]> Yes { name, displayName, status, toolCount }. Needs mcp
mcp.setEnabled(name, enabled): Promise<McpSetEnabledResult> Yes App-wide start/stop. Needs mcp
auth.getSession(): Promise<AuthSession | null> Yes Same shape as the main-process member
auth.fetch(url, init?): Promise<AuthFetchResult> Yes Requires the target origin in auth.outbound
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.

A UI bundle may export activate(host) to register renderer-side contributions. None of this surface is in the published types, so an extension that uses it declares the shape itself. It is the only way to reach tool-card rendering, chat blocks, and dock tabs.

Member In types Description
host.ownerId: string No The extension’s own id
host.tools.registerCardRenderer(displayKind, component) No Renders tool-use cards for a display.kind with your own React component
host.chat.registerBlockKind(kind) No Registers a chat-input block kind with a chip and a serializer
host.chat.insertBlock(block) No Inserts a block into the active chat draft
host.dock.setTabs(viewId, provider) No Publishes a session.dock view’s tabs into the dock’s tab row
host.dock.clearTabs(viewId) No Removes them; an empty list removes the view from the strip

Each registration returns a disposable, and activate may return an object with dispose() that the host calls on unload.

The bridge between an extension’s two halves, and the one capability reached through a global rather than an import. It is declared in the SDK’s ambient globals; every other member of window.electronAPI is deliberately not, and stays a type error.

Member In types Description
invoke<TResult>(channel, ...args): Promise<TResult> Yes Calls a handler your main half registered with ipc.handle. Rejects when none is registered, which the host reports as an extension fault
subscribe<TPayload>(channel, handler): { dispose() } Yes Listens for ipc.broadcast from the main half

The channel prefix before : must be a namespace listed in the manifest’s contributes.ipcNamespaces. There is no renderer-side broadcast.