Skip to content

Plugin authoring reference

A Wuu plugin can combine an agent runtime, desktop UI, skills, hooks, MCP servers, commands, themes, and settings in one local package. Use the agent quickstart or desktop quickstart for a complete example. This page describes the package and runtime contracts those examples use.

The package root contains plugin.json. Declare only the entry points and contributions you need:

{
"schema_version": 1,
"id": "example-plugin",
"name": "Example Plugin",
"version": "0.1.0",
"runtime": {
"protocol": "wuu-plugin-v1",
"command": "node",
"args": ["dist/runtime.js"]
},
"desktop": { "entry": "dist/renderer.js" }
}

This example needs both declared files. Omit runtime for a desktop-only or declarative package; omit desktop if no renderer code is needed. A desktop entry is a package-relative, self-contained ESM module. Runtime dependencies must also be available after packaging, which excludes node_modules; bundle JavaScript dependencies into the runtime entry where practical.

FieldContract
schema_versionPackage schema, currently 1
idStable package identity; keep it unchanged across updates
versionPlugin’s version, separate from Wuu’s product version
minimum_wuu_versionMinimum Wuu CalVer, such as 2026.1108.0; not a substitute for service/API compatibility
platformsOptional supported host platforms
requiresLegacy required package IDs, without version constraints
dependenciesPackage relationships as { id, version?, optional? }; required dependencies must be available and match the version constraint
breaksHard incompatibilities; an active incompatible pair rejects the activation plan
conflictsSoft conflicts reported without automatically disabling either package
skills, hooks, mcpServers / mcp_serversContributions using the corresponding skill, hook, and MCP contracts
contributesCommands, themes, settings, UI declarations, and view entry points

Packages delivered as one product can declare the same interface.family:

{
"interface": {
"family": {
"id": "example.automation",
"name": "Automations",
"description": "Schedule prompts and manage recurring work."
}
}
}

Use a stable, publisher-namespaced id and the same name and optional description in each member. The catalog and enabled sidebar show one family entry, including when only one member is installed. Search also matches member names and descriptions. Open the family to see component availability, failures, permissions and settings; direct links to individual packages still work. Incomplete family metadata (empty ID or name) leaves the package ungrouped.

Families are presentation metadata, available to bundled and community packages alike. They do not install missing members, establish dependencies, grant trust, or enable, disable or update packages together. Each component retains its own source, lifecycle and settings. Older hosts ignore this optional metadata; no data migration is needed.

dependencies adds version checks without changing the package schema. For example:

{
"dependencies": [
{ "id": "text-analysis-example", "version": "^1.0.0" },
{ "id": "draft-export", "version": ">=1.2.0 <2.0.0", "optional": true }
]
}

Omit version to accept any package version. Constraints use SemVer syntax, including exact versions, comparisons, ^, and ~; a prerelease must be explicitly admitted by the constraint. These are plugin package versions, not Wuu’s product CalVer. requires remains supported and combines with required dependencies. Do not declare the same dependency as both optional and required.

A missing, disabled, untrusted, or version-incompatible required dependency leaves its consumer and downstream required consumers inactive. Optional dependencies do not block activation when unavailable or incompatible. Available matching dependencies establish provider-before-consumer ordering; required dependency cycles reject the activation plan. An optional ordering edge that would create a cycle is skipped with a diagnostic. Relationships never download, install, enable, or grant trust to another package. breaks and conflicts remain ID lists. requested_permissions describes requested authority; it does not sandbox the plugin process.

Package dependencies and runtime service requirements serve different purposes. A package dependency selects an installed package and its lifecycle order. provided_services / required_services negotiate a named API and service major version; they do not select by package ID or inherit that package’s version. Declare both when a feature needs a specific package and its API. A service-only consumer can instead accept any eligible provider of that service.

A new plugin can consume an existing service and contribute UI to its owner’s published slots without modifying that owner or selecting a replacement. For example, the counter and controls packages combine an external process state/action API, a typed public helper, cancellable subscriptions, and a nested desktop contribution. This is one testable example, not a limit on the combinations of services, hooks, capabilities and UI a plugin may use.

Declare desktop.required_services before using api.invokeService; runtime consumers declare required_services during initialization. The host routes calls with the consumer’s identity and captured generation. Publish an owned registerUISlot and let other packages add registerUIContribution entries. Use the public UI kit and contract context rather than private DOM or controller access. Registrations and invocation cancellation belong to the generation; stop domain subscriptions when their view unmounts as well.

A missing required dependency rejects interactive installation. An update that would break an active required consumer is rejected before publication, keeping the last working graph. Optional dependencies do not guarantee availability: handle call failure and disable only the affected feature. A package version constraint and a service major version are separate checks. Installing an addon is not an implicit request to replace an existing provider; conflicting providers still need an explicit composition choice.

A plugin can declare servers in mcp_servers using the same fields as user configuration. A declarative-only plugin needs no runtime or desktop entry. The canonical name is plugin.<plugin-id>.<server>; OAuth credentials use that name. Plugin-owned definitions are managed by the package: users connect, sign in, and disconnect them, but cannot edit or remove them as standalone definitions. Disabling the plugin stops its servers; no fallback is substituted.

Plugin-owned servers stay in Coding. A shared Personal host receives only the owner’s global standalone servers.

Use a real provider endpoint. A URL is not proof that the provider supports dynamic client registration; servers that require a fixed OAuth client need oauth.client_id.

The default composer discovers MCP connections through listMCPServers and runtime tool plugins through catalog conversation_tools. A theme or service-only package does not need a second opt-out. Use canonical provenance.plugin_id, not the catalog record’s subject ID, when constructing a runtime plugin reference.

plugin_reference message parts carry readable text and a plugin object (ApplicationPluginReferenceV1). Runtime references use plugin_id; MCP references also retain mcp_server, resource_url, and opaque authorization_id. Preserve the complete part across draft restoration, queueing, editing, and submission. These identifiers are selections, not credentials or execution grants. The host resolves tool names and revalidates the server and authorization at admission and turn start. An OAuth refresh retains the authorization ID; a new login replaces it. MCP clients bind to their accepted grant and fail if a replacement login supersedes it.

An engine’s conversation-plugins capability promises that its turn consumes the host’s selected plugin tools and instructions while retaining ordinary permission checks. External engines receive the explicitly selected tools through the existing turn-scoped MCP bridge. Unsupported adapters must omit this capability; the host rejects referenced submissions instead of silently dropping their intent. New references during an active turn are queued, because an existing external bridge cannot acquire a new connection midway through execution.

Themes, shell skins, and settings need no desktop module. For example, merge this fragment into a manifest:

{
"contributes": {
"themes": [{
"id": "calm-night",
"name": "Calm Night",
"base": "dark",
"tokens": {
"--wuu-color-canvas": "#151820",
"--wuu-color-accent": "#8fa7ff"
}
}],
"settings": {
"enabled": {
"type": "boolean",
"title": "Enable feature",
"default": true,
"scope": "workspace",
"apply": "live"
}
}
}
}

Theme tokens come from the generated theme contract, not arbitrary host CSS variables. Public semantic colors have readable defaults for independent UI shells. Declare colors in the theme’s tokens to override built-in surfaces; omitted colors retain each component’s default appearance. Clearing or disabling the theme restores those defaults. Settings support boolean, string, number, and enum, with scope=user|workspace and apply=live|next-operation|restart. Read settings through host.settings.get/list services in the runtime or host.getSetting in a desktop view. Disabling or removing the package preserves settings and storage by default.

Commands under contributes.commands have a local ID and a kind. prompt_template references a bounded UTF-8 file inside the package and supplies a host-owned composer action. runtime_action points to a command registered by the desktop generation; declaring it alone does not execute code. side_conversation references a package-owned prompt and opens the host’s attached conversation surface. Its messages use the existing side-chat execution and history; the host retains its current permission boundary. The send request identifies the public plugin.command ID, and disabling or removing its owning package cancels active answers without deleting history. Same-ID updates apply to the next answer; an active answer keeps its retained generation. No desktop entry or helper process is required.

First-run setup preserves applied plugin choices if it is interrupted before completion. Inventory distinguishes the effective enabled state from an optional enabled_override user policy; an absent override means the package default applies.

Manifest UI declarations use contributes.presenters, navigation, workspaceTools, or settingsPages. They describe placement and discovery, while the desktop module registers rendering. For declared UI regions, call registerUIContribution with an owner-qualified target; no manifest entry is needed. The former contributes.slots and surfaces fields are rejected. See desktop UI plugins for composition rules.

Configuration changes do not restart the plugin or rebuild its resources. Choose an application mode for each non-secret setting:

applyRuntime reads
liveEach read or refresh sees the latest validated candidate.
next-operationThe first snapshot or non-secret settings read in an authenticated tool, capability, or service execution captures all operation settings; subsequent reads in that execution retain them. A new execution captures newer values.
restartValues are captured before generation preparation and remain fixed for that generation, including retained in-flight work. A successful reload or host restart adopts saved changes.

Use restart for resource structure, such as pool construction or a connection endpoint, and next-operation for operation parameters. Use the callback’s host for execution-scoped reads; a background host has no enclosing operation. For an application-defined operation, capture once and pass that immutable snapshot to all its steps, even when its settings are marked live:

import { createPluginSettings, requireKernelService } from "@wuu/plugin-sdk";
// initialize(): required_services: [requireKernelService("host.settings.list")]
// Inside executeTool, invokeService, or invokeCapability:
const settings = createPluginSettings(host, { signal: execution.signal });
const config = await settings.capture();
// Keep config.values for the whole operation. Later refreshes never mutate it.

host.settings.list still accepts {} and returns entries. It additionally returns an opaque revision, configured scope_revisions, and local restart_required keys. Optional {scope: "user" | "workspace"} filters the returned snapshot and its metadata. Revisions are content tokens: compare for equality, not ordering; they are not an event log. One provider document is read per scope, and all declared values are validated before publication. Scopes are independently committed; a combined snapshot is not a cross-scope transaction. Keep coupled values in one scope. Secret entries remain empty strings and never contribute secret material or secret-presence changes to revisions.

The reader’s refresh() atomically replaces current only after a successful read and optional validate(values) cross-field check. Invalid persisted values, backend failures, or cancellation reject the refresh without altering previously captured snapshots. subscribe(callback) observes changed published revisions; its return value unsubscribes. watch(intervalMs) optionally refreshes every 1 second by default (100–60,000 ms allowed), coalescing with concurrent refreshes. It must be awaited or have a rejection handler. Watching is not started by subscription; it can coalesce intermediate changes and is not a durable event stream. Pass the generation lifecycle signal for background watching. Aborting it removes listeners and prevents late responses from publishing. Listener failures reject refresh after publication and do not roll back settings.

The existing get/getAll interface and host.settings.get/list call shapes remain available. The new helper requires snapshot metadata and explicitly rejects older hosts rather than pretending separate reads are consistent. Desktop host.getSetting is a configuration-editor read of the saved value, not the runtime-effective restart value. Secret reads still reveal no token.

The fingerprint-gated plugin/setting/set editor route accepts an optional patch of additional non-secret keys in the primary key’s scope, alongside the existing key and value. It validates the complete declared candidate and commits it in one selected-provider compare-and-exchange. Duplicate keys, foreign keys, cross-scope edits and secrets in a patch are rejected without a write. This also permits atomic repair of multiple invalid saved fields after a schema change. Removed declarations remain stored for downgrade compatibility but are not exposed to the current plugin. Uncertain provider write failures are not retried automatically; reread before deciding whether to retry.

Run node scripts/plugin-settings-e2e.mjs for a packed-SDK process acceptance check covering in-flight reads, reload, resource stability, rejected candidates, and retirement. It uses synthetic state and writes repeatable evidence under .tmp/e2e/plugin-settings/ (override with WUU_SETTINGS_EVIDENCE).

Declare contributes.skins independently of contributes.themes. Each skin has a plugin-local id, name, an optional placement, an optional userImage, and required light and dark variants. placement is "frame" (default) or "canvas": frame placement decorates the window frame and ribbon only, while canvas placement also paints the artwork behind the main pane — conversations sit under a translucent veil in every state, opaque destinations cover it, and only a plugin view that declares "transparent" exposes the artwork directly. A variant supplies opaque #RRGGBB background and foreground colors, an optional package-relative image, and optional overlay, hover, and active colors (#RRGGBB or #RRGGBBAA). position is center (default), top, bottom, left, or right. Two optional variant fields shape canvas rendering and require "placement": "canvas": veil, a number from 0 to 1 (default 0.86) setting how strongly the theme canvas covers the artwork for translucent destinations, and dots, an object { "pitch": p, "size": s, "shape": "round" | "square" } that renders the artwork as a screen of cells. The host samples one color per cell on whole device pixels; pitch is the cell spacing in CSS pixels (2–32), size the dot diameter or square side (0.5 up to pitch), and shape defaults to "round". Set "userImage": true to let the user’s Background image replace the variant images; the host prepares the picture for the skin’s placement, and the variant image remains the artwork when no picture is saved. placement, userImage, veil, and dots require a host at your minimumWuuVersion; older hosts reject unknown skin fields at install.

{
"contributes": {
"skins": [{
"id": "ocean",
"name": "Ocean",
"placement": "canvas",
"userImage": true,
"light": { "background": "#234b60", "foreground": "#f3fafc", "image": "ocean.svg", "veil": 0.86, "dots": { "pitch": 4, "size": 2.4 } },
"dark": { "background": "#112e41", "foreground": "#e3f1f6", "image": "ocean.svg", "overlay": "#06172555", "veil": 0.82, "dots": { "pitch": 4, "size": 3, "shape": "square" } }
}]
}
}

Images may be PNG, JPEG, WebP, or self-contained SVG, up to 8 MiB each. Use an image for gradients or textures. Image paths remain inside the package; SVG external references and scripts are rejected. The desktop serves only declared images from the requested package fingerprint. With frame placement, artwork covers the full window continuously behind the ribbon and frame; with canvas placement, the frame and ribbon keep the solid variant background and the window-aligned artwork paints only behind the main pane. The host owns layout, cropping, interaction and content boundaries; skin colors do not override content theme tokens — keep theme canvas and surface tokens opaque, because the host derives the text tiers from them and applies translucency itself through the veil and the host glass material. Pair foreground, hover and active colors with artwork that keeps controls readable.

Skin selection is local to the desktop profile, follows light/dark mode, and survives plugin updates. Disabled, removed, or safe-mode plugins stop decorating the frame; re-enabling the same plugin restores the selection. A failed image load uses the variant background and reports the missing artwork in its preview. One package may offer both skins and content themes, and users select them independently. See the installable shell skin example in examples/plugins/shell-skins.

Secret settings and authenticated requests

Section titled “Secret settings and authenticated requests”

Declare a secret setting with default: "", scope: "user", apply: "live", and credential_origins: ["https://api.example.com"]. Each origin must be an exact HTTPS origin without a path, query, fragment or user information. The host-generated settings control accepts masked input and provides explicit Save and Remove actions. Values go to macOS Keychain or Linux Secret Service, with no plaintext-file fallback. Unsupported or locked stores fail visibly. Secret setting reads return an empty string; only the host settings UI receives a configured presence flag. They never enter ordinary settings or tool history.

A runtime declares requireKernelService("host.credentials.request") and calls host.call("host.credentials.request", { credential: "token", url, method, headers, body }, { signal }). The key names its own declared secret setting. The host adds Bearer authentication only for a declared origin, blocks redirects and caller-supplied authentication, cookie, routing or connection headers, and returns { status, headers, body }. Response header names are lower case; only content type, retry timing and request IDs are exposed. Requests time out after 20 seconds and request/response bodies are limited to 4 MiB. Tokens are redacted from response bodies, headers and transport errors. Never accept credentials as tool arguments or copy them to ordinary storage, logs or generated bundles.

The credential is bound to the Wuu home, plugin source identity, local key and origin set; project plugins also bind the project root. Changing any of these requires a fresh credential. Disabling a plugin prevents new calls but preserves its credential until the user removes it. A saved credential does not prove API access. Token refresh and service-specific authorization remain plugin concerns. A failed or timed-out write may have applied remotely: do not retry it silently.

The host starts the manifest’s command and exchanges one JSON object per line over stdin/stdout. Reserve stdout for protocol frames and stderr for diagnostics. The manifest transport name wuu-plugin-v1 and the negotiated capability protocol number are different fields; new service-using runtimes should negotiate protocol_version: 3.

The official plugin authoring SDK is TypeScript: @wuu/plugin-sdk. Implement RuntimePlugin and pass it to runJSONLRuntime; the SDK handles framing, request IDs, cancellation signals, and the lifecycle-v2 handshake. A process written in another language can implement the same JSONL contract, but Wuu does not provide a separate authoring SDK for it.

Return protocol_version: 3 from initialize when adding capabilities, including observers. This is the result field: the host’s params.protocol_version: 1 identifies the transport and must not be echoed as the capability version; params.capability_protocol_version advertises the host’s capability support. Without services, an omitted result version means legacy tool-only v1, not the latest version. The SDK rejects capabilities or model adapters under v1 with invalid_capability_protocol before returning their declarations. Explicit v2 capabilities remain supported; service declarations retain the SDK’s v3 selection.

CallbackResponsibility
initialize(params, host, lifecycle)Return tools, capabilities, provided services, and required services
prepare(host, lifecycle)Read and validate state after service bindings resolve; do not write or start background work
activate(host, lifecycle)Start timers, subscriptions, or other behavior after the runtime change commits
executeTool(params, host, execution)Run a registered tool and return its result
invokeCapability(params, host, execution)Handle a declared capability
invokeService(params, host, execution)Serve one host-routed service call
serviceChanged(params, host)Respond to a service resolution change
shutdown()Stop plugin-owned work and release resources

initialize only declares contracts. Lifecycle v2 requires a separate prepare request after all declarations are bound. Move persisted-state reads and validation from initialize into prepare, rebuild with the matching SDK, and return lifecycle_version: 2 in hand-written adapters. An adapter using an older lifecycle is rejected before commit. Providers prepare before consumers; only declared methods marked preparation: true may be called during preparation, including nested calls. Marking a method promises no durable writes or external effects. Omitted flags mean active-only methods. Defer writes and background work to activate, and make shutdown safe after partial initialization or partial activation. Preparation and persistence failures preserve the old runtime without calling candidate activate. Once the change commits, an activation failure leaves a degraded new generation with failure diagnostics; it does not restore the old generation or undo external effects. Avoid duplicate effects when explicitly retrying activation. Trusted initialization code and MCP preparation may still have external effects outside the host service boundary. See runtime generations for the commit boundary.

Preparation can validate persisted state, but the old generation can still change it before activation. Do not use that preparation snapshot as the basis of activation writes: reread and validate the current state, or verify its version under the provider’s actual handoff contract. Discarding a candidate must not write persisted state or cancel the live generation’s work. This does not require every backend to use compare-and-swap, and rereading alone does not fence concurrent writers.

lifecycle.signal aborts when shutdown is requested or the transport closes. Use it for activation-owned background work and stop timers in shutdown. Cleanup may still use the unscoped lifecycle host to persist state or release resources; do not make cleanup depend on an already-aborted signal.

Process shutdown has a bounded acknowledgement window. If it expires, the host terminates the process and reports cleanup as unconfirmed, not successful. Physical termination does not confirm pending external writes or undo their effects; check their state before retrying. A runtime that exits normally without replying may still complete shutdown successfully.

Register model tools in the initialization result’s tools array. Each tool needs an id, description, and object input_schema. The host creates a namespaced public name. execution_scopes can limit availability to root or child; activity describes read-only behavior, concurrency safety, risk, and whether the tool orchestrates child tools. Declare real effects rather than marking a writer read-only to bypass scheduling or permission checks. These declarations are trusted-extension contracts, not a sandbox. An optional guidance string is the tool’s agent-facing usage contract: when the tool is exposed to an external engine through a host-capability surface, the host shows the collected guidance alongside its own binding instructions.

The TypeScript runtime adapter honors activity.concurrency_safe: true for tool dispatch. Adjacent explicitly safe tools may overlap, with at most eight tool handlers running per runtime. Omitted or false values, unknown tool IDs, and ordinary capability/service callbacks remain FIFO exclusive barriers. read_only alone does not enable concurrency. These barriers apply across both shared and distinct execution IDs. Initialize, prepare, activate, and shutdown also wait for previously admitted reentrant callbacks before proceeding.

Protect shared mutable state before opting in. Keep each invocation’s execution context and scoped host with that invocation; do not store them in a shared current-call variable. Cancellation is registered while a frame is read and checked again before its handler starts, including after waiting for a tool slot. Canceling one execution aborts all its live callbacks without canceling independent executions. Already running code must cooperate with cancellation. Explicitly reentrant capabilities and services retain their separate lane so nested host callbacks can finish even when tool slots are full. The limit does not bound those callbacks or queued frames, and TypeScript overlap does not move synchronous CPU work to worker threads.

Ordinary tools are PTC bindings by default when the session enables PTC. Set direct_only: true for user interaction, explicit delivery, or lifecycle controls that must remain model-direct. Such tools stay in the top-level provider surface and reject nested invocation. The optional flag does not grant permissions or change execution scopes; undeclared tools retain ordinary routing. Keep this host metadata out of input schemas.

executeTool receives arguments and execution context such as cwd, call ID, and available session/turn identifiers. Validate arguments even when a schema is present. Return { result: { content: [...] } }, setting is_error: true for a tool failure. Content can contain text and supported rich result parts. Use importArtifact for a file that should become a thread-owned artifact rather than returning a temporary path that may disappear.

Capabilities are declared separately, with an ID, kind, and version. Current supported capability IDs are:

CapabilityKind and purpose
agent.system_prompt.sectiontransform: contribute a prompt section
agent.request.transformtransform: prepend system messages to the model request
agent.pre_steptransform: append identified messages before a step
agent.step.policydecision: continue or stop default-loop model admission; see harness experiments
agent.turn.completed, agent.turn.lifecycle, agent.turn.interruptedobserve: react to supported turn events
plugin.client.requestdecision: dispatch requests from the plugin’s desktop UI
agent.compactiondecision: experimental compaction contract

agent.request.transform receives a provider-neutral request view; its public output is prepend_system_messages, not arbitrary mutation of a provider’s wire request. The SDK marks compaction experimental, so do not treat it as a stable general replacement for the whole agent loop.

Capabilities can declare priority, depends_on, and conflicts. Higher priority runs first; equal priorities sort by plugin ID. error_policy is propagate, isolate, or ignore: observers cannot propagate, and only observers may ignore. The built-in turn observers default to isolation; other capabilities default to propagation. Handle expected failures inside the plugin rather than relying on one blanket error policy.

The current host exposes host.service.call as the runtime gateway. Declare each consumed service in required_services with its name and major version. Old direct frames such as host.storage.get are not the wire contract. The TypeScript SDK translates typed conveniences such as host.call("host.storage.get", params) to that gateway. Declare requirements only through required_services; helpers such as requireKernelService construct these versioned entries. There is no second routing path in the host.

For example, this initialization result requests storage reads:

{
"protocol_version": 3,
"required_services": [
{ "name": "host.storage.get", "major_version": 1, "required": true }
]
}

Call it through the gateway after activation:

import type { RuntimeHost } from "@wuu/plugin-sdk";
async function readCounter(host: RuntimeHost): Promise<string | null> {
const result = await host.call("host.service.call", {
service: "host.storage.get",
method: "call",
params: { scope: "workspace", key: "counter" },
}) as { value: string | null };
return result.value;
}

requireKernelService builds the corresponding version-1 requirement. host.supports("host.service.call") checks the gateway, not whether a particular service provider exists. Optional services still need a declaration and error handling when unavailable.

Service familyUse
host.storage.get/set/delete/keysString values in the plugin’s user or workspace namespace
host.storage.compare-exchangeConditional replacement using { scope, key, expected, value }; returns { swapped, value }
host.file-lockAcquire and release a process-safe lock under a host-provided data root

| host.model.request | One bounded, text-only model request using host configuration | | host.model.invoke | Independent full logical model calls and streams | | host.model.emit | Events from a streaming adapter or middleware | | host.background | Generation-owned calls to a plugin’s own service | | host.client.invalidate | Content-free hints to refresh plugin-owned client state | | host.process | Conversation-bound shell processes, output and event delivery | | host.settings.get/list | Declared setting values | | host.credentials.request | Origin-bound HTTPS requests using a host-held Bearer credential | | host.session.create/send/list/inspect/cancel/control | Create and manage sessions through host lifecycle rules | | host.session.model | Read or change a plugin-owned user-space session’s model from a live owner UI | | host.session.history.read/search | Bounded history access with sequence snapshots and continuation cursors | | host.workspace.status/apply/discard | Inspect or resolve work from an owned session workspace | | execution.update | Progress for a live execution | | execution.invoke-tool | Invoke a child tool from an orchestrator execution | | host.artifact.import | Import a file or bytes as a thread-owned artifact | | host.user-question.ask | Ask a structured question during an execution |

Most kernel services use method call. Storage is not a transaction across keys: use compare-exchange for concurrent updates to one value, check swapped, and bound retries. A separate read followed by an unconditional set can lose updates. Missing values are null; store structured values as strings, for example JSON. The result describes the completed comparison: swapped: false returns the current value, while swapped: true returns the replacement value (null after deletion).

For session creation, provide a stable request_id, visibility=user|plugin, and context_source=fresh|fork|seed. Sending also needs a stable request ID and input.prompt. Authenticated client input can include content_parts containing text, pasted text, and plugin references. Forward these unchanged in input.content_parts; the host rejects substitutions or omissions and includes the selection in the durable request fingerprint. Plugin references retain resource and authorization identity and are revalidated before execution, including external engines with conversation-plugin support. Admission or queueing is not completion: inspect the turn or handle lifecycle events before consuming its result. Keep model prompts, business state, and retry policy in the plugin; use the host for execution, history, workspace changes, and recovery.

host.session.model reads the current provider and model for a session owned by the calling plugin. To change it, supply both provider and model. The call requires a live owner UI in the same user space, checks session ownership, and rejects a change while the session is busy. An accepted selection is persisted to that session; other sessions and global defaults are unchanged. Previously admitted delegated work retains its own authorization and selection. This service does not configure credentials or select an external engine.

Owned host.session.list results and host.session.inspect for a session owned by the calling plugin include creation_request_id when available. It is the original host.session.create request ID, including before the first turn starts, so a plugin can discover an accepted creation whose response was lost. Shared lists and inspection of another owner’s session omit it. This is correlation metadata, not authority to control the session.

For host.session.send, session_send_unavailable means the host had no send handler and did not dispatch this call. After the service recovers, an explicit retry may release that unaccepted request identity. This code is reserved for that boundary: if an invoked handler returns it, including from a nested call, the outer send reports host_error instead. Generic service_unavailable, other errors, and timeouts do not prove non-admission; retain the request identity and reconcile its receipt before replacing it.

host.session.send.input.attachment_handoff is an additive v1 envelope for agent-supplied bytes: {version:1, session_id, request_id, source:{session_id,seq}, attachments:[{sha256,filename,media_type,data}]}. data is standard base64; at most 16 nonempty files and 20 MiB decoded bytes are accepted. Destination and request IDs must match the send. It cannot combine with human input proofs, receipt resume, ordinary attachment references, or steering. Normal send grants, private-session ownership and receiver policy still apply. Source is asserted provenance from the authenticated sender, not proof of human authorship or a capability to read another session. Producers must use authorized source reads. The receiver verifies digests and safe filenames before writing target working copies and retains plugin-origin/read-only message provenance. Check the source host feature session-attachment-handoff.v1; older receivers reject the new field. Route receipts include the full envelope in their intent digest, so changed bytes cannot replay under the same operation ID.

host.session.attachment.snapshot accepts the same source reference as attachment read (without offset or snapshot ID) and returns {snapshot_id, attachment}. It uses authorized visible history, freezes verified bytes in the selected state.documents provider, and publishes a manifest only after all 512 KiB base64 chunks commit. Repeated calls reuse the same source/plugin/user-space key; a partial commit or lost reply can be retried without replacing existing chunks. Pass snapshot_id and the original source reference to attachment read to recover 128 KiB ranges without rereading the original message. Every range rechecks current session visibility and the complete binding and verifies the stored chunk digest. The snapshot service is a mutation and is unavailable during preparation. Check session-attachment-snapshot.v1 before creating snapshots.

Snapshot chunks are separate from plugin settings’ 1 MiB value budget. Payload storage is approximately 4/3 of raw bytes plus a small manifest, with no payload copies in task revisions or route outboxes. Snapshots, including incomplete chunks, follow workspace-state retention; removal of source access prevents further reads. The final send remains a bounded in-memory envelope: 20 MiB decoded is about 26.7 MiB base64 within the 64 MiB RPC frame limit. It is not an unbounded network stream; serialization and target normalization also allocate proportional to file size. Larger requests fail explicitly. Older receivers reject unsupported fields or their smaller size limit before admission.

host.session.send accepts optional disable_tools: true for a tool-free turn. This removes every model-visible tool and rejects tool execution, including code-mode dispatch; it never changes later turns or grants permissions. Omitted or false preserves normal behavior. Restricted turns cannot use if_running: "steer" or external agent engines: unsupported requests fail before admission. tool_policy: { allow?: string[], deny?: string[] } adds an attenuation-only policy for one isolated turn. Omitted allow retains the ordinary surface minus deny; explicit allow: [] denies every tool. The host intersects this policy with existing session restrictions, checks actual and discovered tool execution, and restores the ordinary surface after success, failure or interruption. It never changes persistent session settings. Check the session-turn-tool-policy.v1 host feature before relying on this contract.

A turn carrying tool_policy is a separate execution boundary: it cannot steer another turn, and incoming plugin or completion input is queued separately. Human turn/steer receives isolated_turn_requires_queue only when the input has not been admitted; the Desktop queues the same original input and client ID on the same captured connection for that exact refusal. Other errors and uncertain responses are not retried this way. Already-held input must use its existing resume flow. Owner interruption remains available. When allowing run_code, its embedded catalog and search/describe text can still describe tools excluded by the turn policy; actual nested calls remain checked. Use a direct-tool-only allowlist for a review that must not advertise unrelated bindings. Tool names restrict the surface, not task authority: the receiving plugin must still validate the exact task, run, revision and tool arguments.

Plugins requiring agent.tools@1 can use toolService(host).resolveOwn({ tool_id: "local_tool_id" }) to resolve their own current public tool name without guessing prefixes or collision hashes. This read-only lookup is usable from a background callback and does not initialize a tool backend. The native provider uses the registry-authenticated caller and its captured generation’s declaration; callers cannot select another plugin. The result is {found:false} or {found:true,name,direct_only?}. Re-resolve before each use. This is declaration identity, not permission or proof that a target session exposes the tool: check the exact admitted model-request surface separately. Missing/unsupported selected providers must keep the operation read-only; never substitute the native provider. Execution methods retain their existing stricter scope requirements.

host.session.history.read is a read-only view of physical audit records, including hidden model context and tool records. Records carry optional hidden, read_only, and origin metadata; omitted flags are false and omitted origin is empty for legacy records. A user role alone does not establish human authorship. Retain these fields when building a conversation projection: exclude hidden records, and treat user-role records as human input only when they are not read-only and their origin is user or empty. Do not classify messages by their wording or strip matching text from genuine input. Projection does not delete audit records or alter provider context.

History records may include display_id, the host-generated turn_id:item_id of the original streamed bubble. Preserve it when projecting history so a client can restore the same display position before a provider ID or history sequence was available. It is a display identity, not a history cursor or execution order; legacy and non-streamed records may omit it.

To reply while that bubble is streaming, send relation.reply_to with its session_id, seq: 0, and display_id. The host validates that the visible assistant message belongs to the target session. The reference remains valid after persistence and restart; use a positive seq for ordinary historical references. Session owners can resolve the current content with host.session.inspect({ session_id, message_display_id }), whose optional message has the history record shape and sequence zero before persistence. Unknown display IDs return no message. This lookup does not finish or interrupt the running turn.

host.session.history.search with view: "conversation" applies those visibility and authorship rules to user/assistant records before pagination. It searches displayed conversation text rather than reasoning or tool output. Omitting view retains the default audit/trace search. Sequence addresses remain physical record addresses, so clients can reopen an original record without treating filtered results as renumbered history.

host.workspace.status and apply compare tracked files against the commit frozen at workspace creation, including committed, staged, and unstaged changes. apply writes the cumulative patch into the parent working tree without committing it, then removes the isolated workspace and rebinds the session. Conflicts, untracked files, or an unavailable baseline fail without discarding the workspace or its binding. Automatic cleanup also preserves committed work and workspaces whose baseline cannot be verified.

A completed turn may have an empty final_output: normal provider completion does not require an outward reply or an acknowledgement tool. It records the end of execution, not proof that the user’s objective was fulfilled. Consumers must inspect results and evidence; transport failures and abnormal stops still fail.

The Peers plugin uses if_running: "steer" to deliver terminal replies into active work when possible. Replies arriving too late for that turn are retained for a follow-up; idle sessions still wake automatically. A running response with steered: true can still be an unconsumed input, not durable delivery. Peers keeps the reply pending until a durable receipt confirms delivery and retries inputs lost on restart with the same request ID.

Subagent and Peers qualify tool-call identities with the host-supplied session, tool, call ID, turn ID and step index when available; transport execution IDs do not define retries. Calls without turn and step metadata retain their legacy mapping. Persisted requests continue recovery using their stored IDs; missing historical turn/step identity is not inferred. Peers’ existing content-duplicate and uncertain-send matching remain separate policies.

host.session.inspect accepts include_skills: true for a session owned by the calling plugin. skills lists the current user-invocable catalog, with name, source, path, description and invocation metadata. This option never grants access to another plugin’s session or the selected Coding workspace. Omitted or empty skills means no available skills. A fresh request refreshes the catalog.

Authenticated host.session.send user input can carry the same explicit /skill identity as the Coding composer. The host validates and expands it before acceptance, freezes the content for queue/recovery, and retains the original draft as display text. Plugin-origin messages do not invoke slash commands.

For a status card or subagent dashboard, declare required_host_features: [SESSION_PROGRESS_HOST_FEATURE] and the ordinary host.session.inspect service requirement. Pass include_progress: true with an explicit request_id. Default inspection does not include content-bearing progress. progress is a version-1 snapshot, not an event stream: replace the displayed snapshot on each read instead of appending it as new messages.

The caller must own the session, and its request must be the turn’s only input identity. A queued request has no turn to project; a steer cannot borrow the original request’s progress. Once different inputs share a turn, neither can claim its output exclusively. In these cases, available: false reports session_not_owned, request_not_exclusive, or turn_unavailable, without content. Shared-session inspection remains available without progress.

Available snapshots identify the session, request, turn, and items. They contain at most 32 recent assistant/tool display items and 32 KiB of JSON. Assistant text is limited to 4096 UTF-8 bytes per item; truncated explicitly reports shortened text or omitted items. Prompts, reasoning, tool arguments/results, attachments, and private provider state are excluded. Empty items is valid before output. Reading progress neither subscribes nor changes the observed work. A finished turn may remain inspectable, but the snapshot is not durable evidence of success, input consumption, or message delivery. Use the existing terminal inspection and lifecycle receipts for settlement; this API provides no replay or lost-event recovery. See the SDK-only session progress example.

Declare { name: "host.file-lock", major_version: 1, required: true } when read-modify-write operations on files must coordinate across plugin processes. The service exposes acquire and release; acquireFileLock and releaseFileLock are the SDK helpers. Acquire takes a relative lock-file path and one root: workspace_state, wuu_home, or project. A successful acquisition returns an opaque lock_id; release returns { released: boolean }. All writers must use the same lock path. The host owns the operating-system lock and releases handles when their plugin generation closes.

Keep the unscoped host passed to initialization for this resource handshake. Pass the current execution ID to acquisition, wait for its authoritative reply, then check cancellation and release in finally:

const { lock_id } = await acquireFileLock(lifecycleHost, {
root: "workspace_state", path: "example/state.json.lock",
}, execution.executionId);
try {
execution.signal.throwIfAborted();
await updateStateFile();
} finally {
await releaseFileLock(lifecycleHost, lock_id);
}

Do not locally abandon an acquisition response: cancellation can race a successful grant, leaving the caller without the handle it must release. The execution ID lets the host cancel a waiting acquisition; generation cleanup remains the fallback for process loss. The lock does not make arbitrary plugin filesystem access a sandbox or create a transaction across different files.

Declare requireModelRequestService() in your initialization result’s required_services, then call requestModel(host, input) from a live host-owned tool, capability (including plugin.client.request), or service callback. Use the handler-scoped RuntimeHost; the TypeScript SDK supplies its execution ID. A generation-owned background job also supplies a live service callback. To outlive an observer or other handler, start such a job and make the request inside its callback. Activation and detached timers or tasks cannot call the model service directly without a live execution scope.

import { requestModel, type RuntimeHost } from "@wuu/plugin-sdk";
async function requestText(host: RuntimeHost, content: string) {
return await requestModel(host, {
messages: [{ role: "user", content }],
max_output_tokens: 256,
timeout_ms: 10_000,
});
}

host.model.request version 1.0.0 uses method call, with schema identifiers host.model.request.input.v1 and host.model.request.output.v1. Service negotiation checks the declared major version; SDK helper availability or host.supports("host.service.call") alone does not prove this service is available on an older host.

InputContract
messages1–32 messages with system, user, or assistant roles; nonblank UTF-8 text, at most 256 KiB of content in total
model_aliasOptional existing agent.model_aliases name; omitted or empty uses the saved effective configuration’s main model
max_output_tokensInteger 1–8192; default 1024
timeout_msInteger 1–45000; default 45000

max_output_tokens is a requested output-token limit, not a billing ceiling. Configured token-limit options cannot raise this call’s limit. If provider preparation removes the limit or raises it above the requested value, the call fails with unsupported_limit before inference submission. This host-side check does not guarantee how a remote server enforces token limits or bills requests.

The timeout is bounded by both timeout_ms and the owning invocation’s remaining lifetime. The runtime’s default capability timeout is 30 seconds, so a request may end before 45 seconds. Set the package’s runtime timeout deliberately rather than assuming the model service extends it. Cancellation, invocation completion, or generation revocation cancels outstanding provider work.

The remote Web client also has an independent 30-second RPC deadline for api.invokeRuntime; increasing the package timeout does not extend it. When that deadline expires, the client requests cancellation of the original host invocation.

The result contains text, provider, model, finish_reason, and truncated. Optional usage contains input, output, cache-creation, and cache-read token counts plus cache_creation_unknown; missing usage does not mean zero cost. Empty/non-text responses and responses requesting tool calls fail instead of starting a tool loop.

The host reuses configured credentials, provider preparation, capacity coordination, retries, and inference accounting. It adds no conversation history, agent-specific request hooks, or tools and creates no persistent session. Select and limit context in the plugin. The service does not accept credentials, endpoint URLs, or arbitrary provider overrides. Explain what draft/history text will reach the configured provider, bound request frequency and output, and treat cancellation as best effort at avoiding further work: a submitted request may already incur charges.

Declare requireClientInvalidationService() to consume host.client.invalidate version 1. After persisting a change, call invalidateClient(host, "reactions"), or use service method call with { topic }. This also fits calendar or other plugin-owned record updates. Topics must match [a-z][a-z0-9.-]{0,63}; arbitrary payloads and caller-supplied identities are rejected.

The host emits plugin/client/invalidated with plugin_id (the package subject ID), fingerprint, user_space_id, product_domain, and topic. Identity comes from the active generation. Disabled, closed, or superseded runtimes cannot publish. Desktop consumers receive the notification through api.onHostEvent, check its package and context, and re-read through their scoped runtime API. The event is only an invalidation hint: it does not send a message, create a turn, write history, or request user attention. Keep polling or another recovery read for missed notifications; a failed hint does not undo the preceding durable write.

Use host.model.invoke version 1 for full logical requests. The earlier host.model.request text-only contract remains unchanged. Declare requireModelInvokeService() in required_services; declarations grant access to the service, while a live tool, capability, service, or background execution supplies the lifetime. Independent inference does not create a conversation turn or execute returned tool calls.

LogicalModelRequestV1 carries version: 1, provider/model identity, ordered messages and tools, cache hints, model options, tool choice, media admission and native replay state. Messages retain reasoning blocks, provider-native items and their fallback history, tool calls/results, discovered schemas, and resolved image/file bytes. Rich tool results retain artifact and deferred-media references and their budgeted model_text. Local working-copy paths, credentials, host retry objects and execution authority are not exported. Preserve unknown provider option keys and opaque native item data when copying a snapshot.

Tool calls may include an optional thought_signature, scoped by provider_item_provider and provider_item_model. Preserve this opaque value verbatim with its call and origin through model, driver, tool-batch, and session round trips. Preserve parallel-call order as well. The host omits foreign-origin signatures when preparing a request for a different provider or model. This is an additive version-1 field; external providers that rebuild tool calls must forward it to support signed Gemini continuations.

These are logical snapshots, not guarantees of identical HTTP bytes or a cache hit. Plugins must invalidate retained snapshots when their context, route, model, adapter, native-state compatibility or media changes. The host remains responsible for configured credentials, provider preparation, cancellation and inference accounting. Native forwarding cannot install arbitrary endpoints or credentials.

SurfaceContract
model.request.transformVersion-1 transform capability. Receives { request, origin }; returns { request? } before provider dispatch.
model.request.observeVersion-1 observe capability. Receives request and terminal response/error phases, with the logical request and host-authenticated origin.
model.request.metricsVersion-1 metadata-only observe capability. Bounded, nonblocking terminal timing and adapter-reported usage; no request or response content.
model.middlewareVersion-1 decision capability with propagate errors. Wraps dispatch, returns a replacement response, or calls its single-use continuation.
model.adapterVersion-1 decision capability with propagate errors. Supplies an explicitly selected adapter implementation.

Plugin-initiated independent calls bypass request hooks and around-call middleware, so observing a normal request and replaying it does not recursively trigger the same policy. origin.kind, plugin_id, operation IDs and available session context are host metadata; supplying copied metadata cannot impersonate an agent request.

Metadata-only model performance observation

Section titled “Metadata-only model performance observation”

Declare MODEL_REQUEST_METRICS_CAPABILITY (model.request.metrics) with kind: "observe", version: 1 and protocol 2 or 3 to receive ModelRequestMetricsInput. This is separate from content-bearing model.request.observe: it sends no prompts, outputs, tool arguments, provider options, or raw errors, and its callbacks cannot delay or change model completion. It supports performance displays and latency diagnostics without retaining a conversation snapshot.

Each terminal record describes one selected-adapter invocation, before outer middleware can rewrite its response. request_id is unique per invocation; origin.operation_id correlates retries, and attempt_id identifies an admitted attempt when available. Session metadata and turn_id come from authenticated host contexts; absent context stays absent. Independent plugin calls are included, except inference descending from a metrics callback, which is excluded to prevent recursive observation. Middleware-only cached responses and rejected model routes that never reach an adapter produce no record. This is not a tool-duration or native external-engine event feed.

  • duration_ms uses the host monotonic clock from adapter admission through the terminal outcome, including capacity wait. started_at is a wall-clock timestamp for display, not duration arithmetic.
  • first_output_ms is the host-observed first nonempty text, reasoning, tool output, or image in a stream. It is a time-to-first-output proxy, not a provider token timestamp. Unary and empty responses omit it.
  • text_delta_events and text_utf8_bytes count nonempty text deltas, not tokens. They exclude reasoning and replacement events; stream resets do not erase already observed traffic.
  • usage is the latest optional adapter assertion, labeled usage_source: "adapter_reported". Native provider adapters forward provider usage; custom adapters can supply their own values. This is not independently verified physical billing evidence. Missing usage means unknown, never zero or an estimate from chunks; cancellation may leave only partial usage. A reported output-token count divided by total duration is a request-wide average, not instantaneous generation speed.
  • status is completed, error, cancelled (including deadline expiry), or incomplete when a stream closes without a terminal event. Each invocation captures at most one terminal record, including early iterator cancellation.

Delivery is best effort and not durable. Each exact plugin registration has one serial callback and a queue of 64 records. Queue overflow drops new records; dropped_before reports overflow since the previous delivered callback. Callback execution has a five-second deadline. A timeout cancels the callback. Timeout or unacknowledged execution cancellation suspends this registration’s metrics delivery until reload, discarding its queue: a cancel signal does not prove a remote handler has stopped. No further metrics callbacks are admitted behind an unacknowledged one; unrelated plugin capabilities remain available. Removal and generation shutdown cancel accepted delivery, discard queued records, and join delivery before the plugin’s ordinary process cleanup; old records never move to a replacement registration. Delivery failures are isolated and appear as fixed, content-free plugin diagnostics (unless error_policy: "ignore"); overflow also leaves a diagnostic. No delivery error becomes a conversation toast or model failure. Do not treat the feed as an exactly-once billing ledger. Keep handlers short and opt into concurrentCapabilities when their own plugin must handle other callbacks while observing.

invokeModel(host, { request, model_alias?, max_output_tokens?, timeout_ms? }) returns { response, origin }. Use an existing configured alias to compare a second model. An explicit alias supplies its configured options and effort defaults. An explicit request effort selects that model’s corresponding variant, and explicit provider_options keys override those defaults. request.adapter takes precedence over the resulting provider_options.adapter. Without an alias, the complete request snapshot is preserved rather than refreshed from current configuration. Output-token limits and timeouts are bounded by the host and owning execution; missing usage does not imply zero cost, and cancellation cannot undo an already submitted request. Full requests preserve their output limit unless max_output_tokens overrides it. The independent-call timeout defaults to 120 seconds and accepts at most 600 seconds. Inputs and retained stream data are bounded to 16 MiB; each stream retains at most 4096 events.

startModelInvocation returns an invocation_id. readModelInvocation accepts that ID plus cursor and optional wait_ms, and returns ordered events, the next cursor, done, and a terminal result or error. Handles belong to the current plugin generation and execution. cancelModelInvocation stops an active stream. streamModel wraps those methods as an async iterator and cancels unfinished work when a consumer breaks or throws. Consume streams within the handler lifetime; use a background job for work that must outlive the originating handler. Preserve opaque provider_state and lifecycle payloads when forwarding events. If reset_partial is true, discard prior partial output before accepting replacement events from a retry. The assistant_item_done event carries a complete assistant message checkpoint for interrupted-turn recovery. Forward it intact; it does not append another visible message.

A middleware receives ModelMiddlewareInput with request, origin, stream and continuation_id. Forward through invokeModel or startModelInvocation with that continuation ID and the unchanged request. The continuation may be used once, only by its live callback; alias/output overrides are not accepted. Use the request-transform seam for input edits. Returning a response without forwarding short-circuits the remaining chain. Forwarding and then modifying the response wraps downstream behavior without creating another inference operation.

Register model_adapters: [{ id: "example.adapter", version: 1 }] during initialization and implement model.adapter. IDs are globally unique dotted names, at most 160 characters, with lowercase letters, digits, _ and - in each segment; duplicate registrations anywhere in a candidate generation fail. Select an implementation with request.adapter, or set options.adapter on a configured model for foreground calls. An explicit request value takes precedence; "native" selects the host protocol. The reserved option is stripped before native transport. A missing or revoked selected adapter fails without falling back. An adapter can return synthetic output or delegate a separate bounded call through host.model.invoke with adapter: "native".

The callback receives { adapter_id, request, stream }. For a unary call, return { response }. For a stream, either return that response or emit ordered events with emitModelEvent, ending in exactly one done or error, then return {}. Declare requireModelEmitService() to emit. Mixing response and emitted events, missing terminal events, or emitting outside the exact streaming callback fails. No credential is sent to the plugin.

Adapter accounting is declared at registration:

  • mode: "direct" (the default) records a potentially billable submission and checks the host workflow budget before invoking the callback. Returned usage is plugin-reported usage for this adapter’s work. Omitted usage remains unknown, not zero. Any additional host.model.invoke calls have their own submissions and costs.
  • mode: "delegate" is for wrappers such as { id: "example.forward", version: 1, mode: "delegate" }. The wrapper has logical-operation accounting but no billable parent submission. Its response usage is a summary only; each host model child records its own actual provider submissions and usage, so forwarding a response does not charge it twice.

These modes are explicit accounting contracts for trusted extensions. Native forwarding uses the host’s configured credentials and provider transport. The Extension API does not provide a credential-attaching arbitrary HTTP transport, and Wuu cannot enforce its inference budget over arbitrary network requests made by plugin code outside host services.

model.adapter and model.middleware handlers are intrinsically reentrant in the TypeScript runtime adapter: they can overlap a tool or callback that is awaiting them. Protect shared mutable state. Additional concurrentCapabilities and concurrentServices remain explicit opt-ins; lifecycle barriers still apply.

Declare requireBackgroundService(), provide a versioned service with a finite handler, then call startBackground(host, { key, service, method, params?, timeout_ms? }) after activation. The target must be a service owned by that same plugin generation. Starting returns { job_id, execution_id }; the callback receives a normal scoped host and cancellation signal. It may call the model service after the originating conversation or observer handler has returned.

Use inspectBackground(host, { job_id }) for running, completed, failed or cancelled and any result/error; use cancelBackground to stop it. A running key is unique per plugin, with at most 64 running jobs and 64 retained terminal results. Parameters and results are each bounded to 1 MiB. Handles and results are not a durable scheduler: reload, disable, generation retirement and process loss cancel owned work; old generations cannot start more. Plugins own timers, scheduling, budgets, snapshot invalidation and durable business state, and clear timers during shutdown.

The public-SDK bounded replay fixture and shadow comparison fixture exercise these surfaces with synthetic data. They are acceptance fixtures, not installed caching or comparison products. Run node scripts/plugin-model-pipeline-e2e.mjs against a source-built core; the driver uses a local fake provider and records repeatable evidence without paid inference.

Declare requireProcessService() to consume host.process version 1. From a live conversation tool callback, call startProcess(host, { key, command, cwd?, timeout_ms?, follow_parent? }). It uses the caller’s permissions and pinned local or remote execution environment, including that environment’s workspace and process manager. A remote path is never interpreted on the plugin’s or host’s local filesystem. Missing, unavailable or incompatible remote execution fails; it never substitutes a local process. Starting from activation or an unrelated capability is rejected.

getProcessEnvironment(host) inspects that live callback’s selected location without connecting or provisioning it. It returns { backend, root, identity? }: backend: "local" identifies local execution; root is the workspace root on the selected backend; identity, when present, is its opaque pinned identity. These are observations, not selectors or authority. The method and the optional environment field on process snapshots were added in host.process 1.1. Existing version-1 start/read/stop/send calls remain compatible. An older host rejects the new method explicitly; missing snapshot metadata does not imply local execution. Snapshot metadata is captured at process acceptance: changing defaults or handing event delivery to a parent does not move a running process.

A repeated key in the same conversation and plugin generation with identical arguments returns its retained receipt; changed arguments conflict. There may be at most 64 running processes per generation. Terminal handles are retained up to the host’s 256-record receipt bound. A disconnected start may already have had effects before its reply was lost. Keys protect retained host receipts, not exactly-once execution across restarts or receipt eviction. A retained failed-start receipt returns the same error for that key, not another launch. Use the reported operation_id to reconcile an uncertain remote start against the selected backend’s process records before starting another command. Helpers never retry automatically.

All process helpers accept an optional final { signal } argument. The callback’s own cancellation still applies. Before acceptance, cancellation prevents or cleans up the start where its outcome is known. After acceptance, the process belongs to the plugin generation and may outlive the tool callback. Cancelling a read or aborting the original call’s signal does not stop that accepted process; call stopProcess for explicit cancellation.

readProcess returns output, byte cursors, truncation and a process snapshot. Omitting stream preserves merged output. With host.process 1.2, stream: "stdout" or stream: "stderr" selects an independent byte cursor and the result echoes stream. Old records, PTYs and backends without separate streams fail explicitly; the SDK rejects a missing stream acknowledgement. Raw output remains passive. Reads accept 4 bytes to 1 MiB (default 64 KiB), and up to 30 seconds of waiting. Use next_offset, including when UTF-8 framing or log retention changes a read’s length. stopProcess stops the entire process tree. A zero timeout means no deadline; archival/deletion, generation closure and host shutdown still stop the process. follow_parent: true transfers the recipient of a completed private child to its parent; snapshots report the current session_id.

sendProcessEvent(host, processID, message) uses the same request identity, presentation and if_running contract as host.session.send, but the host selects the recipient captured by that process. This also permits events to the originating private child without granting arbitrary private-session access. Output and process exit alone never wake a conversation. Plugins own framing, filtering, event policy and durable history; handles do not survive a runtime restart. The bundled Monitor plugin demonstrates this surface.

For observation inputs, presentation.kind: "background_event" keeps the event in model context and durable history while omitting its input bubble from the conversation. Assistant output, tool activity and turn failures remain visible. This presentation is available to any plugin using host.session.send or sendProcessEvent; it does not change admission, steering or request deduplication. Human input retains its ordinary presentation. Older hosts reject the new kind; update the host and clients together before using it.

Return provided_services during initialization and implement invokeService. A descriptor contains a dotted lowercase name, strict MAJOR.MINOR.PATCH version, and methods with input_schema and output_schema identifiers. These identifiers name contracts; they are not inline JSON Schema definitions.

Consumers declare the service name and major version, then call the gateway. The host supplies the caller identity and routes to the active provider. Do not address another plugin’s process or depend on its private files. An adapter may provide and require the same service with an explicit consumer binding to a different implementation; the resolved graph rejects self-routing and cycles. See partial provider extension. Missing required services block the consumer; duplicate providers for a name and major version produce diagnostics rather than merging their implementations.

The service composition example contains two buildable packages: a stateless text-analysis provider and a writing-report tool that consumes its version-1 API. It demonstrates both declarations and a real gateway call.

For composition acceptance, exercise more than successful service calls: activate a provider and a consumer with a package-owned skill, then terminate the provider and make its next initialization fail. After a recovery check, verify that retained and new consumer toolkits cannot load the skill or call the revoked contributions, while independent packages and ordinary workspace skills still work. Verify that an already-created prompt remains unchanged. This scenario needs no model request: a JSONL runtime fixture and direct load_skill calls can check the lifecycle boundary.

The TypeScript runtime adapter keeps ordinary requests ordered by default. Set RuntimePlugin.concurrentServices to the names of provided services whose handlers may safely overlap other handlers:

const plugin: RuntimePlugin = {
concurrentServices: ["example.text.analysis"],
// initialize, invokeService, and other callbacks...
};

Use this only for reentrant handlers: protect shared mutable state and propagate cancellation. In an A → B → A service call chain, A’s callback service must be able to run while its original handler awaits B. Opt-in applies to incoming service.invoke requests for the listed names; it does not make every tool or capability concurrent, grant service authority, or resolve package dependency cycles. RuntimePlugin.concurrentCapabilities provides the same opt-in for listed capability IDs, for example a read-only status request while a foreground task waits. Protect the state each concurrent handler reads or writes. Initialization and activation remain barriers. Shutdown stops new admission and waits for admitted work before cleanup. This is asynchronous overlap in one JavaScript process, not worker-thread execution.

Tool and capability calls have a host-owned execution ID. The SDK supplies execution.signal; pass it to cancellable operations and stop work when it aborts. The host passed to tool, service, and capability handlers automatically binds both the execution ID and its abort signal to service calls. Use that handler host for foreground work instead of an unscoped host saved at initialization. host.call(method, params, { signal }) can add cancellation; it cannot remove the execution’s signal. A canceled pending call rejects and a late response is ignored. Returning from the invocation closes its scope. Late progress cannot reopen a completed execution, and cancellation does not wait for the plugin to acknowledge it.

The TypeScript runtime adapter rejects cancelled tool, capability, and service requests before entering their handlers, including requests waiting behind other work or a lifecycle barrier. Cancelling an execution does not skip lifecycle callbacks needed for cleanup, and reaches every live callback sharing that execution ID. Once a handler has started, cancellation remains cooperative: propagate its abort signal to operations that support it. Cancellation cannot undo completed effects or forcibly interrupt arbitrary plugin code.

App-server’s read-only plugin/executions/list returns { executions: [...] } for the connected runtime’s current generation and older generations still held by active work. Each row adds generation_id and retained to the host-local id, owning plugin_id, available execution scope, and latest progress. Use the pair (generation_id, id) as a stable diagnostic key: different generations can both have exec-1. The original id and cancellation routes do not change. Rows are sorted by generation ID, then execution ID, and disappear when the invocation finishes; reading the list does not keep a generation alive. This is a point-in-time view, not execution history or a cross-workspace inventory. Disabling a plugin cancels its work across retained generations as well.

reportExecutionUpdate, invokeTool, askUserQuestions, and importArtifact call versioned services and require the corresponding declarations. An orchestrator uses activity.orchestrator=true and execution.invoke-tool to delegate effects through the normal tool path. Keep a child call_id stable across retries with identical tool name and arguments; do not reuse it for different work.

The security.authorize service may further restrict an operation, but cannot relax the host’s mandatory boundary. A sandbox.process provider must supply real filesystem enforcement; it is not a command approval dialog. See permissions before implementing either.

The built-in deferred plugin_manager tool accepts status, apply, enable, disable, and reload. apply takes a workspace-local TypeScript file or development package directory and performs a one-shot publish through a host-selected CLI bridge. It accepts neither arbitrary commands nor a caller-selected Wuu home. Source execution requires host-code authorization before validation/import, separately from permission to write the source file. See the agent workflow.

Status returns a plugins list. Compare published_fingerprint with current_conversation_fingerprint and inspect adoption_pending, desired_enabled, trusted, and current_conversation_runtime_state. An error reports runtime diagnostics when present. These fields report publication, policy, and this conversation’s bindings; they do not prove a particular tool succeeded or a desktop view rendered.

The separate hosts list queries live app-server hosts, including independent user/product hosts. Each entry reports its binding, generation, executable and storage identities, fingerprint, runtime state, and error. wuu plugin hosts [id] exposes the same observations. An empty list means no compatible host reported; it is not activation evidence. wuu plugin reload <id> (or plugin_manager action reload) rebuilds host generations to recover a repaired failed runtime. A client request retry does not reload the runtime.

An ordinary update leaves the current turn and its outstanding workers or managed processes on their tool and prompt generation. Discover and invoke updated tools at the next idle turn boundary after that work settles. A committed disable or removal instead revokes the plugin across retained generations without waiting for an idle turn. Publication is not a tool invocation or a rendered UI check, and it does not schedule a continuation by itself.

Local UI control is an exception to ordinary generation retention. Updating an owner revokes its browser/computer-use authority before the replacement activates; disabling, removing, or closing it stops its activity too. A disconnected or replaced activity-bound MCP connection cannot keep controlling the local UI. Reconnecting within the same plugin generation permits a new explicit user run, without reviving old calls. Do not retry an old run with another local-control backend after revocation. See runtime generations.

wuu plugin dev ./extension.ts runs a single file whose default export implements RuntimePlugin. Wuu supplies the matching SDK and generates the process adapter internally; manifests, installation, packaging, and builds are optional for this development path. It requires Node.js 22.13 or later with --experimental-transform-types support and supports only Node builtins and @wuu/plugin-sdk imports. It snapshots each candidate and validates syntax, imports, and its default export before publication, retaining the last published source on loading failure. Import validation executes module top-level code, so it is not a side-effect-free check. initialize declares contracts in the actual host; prepare then validates state through the selected services. Preparation failure preserves the old runtime; activation happens after commit and can leave a failed new runtime, as described above. See the agent quickstart for a complete example.

wuu plugin create generates agent, desktop, or full package scaffolds. For a directory, wuu plugin dev uses its plugin.json and package.json build script, then publishes the built development generation. Both paths keep watching after a failed build/load so fixing and saving recovers without restarting. Filesystem events are debounced, with periodic reconciliation for missed events; --poll must be positive.

An authorized development override keeps the installed home-wide package’s storage identity while retaining its plugin:dev:<id> executable identity and development permissions. The signed generation receipt pins this binding across subsequent publications. Existing receipts without a binding acquire it on the next publication; a standalone development package keeps its own storage identity. Overriding code does not replay or rewrite ownership transfers.

Before replacing the published package, dev asks every registered live host to prepare the candidate with that host’s selected providers and configuration. Rejection preserves the published package and running generation. After publication, results include each host’s observed activation state; an activation failure reports that publication already committed. Repair and republish, or explicitly reload, without deleting ownership metadata. Hosts running an older core must be restarted with a compatible core to participate.

For monorepo packages, package.json may declare additional build inputs with "wuu": { "dev": { "watch": ["../shared/src", "../assets/prompt.md"] } }. Paths are relative to the package directory; sibling files and directories are supported. Wuu watches and fingerprints those inputs as well as the package, so edits rebuild and publish without restarting. Absolute paths, package ancestors, symlinked inputs, and more than 64 additional inputs are rejected. Keep generated files out of the list; .git, node_modules, and dist descendants remain ignored. This metadata affects development watching only, not runtime permissions.

Use validate for package structure, test for executable initialization and negotiated descriptors, and real conversations or rendered UI for behavior. pack creates a local zip; it does not upload to a registry. Inspect the archive before distributing it: preparation excludes .git and node_modules, not every local file that might contain private data.

The install CLI grants trust and enables the package. Updates from the same recorded source retain trust and the enabled state; a changed or previously unrecorded source requires confirmation. Development-directory authorization does not become trust for a distributed package. See plugin management for the commands and security for the trust boundary.

Exact authoring types live in packages/plugin-sdk/src/index.ts. The Go host and its internal application-embedding facade are host implementation code, not additional plugin authoring SDKs. Check these against the Wuu version you target. Product CalVer, package schema, capability protocol, service versions, and desktop snapshot versions are distinct compatibility checks.

A package can move a user-scoped storage prefix to another package with reciprocal storageTransfers.exports and storageTransfers.imports manifest declarations. Each declaration contains id (stable transfer identifier), plugin (the peer’s package ID), scope (user), domain (the host-selected product domain), and keyPrefix (a nonempty valid storage-key prefix). The exporter names the importer; the importer names the exporter. Every other field must match exactly.

The host resolves exact discovered package identities and requires independent trust for both packages. A disabled exporter may declare the transfer without running. The importer must be selected for activation. Missing exporters do not trigger a search of old identities or other user/product roots. These declarations are fingerprinted package inputs, not cross-plugin storage RPC permissions.

Importers must defer reading the imported prefix until their effectful activation callback, not cache its contents during initialization. Preparation does not mutate storage. The selected settings provider checks ownership, target conflicts, and quotas during preparation; a failed check rejects the candidate before commit. At accepted startup or generation activation, the host rechecks the complete target document before fencing the source prefix, then atomically persists the raw copied values and an import receipt. Conflicts or storage quota overflow fail closed without overwriting target state. Transfer failures block the importing runtime. Pending transfers recover on the next accepted activation; a completed receipt prevents replay after target deletion. The existing per-plugin entry and byte quotas still apply to the combined target.

Source bytes remain as an inactive backup. Ordinary reads and writes cannot access that prefix after its durable fence, including from retained older generations. Other source prefixes and other product/user roots remain unchanged. The host does not parse plugin business records. Returning to an older plugin version does not undo ownership or restore access to the backup.

Transferred documents use storage schema 2 so older host binaries fail closed rather than ignore ownership metadata. Untransferred documents remain schema 1. Host downgrades cannot use transferred state; recovery requires a migration-aware host and retained raw backups, not automatic restoration of the previous owner.

Besides local directories and ZIP files, inspect, install, and update accept npm:<name>@<exact-version>. Both scoped and unscoped names work; ranges, tags, Git URLs and implicit latest do not. The default registry is https://registry.npmjs.org; --registry explicitly selects another HTTPS npm registry. Put options before positional arguments:

Terminal window
# Replace this illustrative package name with a package you trust.
wuu plugin inspect --json npm:@your-scope/wuu-plugin@1.2.3
wuu plugin install --integrity 'sha512-BASE64_FROM_INSPECTION' npm:@your-scope/wuu-plugin@1.2.3
wuu plugin update my-plugin npm:@your-scope/wuu-plugin@1.2.4

The archive must contain a package/ root with package.json, a supported Wuu manifest, and all runtime assets. The npm name/version must match registry metadata, and the plugin manifest version must equal the npm version. The plugin ID remains independent of the npm name; update requires that installed ID. Publish self-contained JavaScript bundles, with build tools and the SDK as development dependencies. Nonempty dependencies, optionalDependencies and peerDependencies are rejected, not installed. Wuu never invokes npm, lifecycle scripts or package builds during inspection or installation. Single-file TypeScript development remains unchanged and needs none of this packaging.

Each remote artifact requires a SHA-512 integrity value, verified before extraction. Inspection returns a distribution object; installation stores that same lock schema in <WUU_HOME>/plugins/<id>/.wuu-package-lock.json, covered by the package fingerprint. Keep an independent copy before updating:

Terminal window
cp "${WUU_HOME:-$HOME/.wuu}/plugins/my-plugin/.wuu-package-lock.json" ./my-plugin.lock.json
wuu plugin install lock:./my-plugin.lock.json
# Or deliberately restore the pinned artifact for an already installed ID:
wuu plugin update my-plugin lock:./my-plugin.lock.json

Lock replay fetches only the recorded tarball, never mutable registry metadata; it still needs network access. The lock identifies registry, package name/version, plugin ID, tarball URL and integrity. It is artifact provenance, not an npm provenance/signature verification, author certification or safety audit. Trust the publisher before installing. Restoring an older artifact does not reverse provider data migrations or completed external effects.

Downloads use HTTPS without credentials, cookies, URL query strings or fragments. Wuu does not load npm credentials from .npmrc or token environment variables. Tarballs and redirects must stay on the selected registry origin; authenticated registries and cross-origin CDN URLs are not supported by this path. Metadata is limited to 4 MiB, compressed archives to 128 MiB, and extracted packages to 512 MiB / 10,000 entries. Links, traversal, case-colliding paths and a publisher-supplied lock file are rejected. There is no alternate-registry or local-package fallback.

Fetching and validation finish before acquiring the catalog mutation lease. The resolved bundle then uses the same preparation, trust, atomic package publication and generation lifecycle as a local package. Registry plus npm name is the recorded source identity: exact-version updates from that same source retain trust and explicit enabled/disabled choices; a registry or package-name change requires source confirmation. An invalid artifact or failed preparation keeps the prior generation. Post-commit activation failures report the committed state rather than pretending rollback occurred. Already admitted work retains its captured generation.

The plugin/package/inspect and plugin/package/install RPCs accept the same source in path, with optional registry and integrity fields. A lock source does not accept registry or integrity overrides. Registry requests run outside the serial RPC dispatcher, so unrelated sends and reads do not wait for a download. CLI signals cancel downloads; RPC downloads inherit the request context and are canceled and drained when the connection’s server closes. There is no separate package-download cancel RPC. Official and community npm packages go through these same public contracts; installation does not grant bundled-package privileges.

A replacement personal.agents@1 provider exposes observe-execution with {session_id} and returns PersonalExecutionFacts from the SDK. It resolves its own agent, task, requirement revision, run, active/cancelled state, owning routine, and exact original human source. The host independently checks session ownership, source history, the selected generation and execution grant. These facts do not grant permission to execute an action.

For revised tasks, return task.requirement_sources with one {revision, session_id, message_seq} entry per requirement revision, in order. References must come from host-bound human turns, never model arguments or goal summaries. Repeated references are allowed for retries; sequence numbers must not move backwards. The host resolves active human history in the owning agent’s main conversation, checks the complete chain and binds it to the action digest. Later restrictions override earlier permission. Missing evidence disables intent review; retracted or foreign sources cannot authorize dispatch. Legacy single-source facts remain eligible only at revision 1. Do not reconstruct missing provenance from cached text or silently omit revisions.

For the existing Personal review/memory coordinator, store these durable facts in the provider’s user-scoped host storage, in any key/value format. During an effect the coordinator retains the selected provider and holds that storage’s opaque durable inspection, along with session and review guards. A standalone observation is not a transaction or an execution fence. An alternative storage backend must preserve the host storage inspection contract; an in-memory cache must not become an independent authority for these observations.

legacy-memory returns a map of legacy record IDs to their serialized values for migration into the selected memory journal, or an empty object if none exist. The coordinator never interprets another plugin’s private storage keys. Neither method writes state or restarts a task. A disabled or unavailable provider must not be replaced with a hidden bundled fallback.

If the same runtime also calls back into host.personal (for example from a memory operation), include personal.agents in its SDK concurrentServices: the host may read these facts before completing the original call. Do not queue that read behind the invocation waiting for the host. Keep mutations synchronized through storage and the inspection contract rather than serializing the entire service.

personal.coordination@1 is the replaceable review/memory/file policy service. The Personal bundle selects wuu.personal-client, the native wuu.personal-actions-consumer coordinator, and wuu.personal-effects host mechanisms. Binding the client to another provider or disabling the coordinator never constructs a native fallback. The Go host still authenticates ingress, resolves sessions and enforces resource boundaries.

Coordinator requests are {operation?, input?, session_id?, call?, scopes?}. call handles review.* and memory.* with the existing operation input/result; file-gate receives a tool call and returns a tool result; scope and cancel support exact session/task cancellation; inspection handles owner-admitted review-policy/settings/session recovery, serialized with live Personal effects. initialize and drain are lifecycle/scheduler methods.

A replacement declares host.personal-effects@1. Its public methods are:

  • prepare-file: a tool call -> {effect_id, file} containing the frozen action.
  • execute: {effect_id} -> {tool_result}; each handle executes at most once.
  • release: {effect_id} -> {}; a declared cleanup method, safe to repeat.

Only the selected coordinator can use these mechanisms. Process callbacks carry host provenance through their execution ID, not serialized State/toolkit or owner claims. Deferred file handles survive the originating callback and retain the original graph and toolkit; later authenticated owner admission does not retarget them. They are process-local capabilities, not durable approval receipts. A cold restart requires new admission and revalidation of the persisted frozen action; uncertain outcomes are never automatically replayed. Connector handles are synchronous and expire when the broker callback finishes.

Compatible native generations share pending coordination state with counted ownership. Old-generation disposal cannot close a core borrowed by its successor. Explicit disable/crash seals admission, cancels and drains owned effects before provider disposal; host close also releases pending resources. Native mechanisms use ServiceRegistry.RegisterRetirementCleanup for this physical cleanup barrier and DeferServiceCompletion only for final graph-reference releases, which may reenter shutdown. These are lifecycle mechanisms, not extra policy authority.