Yujie Zhou

More Than Tools: How Open Nua Turns Plugins into Domain Products

Open Nua Engineering

Many Agent products treat a plugin as a list of tools: connect a few APIs, add a prompt, and let the model decide when to call them.

That is enough for a demo, but not for a domain product that must remain operable over time. Where do the tools come from? Who may call them? Should a query and a deletion follow the same approval path? How does a domain UI obtain data? How does local state migrate? When a plugin is disabled, do its skills, capabilities, and surfaces disappear together? How is the package itself installed, verified, and audited?

In Open Nua, a Plugin is not a synonym for “more tools.” It is a versioned delivery unit that combines domain knowledge, governed capabilities, Agent definitions, user interfaces, and state contracts. Desktop, Gateway, and Hub then execute only the parts they own.

This article explains why the system is designed this way and how a Plugin travels from an archive to a domain workbench on screen.

The original problem: one runtime kind carried too much meaning

An earlier design divided official Plugins into remote_mcp, local_app, and python_agent. The taxonomy looked simple, but it mixed several unrelated questions:

  • whether a capability came from a remote service or the local Host;
  • whether the Plugin had an App;
  • which capabilities the Agent could use;
  • where state lived;
  • whether the Plugin replaced Desktop's primary Agent graph.

Real products quickly broke that model. Vibe Trading and DeepTutor obtain capabilities through remote MCP services, but both have full workbenches. The claw-machine Plugin keeps its state on the device and uses the same App surface. UI clearly should not be inferred from “remote” or “local.”

python_agent was an even deeper exception. Instead of contributing a normal capability, it could replace the entire Python Agent graph. No official Plugin needed that path, yet Desktop, the Bridge, and installation code all had to preserve it.

We removed those top-level kinds and separated the concerns: a Plugin is one asset, while Skills, Capabilities, Apps, Agent Definitions, and runtime adapters are orthogonal contributions.

One package, two contract layers

An Open Nua Plugin uses Agent Plugins v1 as its portable core:

my-plugin/
├── PLUGIN.md
├── plugin.json
├── skills/
│   └── my-plugin/
│       └── SKILL.md
└── com.opennua.desktop/
    ├── agents/
    └── ui/

The root plugin.json carries standard identity, version, description, and license metadata. Standard Skills live at skills/<name>/SKILL.md. A root mcp.json is needed only when the package intentionally exposes a standard MCP endpoint to generic Agent Plugins clients.

Open Nua-specific semantics live under extensions.com.opennua.desktop: governed Gateway routes, local state, Capabilities, Agent Definitions, declarative UI, App entry points, and signed assets. A generic client can ignore an extension it does not understand and still load the standard Skills. Open Nua Desktop can activate the same package as a complete domain workbench.

One current boundary is easy to misread: Hub can validate a root mcp.json, but Desktop does not use it to establish an Open Nua runtime, and none of the four official packages currently contains one. Open Nua's remote execution path reads gateway_mcp only from the signed extension. A root mcp.json is portable discovery for generic clients, not a second route around Gateway.

This boundary keeps the portable standard honest. It does not pretend to understand one desktop client's sandbox, state, or UI protocol, while client-specific behavior does not leak into the Plugin's universal identity.

The governed path from an Open Nua Plugin package to a domain workbench

A Skill teaches the Agent; it does not grant execution authority

Skills and Plugins are often treated as interchangeable. Open Nua separates them deliberately.

A Skill is operational knowledge for an Agent: when to use a capability, which sequence to follow, what evidence to preserve, and when to stop. It can rely entirely on existing tools and need no new runtime.

A Plugin owns product semantics such as versioning, installation, activation, Capabilities, Apps, and state. It may contribute several Skills, but reading a SKILL.md never grants network, filesystem, account, or tool access.

OpenCLI is the current official skill-only Plugin. It contributes knowledge for authoring adapters, driving browsers, creating sitemaps, and applying repairs without declaring an Open Nua runtime adapter. The example makes the distinction concrete: Plugin is the distribution and governance boundary; runtime is optional.

Capability is the system's smallest security unit

The object that connects Agents and Apps to implementations is not a raw tool name. It is a Capability declaration. Every Capability must state:

  • provider: gateway_mcp or a local read-only context;
  • target: the governed operation it maps to;
  • audience: Agent, App, or both;
  • operation: query or mutation;
  • risk: the risk level;
  • requiresConfirmation / requiresHitl: approval and human-intervention rules;
  • idempotency: how repeated mutations are handled.

“Read a learning path” and “delete a learning path” may come from the same MCP service, but they are not the same authorization. The first can be a low-risk query. The second is a high-risk mutation that requires confirmation and idempotency.

The same declaration governs Agents, declarative UI, and Plugin Apps. A button does not bypass policy because it came from a UI, and an Agent cannot bypass the allowlist by knowing the underlying tool name. Every request returns to the same Capability validation and dispatch path.

In other words, a Plugin does not receive Host authority. It receives a set of auditable capabilities that the Host executes on its behalf.

Two adapters, three product shapes

The current 1.2 contract keeps two internal runtime adapters and permits no adapter at all:

ShapeBest suited forCapability or state ownerCurrent official examples
gateway_mcpCentral data, remote tools, unified policy and auditGoverned domain service behind GatewayVibe Trading, DeepTutor
local_stateOffline, local-first domain AppsDesktop HostClaw Machine
Skill-onlyOperational knowledge using existing toolsNo added runtimeOpenCLI

A gateway_mcp manifest declares only a Gateway path, tool allowlist, and timeout. A Plugin cannot inject an arbitrary upstream URL into the client and does not receive persistent model credentials. Deployment policy owns the actual service location and Gateway owns the proxy boundary.

local_state is intentionally different. An App receives no filesystem, browser storage, or Node access. It reads and writes one JSON document, isolated by user and Plugin, through the Host Bridge. The document has a 64 KiB limit, schema version, revision, and atomic replacement semantics. On a version mismatch, the Host returns the old data and requires an explicit Plugin migration. Corrupt state produces a recoverable error instead of silent deletion.

Version 1.2 does not allow one Plugin to combine gateway_mcp and local_state. This is not a file-format limitation. A mixed trust boundary needs its own security design and should not be enabled by casually adding one more field.

An App is a governed view, not a second client hidden in an iframe

Some domain experiences do not fit inside chat. Learning paths need progress and review queues. Investment research needs evidence and run comparison. A claw machine needs a live 3D scene. Plugins may therefore contribute conversation cards, declarative workbenches, and full App surfaces.

A full App is still not an arbitrary web client. The release package records SHA-256 digests for the entry HTML and dependent JS, CSS, WASM, GLB, and other assets. Desktop verifies them and serves the multi-file SPA through the openneo-plugin:// protocol. The App runs in a restricted frame without direct Electron IPC, model, MCP, network, or filesystem access.

It can do two things: render its packaged assets and ask the Host Bridge to invoke a Capability that is declared in the manifest, authorized for the current user, and exposed to the App audience.

Rich UI and broad authority are therefore separate decisions. The claw machine can load Babylon.js, Havok WASM, and 3D models while retaining only governed local state. Vibe Trading can render a complex research workbench without gaining arbitrary network access inside the frame.

From an archive to a real invocation

A Plugin crosses a clear ownership chain before it becomes usable:

  1. The Plugins repository owns the domain product. Official packages, Skills, Agent Definitions, UI source, state schemas, domain runtimes, and tests evolve there.
  2. Backend Plugin Hub verifies the distribution asset. It checks archive shape, path traversal, links and special files, manifest schema, SemVer, Skills, Agent Definitions, and App and asset digests. At publication, Hub creates a signed envelope that binds version identity to the archive hash.
  3. Desktop verifies, atomically installs, and defaults to disabled. It verifies the archive SHA-256, manifest SHA-256, Ed25519 signature, and archive binding; revalidates Skills, Agents, and App assets in staging; and publishes the directory with one atomic replacement. Only an installed + enabled Plugin contributes Skills, Agent context, Capabilities, and Apps to new conversations. Disabling removes those runtime contributions without silently deleting local state.
  4. Desktop compiles one runtime plan. The main process parses the manifest into a PluginRuntimePlan and activates a PluginHandle. Renderer and App Host do not need adapter implementation details.
  5. Every invocation follows one authorization path. Desktop checks activation, audience, risk, and target mapping. Remote capabilities use a scoped tool token through Gateway's governed MCP route; local capabilities remain inside the Host boundary.
  6. Domain results return to the shared conversation model. A Plugin workbench reuses Desktop's standard Threads, executions, traces, and artifacts instead of maintaining a private chat runtime.

There is no monolithic “Plugin service” that owns everything. Hub owns packages and version facts. Gateway owns the controlled remote proxy. Desktop owns the local Host and user interaction. Each Plugin owns its domain logic and experience. Clear ownership is what allows independent evolution.

One workbench, two execution paths, one standard conversation

A Plugin workbench does not recreate chat. It reuses Desktop's standard Thread and therefore inherits message persistence, streaming, tool calls, HITL approval, retry, traces, artifacts, diagnostics, and remote control. Whether a user enters Vibe Trading from a normal conversation or opens the assistant from the claw-machine App, the underlying session is the same kind of conversation—not a Plugin-private message store, executor, and debugging stack.

Reusing a conversation does not mean putting domain state inside it. Research ledgers, learning progress, and game state remain facts owned by the Plugin runtime. A Thread records the human-Agent collaboration around one domain object. The App does not reconstruct its UI by parsing assistant text. Closing the right-hand assistant therefore collapses only the conversation surface; it does not delete the conversation. Reloading the App restores domain state from the runtime without replaying chat history.

The two are connected by a runtime-minted, Host-validated contextRef. It is a governed locator for a domain object, not a bearer token. A standard Thread carries an immutable (pluginId, contextRef, agentId) binding:

  • When the App calls activateContext(contextRef), the Host revalidates the reference and restores the most recent exact-match Thread.
  • If no matching conversation exists, the workbench stays empty until the user sends the first message, avoiding empty Threads for objects that were merely viewed.
  • When the user switches a research project, course, or game instance in the App, the Host selects a different bound conversation. It never silently rebinds an existing Thread to the new object.
  • Before each run, the Plugin Agent receives a minimal, versioned context projection from the runtime. The App cannot rewrite system instructions or model context directly.

This is also why the workbench has two execution paths. Deterministic operations—saving a research item, updating learning progress, or resetting a game—go from the App through invokeCapability, then through the same Host authorization, confirmation, and audit path. Work that needs explanation, research, judgment, or generation enters the standard Thread bound to the domain context, where the Agent invokes the same governed Capabilities. Both paths share permissions and domain facts without forcing every button to masquerade as a prompt or treating natural-language output as a state-write protocol.

Synchronization between the conversation and the App surface is bidirectional, but they exchange intent and invalidation—not each other's internal state:

The Plugin workbench synchronizes context, capability calls, and invalidation events across the App surface, Desktop Host, standard Thread, and Plugin runtime

From App to conversation, openAssistant only expands or restores the Host-owned assistant. A from-scratch exploration may request an unbound draft, but the first real message still becomes a standard Thread. From conversation to App, the Host derives a revision from the active Thread, message count, and pending approval, then tells the restricted frame that data may have changed. The App re-reads authoritative data; the Host does not push raw messages, MCP results, or arbitrary domain objects into the iframe.

After a deterministic App operation, a Plugin may also produce a governed conversation continuation: a compact handoff describing what happened and what might be discussed next. The user can click a domain action and have the assistant continue with explanation or analysis, while the domain write has already been completed by the Capability. The assistant remains a collaborator instead of becoming a fragile synchronization bus.

The extra boundary buys concrete consistency. Apps recover independently, Threads retain long-lived history, switching domain objects cannot contaminate an old conversation, closing the assistant does not close the workbench, and disabling or reinstalling a Plugin does not require cascading deletion of standard conversation history. Workbench and conversation stay synchronized without competing to own the same state.

Five lessons from the design

1. A Plugin is a product delivery boundary, not a tool transport format. Domain experiences become maintainable only when versioning, activation, UI, state, capabilities, and audit are designed together.

2. Orthogonal contributions outlast mutually exclusive types. Apps, Skills, Agent Definitions, and adapters can be composed. One runtime kind that predicts every behavior will eventually fail against real products.

3. Skills must not become an authority shortcut. They teach an Agent how to work; execution authority comes from an independent, verifiable Capability.

4. The richer the UI, the simpler the Host boundary should be. A narrow Bridge makes it possible to support multi-file SPAs, WASM, and 3D assets without losing a legible security model.

5. A shared conversation is not shared state. Threads own collaboration history, runtimes own domain facts, and Apps own presentation and ephemeral interaction. Governed references and invalidation are more reliable than copying state or parsing assistant text.

The Open Nua Plugin system will continue to expand, but the next step should not be an endless list of new top-level kinds. The better direction is to deepen the same boundaries: clearer capability policy, stronger signing and distribution, better developer tooling, and a portable standard core that remains useful across more Agent clients.

A good plugin system does not make an Agent able to call anything. It makes every domain capability answer where it came from, who may use it, which boundary executes it, and who owns the failure when something goes wrong.