A Convax Plugin package is offline content and is inert during validation and
packing. A Plugin with a Web surface is served through a private protocol and
mounted in an iframe with exactly sandbox="allow-scripts". It has an opaque
origin: it cannot inspect the parent DOM, use browser storage as shared application
state, or access Node/Electron. The one exception at runtime is an explicitly
declared and authorized OpenCode Hook module described below.
convax.plugin/2 through /6 may instead be headless Tool Plugins. Local
executable contributions name a separately distributed bare mcp-stdio command;
their ZIP still contains no executable code. v4 and later may own Skills, v5 adds
transport-neutral Project/Canvas grants and LLM display metadata, and v6 may expose
one HTTPS remote Agent MCP server without a local runtime. Concrete Plugin, Skill,
and reviewed companion source belongs in this repository; the Convax repository
supplies only the generic host ABI and lifecycle.
package/manifest.json uses convax.plugin/1 through /6. Only
documented fields are accepted. Source metadata must use the matching pair:
convax.plugin/1withconvax.plugin-host/1;convax.plugin/2withconvax.plugin-host/2;convax.plugin/3withconvax.plugin-host/3;convax.plugin/4withconvax.plugin-host/4;convax.plugin/5withconvax.plugin-capability/1;convax.plugin/6withconvax.plugin-capability/1.
There is no convax.plugin-host/5 or /6. The capability protocol is versioned
independently from the manifest schema so later schemas can evolve without another
major-specific host bridge.
The v1 schema is static-only:
{
"schema": "convax.plugin/1",
"id": "my-plugin",
"name": "My Plugin",
"description": "A focused Canvas surface.",
"version": "0.1.0",
"entry": "index.html",
"capabilities": ["canvas.node.read"],
"contributes": {
"canvas": {
"renderer": { "create": true, "width": 640, "height": 400 },
"toolbar": [{ "id": "refresh", "title": "Refresh", "command": "refresh" }]
}
}
}Renderer matching may use create, extensions, mimeTypes, or nodeKinds.
Package paths are POSIX-relative and case-sensitive. The optional skill points to
a companion SKILL.md inside the same Plugin ZIP; installing it remains an explicit,
independent user action. This legacy field is available only through v3. Do not use
it for a Skill whose lifecycle belongs to its Plugin.
convax.plugin/5 adds a sandboxed Pet feature contribution. A package contributes
one Pet feature Plugin and
owns its static overlay, settings, packaged collection, animation rules, and
selection. Its convax.plugin-capability/1 compatibility label describes manifest
support; the surfaces use the separate convax.pet-host/1 protocol:
The contributes.pet object declares the library and both static feature surfaces:
{
"schema": "convax.plugin/5",
"id": "convax-pet",
"name": "Convax Pet",
"description": "A local desktop companion and pet library for Convax activity.",
"version": "0.2.1",
"capabilities": [
"pet.activity.read",
"pet.activity.open",
"pet.preferences.write",
"pet.custom.manage"
],
"contributes": {
"pet": {
"library": "pet-library.json",
"overlay": "pet/index.html",
"settings": "settings/index.html",
"protocol": "convax.pet-host/1"
}
}
}The convax.pet-library/1 document contains one to 64 unique pet entries. Each
entry supplies id, displayName, description, package-relative spritesheet,
spriteVersion: 2, and alt. Every atlas is a 1536×1872 PNG or WebP containing
eight 192×208 cells across and nine state rows in this order: idle,
running-right, running-left, waving, jumping, failed, waiting,
running, and review.
The exact pet.custom.manage grant exposes only the scoped
collection.get/collection.import/collection.delete host methods. Import uses
the native Convax file picker; a Pet surface never receives a filesystem path.
Convax accepts one current-format transparent 1536×1872 PNG or WebP atlas, stores a
managed copy, and serves it through convax-pet-asset:. Legacy Goku folders,
pet.json, remote assets, and arbitrary file reads are not supported.
Pet Plugins that do not offer custom collection management omit this optional
grant while retaining the three required activity and preference capabilities.
The settings and overlay pages run with no Node, Electron, remote network, native
path, or arbitrary IPC access. Their surface-scoped convax.pet-host/1 ports expose
only content-free activity, validated navigation, overlay movement, preferences,
and wake/tuck lifecycle. Installation never wakes the pet automatically. New pets
ship as library entries in a new version of the same feature Plugin.
hooks names one self-contained .js or .mjs OpenCode Plugin module:
{
"schema": "convax.plugin/2",
"id": "agent-observer",
"name": "Agent Observer",
"description": "Observes Agent session lifecycle events.",
"version": "1.0.0",
"hooks": "hooks/index.mjs",
"capabilities": [],
"contributes": {}
}export const AgentObserver = async ({ client }) => ({
event: async ({ event }) => {
if (event.type !== "session.idle") return;
await client.app.log({
body: {
service: "agent-observer",
level: "info",
message: "Agent session became idle",
},
});
},
});This is OpenCode's native Plugin function and native Hook object. Convax does not
define another event enum or dispatch engine. The module may use the Hook events
supported by the OpenCode version bundled with the host. Callable product tools
still belong in standard MCP contributions; hooks is for lifecycle interception
and observation.
The module is executable Agent code with the OpenCode Plugin context, including filesystem, network, SDK, and Bun capabilities. It is not an iframe and is not sandboxed. Therefore an explicit Plugin install or update is execution consent for the normalized manifest and exact Hook bytes. Convax copies those bytes to a private immutable snapshot and OpenCode never imports the mutable Plugin package path. Missing or changed bytes disable that Plugin's Hook and require reinstall. Default/background provisioning cannot silently authorize a new Hook or changed Hook bytes.
The first version deliberately authorizes one file only. It must be valid JavaScript
ESM and export at least one OpenCode Plugin entry. Bundle all runtime code into that
file. Only static node: and bun: built-in imports may remain; package, relative,
absolute, CommonJS globals, runtime module loaders such as node:module, and dynamic
imports are rejected. Do not ship a dependency tree, dynamically download code, or
rely on project-local OpenCode configuration. A Hook-only Plugin uses
convax.plugin/2 or later; v1 still requires its ordinary static Canvas surface.
Hook modules are loaded in stable Plugin-id order after host-configured OpenCode
Plugins and before Convax's protected-path guard, so the guard sees the final tool
arguments.
convax.plugin/4 introduced explicit
Plugin-owned Skill contributions. The owner must still provide a real Plugin
capability—such as a sandboxed Canvas renderer, an executable generation/service
runtime, a v5 Project/Canvas grant, or a v6 remote Agent MCP endpoint—beyond merely
wrapping a Skill. v4, v5, and v6 all use the same contributes.skills ownership
contract:
{
"schema": "convax.plugin/4",
"id": "creative-tools",
"name": "Creative Tools",
"description": "Provides local creative operations and their Agent workflow.",
"version": "1.0.0",
"contributes": {
"generation": {
"models": [],
"tools": [
{
"id": "media.inspect",
"title": "Inspect media",
"description": "Inspect one staged media input.",
"output": "text",
"acceptedInputs": ["reference_video"]
}
]
},
"skills": [
{
"name": "creative-tools-workflow",
"path": "skills/creative-tools-workflow"
}
]
},
"runtime": { "type": "mcp-stdio", "command": "creative-tools-mcp" }
}Each name is a portable Skill id and each path is exactly skills/<name>.
The Skill is authored once under packages/skills/<name>/package/; do not copy it
into the Plugin source. The Skill source metadata declares ownerPluginId, and the
packer injects its files into the Plugin ZIP after validating both declarations.
Any owned Skill byte change also changes the Plugin ZIP, so publish a new owner
Plugin version together with the new Skill version; release coverage rejects stale
same-version owner bytes.
Convax may list the Skill with a “Provided by” relationship, but users install,
update, and remove it only through the owning Plugin. Its standard standalone ZIP
remains usable by Codex and other Agent Skills clients. A normal standalone Skill
that merely benefits from an optional Plugin must omit ownerPluginId and provide
an honest missing-tool fallback instead.
convax.plugin/5 replaces the numbered Plugin Host compatibility pair with
convax.plugin-capability/1. A headless v5 Plugin may request one or more
Project/Canvas grants (projects.read, canvas.catalog.read,
canvas.document.read, canvas.document.write, or canvas.events.subscribe)
without inventing a Web surface or local runtime. The native host routes those
calls through the same scoped Project and Canvas services used by the product.
v5 also permits generic contributes.llm display metadata for a verified local
runtime. Provider credentials, base URLs, and routing do not belong in the
manifest. v6 inherits the v5 capability contract and all v4 owned-Skill behavior.
convax.plugin/6 may contribute one standards-based remote MCP server directly to
the Agent:
{
"schema": "convax.plugin/6",
"id": "remote-editor",
"name": "Remote Editor",
"description": "Connects the Agent to the Remote Editor service.",
"version": "1.0.0",
"contributes": {
"agent": {
"mcp": {
"type": "remote",
"url": "https://editor.example.com/mcp",
"oauth": "auto",
"headers": { "X-Client": "convax" }
}
}
}
}The URL must be absolute HTTPS with no embedded credentials or fragment. oauth
is auto (the default) or none. headers is optional and contains at most 16
static literal, non-secret values; Authorization, Cookie, and
Proxy-Authorization are forbidden, as are environment/file placeholders.
Convax delegates this declaration to OpenCode/the native MCP host, which owns the
remote connection and standard OAuth flow. The service keeps its own account and
authentication system. Do not add a Convax-specific adapter, runtime, local
command, executable fallback, credential, or token to make the remote MCP work. A
runtime may coexist only when it backs a separate local contribution. This
agent.mcp contribution is itself a real Plugin capability and may coexist with
v4+ owned Skills and v5 Project/Canvas grants.
A headless v3 through v6 local executable package declares runtime together with
contributes.generation, contributes.service, and/or the v5+ contributes.llm.
It does not need an entry, fake HTML, Canvas renderer, provider connection
details, or credential fields. The generation execution catalog and model catalog
are deliberately separate:
{
"schema": "convax.plugin/3",
"id": "creative-tools",
"name": "Creative Tools",
"description": "Generates Canvas media through an external MCP tool.",
"version": "1.0.0",
"contributes": {
"generation": {
"models": [{ "tool": "image.generate", "name": "Imagine Pro" }],
"tools": [
{
"id": "image.generate",
"title": "Generate image",
"description": "Generate an image from a prompt and optional visual references.",
"output": "image",
"acceptedInputs": ["reference_image"]
}
]
}
},
"runtime": {
"type": "mcp-stdio",
"command": "creative-tools-mcp"
}
}generation.tools is the complete executable MCP tool contract.
generation.models is required in v3 and is the only source for the model picker;
it contains {tool,name} references to generation tools and may be [] for an
operation-only Plugin such as FFmpeg. Model names and referenced tools are unique.
This positive declaration prevents utilities from appearing as generation models.
Outputs are text, image, video, or audio. acceptedInputs may contain only
reference_image, reference_video, first_frame, last_frame, audio, and
text. It describes optional Canvas references; the prompt is always a separate
argument, so a prompt-only tool declares []. Tool ids are unique within the
Plugin, and execution callers see <plugin-id>/<tool-id>.
v3 and later local generation declarations may expose selected non-model tools to the Agent with
contributes.agent.tools. Each item has a stable Agent id matching
^[a-z][a-z0-9_]{0,63}$ and a tool reference. At most 32 are allowed; ids and
tool references are unique, and model tools cannot also be Agent tools. Hosts
derive the public name generically from the Plugin and Agent ids, for example
plugin_ffmpeg_tools_run_video. MCP clients may add their server namespace, such
as convax_plugin_ffmpeg_tools_run_video. A Plugin id never creates a host special
case.
v6 operations may set "delivery": "return" when output is text. The host
then reuses the normal verified companion, bounded input staging, stale-source
checks, cancellation, and at-most-once execution path, but returns one bounded
text result to the Agent instead of creating a Canvas node. Return-delivery tools
cannot be models or Canvas selection actions.
An Agent operation that represents a Canvas sink may additionally declare
"inputBinding": "direct-incoming". Its Agent tool requires an ownerNodeId;
the host verifies that this is a Canvas node owned by the same installed Plugin
and that every supplied reference is still connected directly into it, both
before staging and immediately before execution. Such a tool must accept at least
one reference role and cannot be a model. This is a generic graph constraint, not
a Plugin-id special case.
Video-node actions are declared under contributes.canvas.selectionActions.
Each action supplies localized title and description, target: "video", one
of the fixed editors (time-point, time-range, crop-region, or
confirmation), and up to 16 ordered {tool} steps. Interactive editors require
exactly one step. Every step must reference a non-model tool whose
acceptedInputs includes reference_video. A confirmation action may declare
multiple steps, which supports paired outputs such as video-only plus audio-only.
canvas.renderer is optional; toolbar contributions remain renderer-only.
runtime.command is a portable bare executable name, never a path. Optional args
are bounded static tokens without whitespace, shell syntax, native paths, or
traversal. Keep reviewed sidecar source under packages/tools/<id> when it belongs in this
repository, distribute it separately, and keep the executable and dependency tree
out of package/. A first-party package declares its reviewed source, companion
version, and platform targets in the adjacent convax-package.json; publishing
turns those build paths into immutable Registry URLs with byte size and SHA-256.
Convax installs only an exact platform/architecture target and fingerprints it.
Explicitly installing or updating the Tool Plugin is consent to run that exact
manifest/executable binding; normal generation and service calls do not add a
first-call or per-billable-call command prompt. Missing or changed bytes fail
closed and require reinstall. The Plugin manifest never contains build paths,
vendor credentials, or a fallback download URL, and the user does not need to copy
the executable into PATH.
A v2 through v6 Web surface that calls installed generation tools requests
generation.execute and uses an ordinary entry plus Canvas contribution. It may
omit runtime and contributes.generation. Declaring a runtime does not grant the
Web surface caller authority, and granting generation.execute does not let the
iframe start processes or send arbitrary MCP requests.
A v2 through v6 executable Plugin may expose bounded account/service state through the same verified sidecar process used by generation. The manifest declares only which fixed host actions are meaningful; it cannot choose MCP method names or attach an action payload:
{
"schema": "convax.plugin/3",
"id": "account-tools",
"name": "Account Tools",
"description": "Shows bounded account status from an external tool.",
"version": "1.0.0",
"contributes": {
"service": { "actions": ["sign_out"] }
},
"runtime": {
"type": "mcp-stdio",
"command": "account-tools-mcp"
}
}actions is a unique subset of authorize, reauthorize,
authorization.cancel, and sign_out; an empty array declares status-only UI.
The sidecar must always expose service.status, plus the corresponding fixed MCP
tool for every declared action (service.authorize, service.reauthorize,
service.authorization.cancel, or service.sign_out). Every tool accepts exactly
an empty object.
Successful service tools return structuredContent with exactly the
convax.plugin-service-status/1 display contract: schema, state, credential,
account, credits, and usage. Do not return credentials, URLs, native paths,
cookies, arbitrary diagnostics, or provider configuration. Unsupported account,
credit, or usage APIs must be represented as { "availability": "unavailable" },
not guessed values. Declaring an action does not grant browser, Cookie, or generic
network access; the separately reviewed sidecar remains responsible for its own
documented API boundary.
If a reviewed sidecar must retain a higher-privilege first-party Web session for
live service metadata, say so in the installed Plugin description. Store it
separately from generation credentials in atomic Plugin-private storage, bind it
to the matching authorization generation, never return it through MCP, and clear
it on sign-out. Mode 0600 is best-effort isolation from other OS users; it does
not protect against processes already running as the same OS account.
convax.plugin/5 may contribute one OpenAI-compatible provider through the same
verified sidecar lifecycle. The manifest contains display and selection metadata
only:
{
"contributes": {
"llm": {
"provider": { "id": "example-llm", "name": "Example LLM" },
"models": [{ "id": "example-main", "name": "Example Main" }]
}
}
}The sidecar must expose the fixed, empty-input MCP tool llm.gateway.start. Its
Main-only structuredContent is exactly {schema, base_url, api_key} with schema
convax.llm-gateway/1, an ephemeral http://127.0.0.1:<port>/v1 URL, and a random
process-lifetime key. The gateway accepts only authenticated OpenAI-compatible
requests for declared models. It owns upstream URLs, headers, credentials, Cookies,
streaming, cancellation, and vendor error adaptation; none of those values belongs
in the manifest, renderer, service status, or durable OpenCode config.
Hosts namespace provider ids by Plugin identity, verify the installed executable before starting it, and discard the gateway when that exact Plugin runtime changes. An unavailable or invalid gateway is omitted rather than weakening loopback or executable verification.
The v5 compatibility pair deliberately uses the independently versioned
convax.plugin-capability/1 broker. It does not extend the legacy iframe
convax.plugin-host/N sequence.
Convax transfers one fresh MessagePort to each mounted Plugin node using the
versioned convax.plugin-host/1 through /4 protocols. Accept it only from
window.parent, for the host protocol matching the manifest major, the exact
Plugin id, and only once. The transport-neutral v5 compatibility label does not by
itself grant this Canvas port. A Pet feature provider instead receives a separate
convax.pet-host/1 port only on its declared overlay and settings surfaces:
const PROTOCOL = "convax.plugin-host/1";
window.addEventListener("message", function connect(event) {
const message = event.data;
if (
event.source !== window.parent ||
message?.protocol !== PROTOCOL ||
message?.type !== "connect" ||
message?.pluginId !== "my-plugin" ||
event.ports.length !== 1
)
return;
window.removeEventListener("message", connect);
const port = event.ports[0];
port.start();
});Requests and responses use the transferred port, never global postMessage:
{
"protocol": "convax.plugin-host/1",
"type": "request",
"id": "1",
"method": "host.context.get"
}Responses repeat protocol, type: "response", and id, with either
{"ok":true,"result":...} or {"ok":false,"error":"..."}. Toolbar commands
arrive as {"protocol":"convax.plugin-host/1","type":"command","command":"refresh"}.
v5 and v6 use convax.plugin-capability/1, not a synthesized
convax.plugin-host/5 or /6. Use only the typed capability client supplied by a
compatible host; do not recreate that transport or forward arbitrary methods.
| Method | Manifest capability | Scope |
|---|---|---|
host.context.get |
none | current Project, Canvas, and owning node |
canvas.connectedInputs.list |
canvas.connectedInputs.read |
pathless metadata for direct incoming media |
canvas.node.get |
canvas.node.read |
owning node only |
canvas.node.updateState |
canvas.node.write |
Plugin-namespaced node state |
canvas.connectedImages.list |
canvas.connectedImages.read |
directly connected managed Canvas image nodes |
canvas.connectedImages.read |
canvas.connectedImages.read |
bounded bytes for one directly connected managed image |
canvas.image.create |
canvas.image.write |
one bounded PNG imported as a managed adjacent Canvas image |
project.file.readText |
project.files.read |
current Project-relative text file |
agent.prompt |
agent.prompt |
current Project and owning node resource |
generation.tools.list |
generation.execute |
installed generation contracts in the current scope |
generation.canvas.execute |
generation.execute |
shared scoped Canvas generation operation |
Request the smallest set. Arguments cannot select another Project, Canvas, or node.
Treat results as untrusted structured data, bound message sizes, handle errors, and
render a useful disconnected state. A successful domain mutation may be followed by
a failed optional view effect; do not report that as a reverted mutation.
canvas.image.create accepts a bounded PNG data URL and a portable display name;
the host owns asset admission, node placement, the connection from the Plugin node,
persistence, and rollback if the Canvas commit fails.
The canonical production example is
panorama-viewer; its complete Web surface
and manifest live in this repository, while Convax Desktop owns only these generic
host operations.
canvas.connectedInputs.list returns only bounded node id, kind, label/name, MIME,
status, and basic media metadata in direct-edge order. It never returns bytes,
native paths, managed Project paths, or account credentials. The corresponding
canvas.connectedInputs.changed command means the pending list is stale; it does
not authorize upload or any other external side effect. A Web surface should
refresh its list and wait for an explicit user action.
No remote scripts/assets, iframe network APIs, popups, downloads, eval-generated
code, native/WASM executables, packaged Node servers, filesystem paths, secrets,
telemetry, service workers, or generic method forwarding. Do not edit .convax
files. A v2 through v6 external runtime is a separately installed and authorized
tool, never a Plugin ZIP asset. A v6 remote Agent MCP URL is host-consumed metadata,
not iframe network permission. Use host capabilities only.