VibeX

Plugin architecture

A VibeX plugin is an installable, toggleable, configurable product unit. One package is one user-visible capability: stable identity, one-line summary, content tree, root config, and lifecycle. App, Agent, Host, and Runtime are integration targets inside the package. Settings → Plugins shows name, summary, publisher, version, and the enable switch.

Changes to platform source, Tauri commands, or Application Core belong under Platform architecture. When the public SDK lacks a Host capability, add a generic capability and SDK export first, then consume it from the plugin. Core may know contribution kinds and Host slots. Special cases keyed by plugin ID or file format are debt.

Trust model

Install or enable means Full Trust. Worker, App, declared Runtimes, filesystem, process, and network use the same local rights as the Host. Separate processes and App frames handle hot reload, crash isolation, and dispose.

v4 public manifests default packageClass to full-trust. The product UI omits per-capability grant dialogs. Integrity depends on a deterministic digest, candidate rollback, revision-conflict handling, and resource cleanup. The permissions array remains as compatibility metadata for older v4 packages. New packages omit it.

Isolated execution is declared by a v5 manifest: manifestVersion 5, packageClass isolated, schema at packages/plugin-contract/schemas/plugin.v5-isolated.json in the Host repository. Isolated Worker author APIs match Full Trust. The Host applies OS policy at spawn: sandbox-exec on macOS, bwrap or Landlock on Linux, AppContainer on Windows. When that primitive is absent, spawn fails with plugin_class_unsupported. v4 plugin.json rejects unknown top-level fields; Isolated fields belong on the v5 manifest.

Identity and compatibility

Identity is Publisher plus Plugin ID. Display name, folder name, and similar contents remain presentation. Packages with the same ID and different publishers keep separate grants and data. engines.vibex and engines.pluginSdk declare the compatible range. Current matrix: Host 0.1.3, protocol 1.1, SDK 1.0.0.

A linked development plugin follows the author-chosen source directory. After the source changes, the Host revalidates package identity and every contribution, then forms a new digest and candidate generation. The Host keeps that directory.

UI contract

/plugins keeps the settings sidebar with a single-column catalog. A row opens /plugins/:pluginId. The Content tab shows the README and Host-validated contents/. The Config tab renders config.schema and atomically writes root config.json. Generation, Runtime locks, and handler lists belong in doctor and developer tools.

Completion

  • A user can enable the package and finish the promised operation on Content or Config.
  • Manifest, SDK, CLI validation, Host parsing, and UI consumption agree on every integration.
  • After a linked install, the real Host path works. A harness covers in-process contracts. File tabs, preview processes, Runtimes, and remote observation are accepted on a running Host.
  • The README states runtime requirements, offline and network behavior, troubleshooting, third-party licenses, and config retention after uninstall.

Package specification

v4 package paths are relative to the package root and use /. Host and CLI reject absolute paths, .., symlinks, hard links, case collisions, duplicate normalized paths, and over-limit content.

text
my-plugin/
├─ .vibex-plugin/
│  ├─ plugin.json
│  ├─ content.index.json
│  ├─ package.lock.json
│  ├─ signature.json
│  └─ sbom.spdx.json
├─ README.md
├─ config.json
├─ contents/
│  ├─ skills/
│  ├─ mcps/
│  ├─ hooks/
│  ├─ workflows/
│  └─ resources/
├─ depends/
│  ├─ runtimes/
│  └─ packages/
├─ runtime/
└─ dist/

runtime/ holds author source. The release pack contains README, the initial config.json, contents/, depends/, dist/, and .vibex-plugin metadata. signature.json and SBOM are optional.

README

Root README.md frontmatter must contain a standalone summary: one sentence, non-empty, at most 200 Unicode characters, plain text. The catalog and detail title region show summary. The body is the README with frontmatter removed. Host and CLI read the summary field.

markdown
---
summary: Preview, create, and transform Word, Excel, and PowerPoint documents.
---

# VibeX Office

Configuration

Root config.json is the only Config-tab fact. The Host validates against config.schema in plugin.json, then writes via a sibling temp file, fsync, and atomic replace. The file is omitted from the executable digest, signature, and activation generation. Package updates keep the user's existing values by default and run a compatibility check against the new schema. The README states config retention after uninstall.

Persist settings in root config.json. Worker storage.settings.get and storage.settings.put echo the current input.

Content index

.vibex-plugin/content.index.json is initialized by the CLI, maintained by the author, strictly checked at build, and checked again by the Host. The UI reads the Host-returned index. kind drives icons and rendering. The Content tab defaults to README, then lists contents/ items. Each items[].path must resolve to a real file under contents/.

json
{
  "schemaVersion": 1,
  "items": [
    {
      "path": "contents/skills/office-docx/SKILL.md",
      "kind": "skill",
      "title": "Word documents"
    }
  ]
}

plugin.json

manifestVersion is 4. apiVersion is "1.0". readme is fixed as README.md. content.root is fixed as contents. entrypoints.worker.runtime is node, python, or native. Worker protocol 1.1. App protocol 1.0. Omit the retired format=javascript-esm field.

json
{
  "$schema": "https://schemas.vibex.dev/plugin/v4/plugin.schema.json",
  "manifestVersion": 4,
  "apiVersion": "1.0",
  "id": "notes",
  "publisher": "you",
  "version": "0.1.0",
  "name": "Notes",
  "readme": "README.md",
  "engines": { "vibex": ">=0.1.3 <1.0.0", "pluginSdk": "^1.0.0" },
  "content": {
    "root": "contents",
    "index": ".vibex-plugin/content.index.json"
  },
  "config": { "schema": { "type": "object", "properties": {}, "additionalProperties": false } },
  "entrypoints": {
    "worker": { "path": "dist/worker.mjs", "runtime": "node", "protocol": "1.1" },
    "app": { "root": "dist/app", "document": "index.html", "protocol": "1.0" }
  },
  "integrations": [],
  "interface": { "icon": "assets/icon.svg" }
}

id matches ^[a-z0-9][a-z0-9._-]{1,62}$. publisher matches ^[a-z0-9][a-z0-9._-]{0,62}$. version is semver.

Dependencies

depends/ stores dependency descriptors. The dependencies array in plugin.json references them. Each item has:

Field Meaning
kind runtime or plugin
descriptor Package-relative path under depends/
optional When omitted, the dependency is required; true allows enable while the peer is not ready

When kind is runtime, exact Runtime identity is id + version + target + digest. Versions may coexist. Download, integrity, probe, refcount, and reclaim are Host-managed.

When kind is plugin, enabling this package requires the peer to be installed, same publisher, enabled, and to have a live generation. The operator installs the peer.

Build and pack

  1. vibex-plugin build validates summary, schema, index, integrations, and dependencies. When runtime/main.mjs exists, it compiles to dist/worker.mjs. When runtime/app.mjs and runtime/app.html exist, they compile to dist/app/index.html. When an MCP resource declares managedRuntime.source, that source compiles to managedRuntime.entrypoint.
  2. vibex-plugin pack hashes the release file list, writes .vibex-plugin/package.lock.json, emits a deterministic .vxp, and prints sha256:<digest>. install --link also computes and writes the lock from the current tree.
  3. The release pack contains README, the initial config, contents/, depends/, dist/, and .vibex-plugin metadata. The source tree keeps runtime/, test/, package.json, lockfiles, source maps, .git, node_modules, and developer-link.

The Host starts a Node Worker with node --max-old-space-size=128 <entrypoints.worker.path>. A Python Worker is the Host-locked CPython plus that path. A native Worker path must be a compiled executable.

Contribution model

integrations maps package resources onto Host extension points. Each row has a stable id, a Host-known kind, and an in-package resource. Runtime may bind only declared rows. Agent-side contributions inject into conversations created or rebound after the plugin is enabled.

Handler ids

Worker handler ids must match:

text
^[a-z][A-Za-z0-9]*(?:[.-][a-z][A-Za-z0-9]*)*$

The id starts with a lowercase letter. Every segment after . or - also starts with a lowercase letter. Valid: hello, surface.createSession, office-preview. Invalid: Hello, save.XML, task.1. JavaScript, Python, and Rust SDKs share this regex. CLI validation requires surface.createSession for an editable file-tab App surface.

Kinds

kind Role Author notes
content.skill Project contents/skills/... to compatible agents as a read-only native Skill entry resource must exist in-package
content.mcp Host-managed MCP started per Agent session See managedRuntime below
content.hook Hook resource resource and event
workflow.binding Expose contents/workflows/ to Composer and Automation resource is workflow JSON
file.opener Open by extension, fileNameSuffixes, or mediaTypes Exactly one previewProvider or exactly one editorSurface
artifact.preview Broker-managed preview process handler, optional runtime and process.argv
app.surface Full Trust App appEntrypoint is app; editable text uses slot: artifact.editor; detail panel uses plugin.detail.panel
app.command Command palette Keeps Plugin/Command identity
app.toolbar Toolbar slot is toolbar.main
app.status Status bar The UI shows at most three
app.composer.slash Composer slash command Coexists with agent-native commands by source
app.timeline.card Timeline card Separate kind
app.settings.section Settings section config.json remains the config fact
host.service Periodic Worker handler intervalSeconds minimum 5, default 30; at most eight per package

Validation rejects missing, ambiguous, and wrong-slot references.

Skill

resource points at an existing path under contents/. Official packages use a skill directory such as contents/skills/office-docx with SKILL.md inside. The CLI accepts that directory and also accepts a path to SKILL.md. The content index items[].path names a file:

json
{
  "path": "contents/skills/office-docx/SKILL.md",
  "kind": "skill",
  "title": "Word documents"
}

A user Skill of the same name stays. targets may limit agents; the default is the all-compatible-agents binding. SKILL.md is written for the agent: when to apply, steps, inputs and outputs, forbidden actions. An undeclared external CLI may still import; readiness marks the dependency unknown.

MCP

Host-managed MCP uses managedRuntime:

json
{
  "managedRuntime": {
    "source": "runtime/mcp-server.mjs",
    "entrypoint": "dist/mcp/server.mjs",
    "protocolRevision": "2026-07-28",
    "defaultBinding": "all-compatible-agents"
  }
}

protocolRevision must be 2026-07-28. entrypoint must exist in-package. source is optional; when present, build compiles it to entrypoint. A hostFamilyBinary plus binaryId selects a Host-family binary.

The Host injects Workspace and parent Conversation connection context. Plugin storage omits Server URL and credentials. New MCP uses protocol revision 2026-07-28 and negotiates a compatible version. contents/mcps/*.json may also project into agent-native config. Native config remains the Agent Runtime authority.

Workflow and files

Workflow references carry identity and dependency evidence. Publish, debug, and run use Workflow Core on the Host. Those entries close when the Host is offline.

artifact.preview declares handler, optional runtime, process.argv, and a ready timeout. After idle timeout the preview process exits and the next open starts it again. The Worker opens a preview with environment.host.call("artifact.preview", "open", { artifactHandle, providerId }). artifactHandle is issued by the Host, lives about 30 seconds, and is consumed once. The current Host routes every artifact.preview operation to open-preview. Lease expiry and generation drain reclaim the process.

Editable file tab:

  1. file.opener sets extensions, fileNameSuffixes, or mediaTypes, and editorSurface to an App surface id.
  2. That app.surface uses slot: artifact.editor, appEntrypoint: "app", handler: "surface.createSession".
  3. The Worker registers surface.createSession.
  4. The App calls bridge.artifact.readText() and saves with writeText(content, expectedRevision).

The Host holds the canonical path. The App receives file name, revision, and bridge.artifact. Writes use expected revision and atomic replace. An external edit yields a recoverable conflict with code artifact_revision_conflict.

init --template file-tab emits a .txt artifact.preview and a plugin.detail.panel. Declare an editable file tab with the four steps above.

Commands and host services

app.command and app.composer.slash keep Plugin/Command identity and coexist with agent-native commands by source. host.service fits periodic work. The handler must be registered on the Worker. The interval is intervalSeconds, minimum 5, default 30. A package schedules at most eight host.service rows. A tick is skipped while the previous invocation is still running.

Development workflow

Recommended order: locate the local contract, pick a template, declare integrations, implement Worker or App, wire the stdio entry, validate and test, link a running Host, pack.

Locate the contract

From the VibeX repository root, list the contract files that must be read:

bash
python3 .agents/skills/vibex-plugin-development/scripts/locate_toolchain.py

The script prints JSON. The required array contains:

  • packages/plugin-sdk/src/manifest.ts
  • packages/plugin-sdk/src/protocol.ts
  • packages/plugin-sdk/src/worker.ts
  • packages/plugin-sdk/src/app.ts
  • packages/plugin-sdk/src/testing.ts
  • packages/plugin-cli/src/validation.ts
  • docs/plugins/package-v4.md
  • docs/plugins/sdk-and-cli.md

Read every path in required. When missing is non-empty, restore those files first.

Print the built CLI and language SDK paths:

bash
node packages/plugin-cli/dist/cli.js toolchain

toolchain prints hostVersion, cli, contract, js, python, rust, and templates. The Python SDK lives at sdk/python. The Rust SDK lives at crates/plugin-sdk. The checkout is authoritative.

Templates

bash
node packages/plugin-cli/dist/cli.js init my-notes --publisher you --template full

init writes the manifest, README, config.json, content index, tests, and matching source, then builds immediately. Default template: full. --template agent has been removed.

Template Output
skill Skill projection
mcp Managed MCP descriptor and placeholder process
hooks Hook resource
file-tab Node Worker, read-only .txt preview (artifact.preview), and a detail panel (slot: plugin.detail.panel)
full Node Worker, App detail panel, Workflow
ts-worker TypeScript Worker definition (runtime/main.ts)
node-worker JavaScript Worker definition (runtime/main.mjs)
python-worker CPython Worker (runtime/worker.py includes the stdio entry)
rust-worker native Worker source (runtime/src/main.rs includes the stdio entry)
host-service Periodic handler, default intervalSeconds 30

python-worker and rust-worker source already call run_stdio_plugin_worker / run_stdio_plugin_worker_blocking. Node templates write the handler definition to runtime/main.mjs (or runtime/main.ts). The Host starts node dist/worker.mjs; that file must call runStdioPluginWorker at top level. See “Node Worker entry” below.

Declare an editable file tab with file.opener.editorSurface plus an app.surface whose slot is artifact.editor. Steps: Contribution model.

Node Worker entry

The Host starts a Node Worker:

text
node --max-old-space-size=128 <entrypoints.worker.path>

The process must speak protocol 1.1 on stdin/stdout. Recommended split:

text
runtime/worker.mjs   # definePluginWorker(...)
runtime/main.mjs     # runStdioPluginWorker(definition)

runtime/main.mjs:

js
import { runStdioPluginWorker } from '@vibex/plugin-sdk/stdio';
import definition from './worker.mjs';

await runStdioPluginWorker(definition);

vibex-plugin build bundles runtime/main.mjs to dist/worker.mjs. Manifest:

json
"entrypoints": {
  "worker": {
    "path": "dist/worker.mjs",
    "runtime": "node",
    "protocol": "1.1"
  }
}

Official Office uses this split. init --template node-worker writes the definition in runtime/main.mjs; add runStdioPluginWorker there, or split into two modules as above. Tests import the definition from runtime/worker.mjs.

CLI

Run from the plugin root. Connect with --host or VIBEX_PLUGIN_DEV_HOST. Authorization reads VIBEX_PLUGIN_DEV_GRANT. Passing --token or setting VIBEX_PLUGIN_DEV_TOKEN throws dev_link_host_only. The URL must be a loopback HTTP origin: http://localhost, http://127.0.0.1, http://[::1], pathname /. HTTPS, a path, a query, or userinfo yields plugin_dev_host_must_be_loopback_http_origin. Settings → Plugins developer tools emit the loopback connection. Grant file mode is 0600.

Command Role
validate [--json] Validate manifest, index, references
build Validate the package; compile runtime/main.mjs to dist/worker.mjs; compile App and managed MCP sources
test build, then run test/*.{test,spec}.{mjs,js,mts,ts} from a temp directory
install --link . Link the development directory and write the lock; published .vxp installs by drag-and-drop
dev build, link, watch; reload a candidate generation when the digest changes
doctor Install, activation, Runtime, surfaces, bindings, recent crashes
pack [--output file.vxp] Write package.lock.json, emit a deterministic .vxp, print sha256:
uninstall [--delete-data] Unlink; user data kept by default

A failed candidate leaves the previous complete activation generation visible.

Host acceptance

  1. Edit README summary and body.
  2. Declare only integrations you implement.
  3. Keep handler ids aligned with declarations and the handler regex.
  4. For a Node package, confirm runtime/main.mjs calls runStdioPluginWorker.
  5. build, validate, test.
  6. Open VibeX and copy the developer connection (grant + loopback origin).
  7. install --link . or dev.
  8. Enable under /plugins. Exercise the integrations this package actually declares: Skill projection, MCP injection, read-only preview, revision conflict on an editable file, detail panel, slash command, disappearance after disable.
  9. doctor for crash rings and mcpRebindingRequired.
  10. pack and drag the .vxp into the desktop to distribute.

The harness covers in-process contracts. File tabs, preview processes, Runtimes, and workstation observation complete on a running Host.

Testing, security, and release

Test scope

Package tests cover the paths that apply:

  • Declarative package: summary, schema, index, integration refs.
  • Worker: registration, invoke, typed error, dispose.
  • App: mount, ready, revoke, abort.
  • Editable file tab: readText, writeText with revision, external-edit conflict (artifact_revision_conflict).
  • Generation: rollback when a required handler is missing; old handles stale after a successful switch.
  • Negative fixtures: illegal summary, path escape, unknown integration, oversized document.
bash
vibex-plugin build
vibex-plugin validate
vibex-plugin test
vibex-plugin pack
vibex-plugin doctor .

test runs build first. The CLI bundles Node tests into a temp directory and hands them to node --test. Reading plugin-root files via import.meta.url fails in that temp directory; put fixtures in the test module or export constants from test/*.mjs.

Python packages use harnesses in sdk/python with await create_worker_harness(...). Rust packages use cargo test -p vibex-plugin-sdk plus in-package tests. Templates that ship test/plugin.test.mjs assert handler lists via createWorkerHarness.

After a linked install, complete enable, Content, Config save, session injection, and uninstall (data kept by default) on a running Host. A workstation window presents results of execution that happens on the Host. On a failed update, the UI and doctor still point at the previous complete generation.

Security

  • Deterministic pack: the same source yields the same digest. Activation binds the digest.
  • Candidate bytes are content-addressed. A failed validate, migration, or dependency-ready step keeps the previous generation.
  • App and Runtime sessions bind a generation and are revoked on disable, replace, expiry, or uninstall.
  • Dispose listeners, timers, child processes, MessagePorts, and temp files. onDispose has a deadline; the Host force-kills after it. Persist state during normal operations.
  • Editable files use bridge.artifact. The Host holds the canonical path. Save carries the expected revision. Conflict shows both versions.
  • Pin or document external editors, Runtimes, executables, and network endpoints. Offline behavior and data flow belong in the README.
  • Validate messages from third-party frames before they touch user data. Licenses and NOTICE live in the package.
  • Logs, traces, error details, events, and URLs omit secrets, pairing codes, and device tokens. Worker stderr is scoped diagnostics.
  • The permissions array exists so older v4 packages still validate. New packages omit per-capability grant simulation.

Full Trust network access uses the language runtime (Node fetch, Python fetch_url, and equivalents). The network.fetch Host RPC returns network_denied. The Isolated OS sandbox denies socket / connect. Until a workspace root is bound to the Worker, files.read / files.write / files.stat / files.list return files_root_denied. Until a conversation is bound, conversation.read.get and conversation.append.enqueueInput return conversation_scope_denied.

Release checklist

  • Import only the public SDK modules for that language: @vibex/plugin-sdk, /worker, /app, /testing, /protocol, /stdio; Python vibex-plugin; Rust vibex-plugin-sdk.
  • README: requirements, operation, offline and network, troubleshooting, licenses, config retention on uninstall.
  • A Node package dist/worker.mjs contains the stdio loop. A native package worker path points at a compiled binary.
  • sha256 from pack is reproducible.
  • The changelog states the effect on already-enabled conversations: old sessions keep the tool list from creation; new or rebound sessions receive the new list.

Marketplace submissions include the pack, digest, test log, and a linked-install recording. Identity is Publisher + ID.

SDK overview

The Plugin SDK is versioned with the Host. Worker protocol is 1.1. App protocol is 1.0. API version 1.0. Current matrix: Host 0.1.3, SDK 1.0.0.

Pick an implementation from the Worker runtime:

Runtime Language Package Source Template Host launch
node TypeScript @vibex/plugin-sdk packages/plugin-sdk ts-worker node --max-old-space-size=128 dist/worker.mjs
node JavaScript @vibex/plugin-sdk packages/plugin-sdk node-worker same
python Python vibex-plugin sdk/python python-worker Host-locked CPython 3.12.11 plus runtime/worker.py
native Rust vibex-plugin-sdk crates/plugin-sdk rust-worker spawn the binary at entrypoints.worker.path

All four share the stdio JSON-line protocol, handler rules, generation semantics, and Host capability names. App surfaces ship definePluginApp in the TypeScript / JavaScript SDK. Python and Rust SDKs cover Worker.

Language chapters:

bash
node packages/plugin-cli/dist/cli.js toolchain

The command prints local cli, contract, js, python, rust paths and templates. The contract file list is the required array from locate_toolchain.py; see Development workflow.

Transport

Each Worker has one stdio connection, one JSON object per line. Frame limit 1 MiB. Default request timeout 30 seconds. Cancellation is an explicit notification. stderr is scoped diagnostics.

Handshake:

  1. Host sends initialize with protocolRange: ["1.1"], hostVersion, pluginIdentity, packageDigest, generationId, declaredContributions, packageClass, and limits (maxFrameBytes, requestTimeoutMs).
  2. Worker replies protocolVersion: "1.1", sdkVersion, registrations: [], requestedFeatures.
  3. Host sends activate with pluginId, pluginVersion, generation, packageClass, grantedCapabilities.
  4. Worker runs setup, freezes the handler table, and replies with the registered handler list.
  5. Later messages are invoke / ping / dispose. Worker-to-Host uses host.call with capability, operation, input.

protocolVersion must be 1.1; otherwise the Host returns worker_protocol_unsupported.

Registration

Setup only registers handlers, then freezes. Duplicate ids, undeclared ids, or missing required handlers fail candidate activation. Handles from an old generation become stale after a new generation publishes. Further calls return a stable error. The Host starts a Runtime when a contribution needs it.

Handler regex: Contribution model.

Host capabilities

environment.host.call(capability, operation, input?) is the only Host RPC. Current Host behavior (crates/plugins/src/host_capability_broker.rs):

capability.operation Result
runtime.execute plus any operation Spawn the locked Runtime, JSON on stdin, 120s timeout, 1 MiB input cap
artifact.preview plus any operation Open preview. input needs a Host-issued artifactHandle (~30s, single use) and providerId
artifact.readText / artifact.writeText artifact_not_found. Text I/O is on the artifact.editor App bridge
storage.kv.get / put / delete / list In-process map isolated by plugin ID; cleared when the process exits
storage.settings.get / put Echo current input. Persist settings in root config.json
log.debug / info / warn / error Empty object; scoped diagnostics
plugin.self.doctor { pluginId, generation, diagnostics, recentCrashes }
secrets.get / put / delete { "present": false }
network.fetch network_denied. Full Trust uses the language runtime
files.read / write / stat / list files_root_denied
conversation.read.get / conversation.append.enqueueInput conversation_scope_denied
agent.invoke handler_not_visible
events.subscribe / ack Placeholder success
app.notify.toast Empty object
other capability_unimplemented

Isolated

Isolated execution is declared by a v5 manifest; see Plugin architecture. Author APIs match Full Trust. Python Isolated Workers import define_plugin_worker from vibex_plugin.isolated. Rust Isolated builds use --no-default-features --features isolated. The Host locks Isolated and managed interpreters to CPython 3.12.11 (python-build-standalone).

TypeScript SDK

Package @vibex/plugin-sdk, Node >=20. Template ts-worker writes the Worker definition to runtime/main.ts and re-exports it from runtime/main.mjs. Engine field pluginSdk is ^1.0.0. Templates full, file-tab, and host.service use the same npm package with .mjs sources.

Editable file tabs, detail panels, and host.service consume this package's Worker, App, and testing modules.

Modules

Export Role
@vibex/plugin-sdk / /protocol VIBEX_PLUGIN_API_VERSION ("1.0"), VIBEX_PLUGIN_PROTOCOL_VERSION ("1.1"), JSON and context types
/worker definePluginWorker, activatePluginWorker, PluginSdkError
/app definePluginApp, VibeXAppBridge
/stdio runStdioPluginWorker
/testing createWorkerHarness, createGenerationHarness, createAppHarness

The public SDK exports the modules above. Tauri commands, Axum routes, SQLite schema, and absolute Host paths stay in the Host. Full Trust Workers may use the Node standard library. Structured Runtime / Artifact lifecycle uses environment.host.call.

Worker

ts
import { definePluginWorker } from '@vibex/plugin-sdk/worker';

export default definePluginWorker((registrar, environment) => {
  registrar.handle('hello', async (input, env) => {
    env.log.info('hello', { input });
    return { ok: true };
  });
  registrar.onDispose({
    dispose() {
      environment.log.info('disposed');
    },
  });
});

registrar.handle(id, handler): id must match the handler regex and appear in the manifest. handler(input, environment) takes JSON and returns JSON or a Promise. Duplicate registration throws handler_duplicate.

registrar.onDispose(disposable) accepts { dispose() } or a function. Unload runs them in reverse.

environment:

Field Meaning
context.pluginId Plugin ID
context.pluginVersion Version
context.generation Current activation generation
context.packageClass full-trust or isolated
context.grantedCapabilities Usually ["*"] under Full Trust
host.call(capability, operation, input?) Host RPC
signal AbortSignal; aborted on dispose
log.debug/info/warn/error(message, fields?) Structured log

activatePluginWorker(definition, environment) is for tests or self-hosting. A apiVersion other than "1.0" throws sdk_incompatible. Invoke after dispose throws worker_disposed. A missing handler throws handler_not_found.

stdio entry

The Host runs node --max-old-space-size=128 dist/worker.mjs. Split definition and entry:

ts
// runtime/worker.ts
export default definePluginWorker((registrar) => {
  registrar.handle('hello', async () => ({ ok: true }));
});
js
// runtime/main.mjs
import { runStdioPluginWorker } from '@vibex/plugin-sdk/stdio';
import definition from './worker.ts';

await runStdioPluginWorker(definition);

init --template ts-worker writes the definition in runtime/main.ts and export { default } from "./main.ts" in runtime/main.mjs. Add runStdioPluginWorker in main.mjs, or split as above. build emits runtime/main.mjs to dist/worker.mjs.

Tests import the definition module:

ts
import definition from '../runtime/worker.ts';

App

ts
import { definePluginApp } from '@vibex/plugin-sdk/app';

export default definePluginApp(({ bridge, root, signal }) => {
  const button = document.createElement('button');
  button.textContent = 'Refresh';
  button.addEventListener('click', () => {
    void bridge.invoke('dashboard.refresh', {});
  });
  root.replaceChildren(button);
  bridge.ready();
  const dispose = () => root.replaceChildren();
  signal.addEventListener('abort', dispose, { once: true });
  return dispose;
});

bridge: pluginId, generation, invoke(handler, input?), subscribe(channel, listener) (returns unsubscribe), ready(). An artifact.editor mount also has artifact.

bridge.artifact: name is the file name; readText() returns { name, content, revision }; writeText(content, expectedRevision) writes by revision. An external edit yields a recoverable conflict with code artifact_revision_conflict. The Host gives the App the file name, revision, and this bridge. Theme and locale arrive in Host bootstrap.

Editable file tab:

  1. file.opener declares extensions and editorSurface.
  2. **app.surface uses slot**: artifact.editor, appEntrypoint: "app", handler: "surface.createSession".
  3. The Worker registers that handler.
  4. The App calls readText(), keeps the revision, and passes it on save.

Testing

ts
import { createWorkerHarness, createGenerationHarness } from '@vibex/plugin-sdk/testing';

const worker = await createWorkerHarness(definition, {
  context: { pluginId: 'you.notes', pluginVersion: '0.1.0', generation: 1 },
});
await worker.invoke('hello', {});
// worker.hostCalls records host.call
await worker.dispose();

const gen = await createGenerationHarness(definition, {
  requiredHandlers: ['hello'],
});
await gen.activateCandidate(definition);
await gen.dispose();

createAppHarness(definition, { root, artifact }) simulates the bridge, subscriptions, revoke, and artifact revision conflicts. A missing required handler makes activateCandidate dispose the candidate and throw required_handler_missing.

Record "@vibex/plugin-sdk": "^1.0.0" in package.json. Until the SDK is on npm, use a file: path to the Host checkout or the Host-family sdk/. When developing VibeX itself, run pnpm --filter @vibex/plugin-sdk build first.

JavaScript SDK

JavaScript Workers share npm package @vibex/plugin-sdk, protocol 1.1, and the same handler rules as TypeScript. Template node-worker writes runtime/main.mjs and "type": "module" in package.json. Engine fields, digest, and generation match the TypeScript package.

Type imports may be omitted. Runtime is ESM. Node >=20.

Worker

js
import { definePluginWorker } from '@vibex/plugin-sdk/worker';

export default definePluginWorker((registrar, environment) => {
  registrar.handle('hello', async (input, env) => {
    env.log.info('hello', { input });
    return { message: 'Hello from VibeX', input };
  });
});

definePluginWorker, registrar.handle, onDispose, environment.host.call, log, and signal follow TypeScript SDK. Error codes match: handler_duplicate, handler_not_found, worker_disposed, sdk_incompatible.

stdio entry

The Host runs node --max-old-space-size=128 dist/worker.mjs. Recommended split:

js
// runtime/worker.mjs — handler definition
export default definePluginWorker((registrar) => {
  registrar.handle('hello', async (input) => ({ message: 'Hello from VibeX', input }));
});
js
// runtime/main.mjs — Host entry
import { runStdioPluginWorker } from '@vibex/plugin-sdk/stdio';
import definition from './worker.mjs';

await runStdioPluginWorker(definition);

vibex-plugin build emits runtime/main.mjs to dist/worker.mjs. Manifest:

json
"entrypoints": {
  "worker": {
    "path": "dist/worker.mjs",
    "runtime": "node",
    "protocol": "1.1"
  }
}

init --template node-worker writes the definition in runtime/main.mjs. Add runStdioPluginWorker there, or split into worker.mjs and main.mjs. Official Office uses the split.

Tests import runtime/worker.mjs. Importing dist/worker.mjs starts the stdio loop.

App

JavaScript Apps use the same definePluginApp. full and file-tab templates emit runtime/app.mjs, app.html, and app.css. bridge.invoke reaches only Worker handlers registered in this generation. Call bridge.ready() after the first paint is mounted.

Without a TypeScript compiler, keep .mjs sources. For types, use ts-worker or add .d.ts in the same package; runtime remains Node ESM.

The file-tab template App mounts on plugin.detail.panel. An editable file tab uses slot: artifact.editor. Declaration steps: Contribution model.

Testing

js
import test from 'node:test';
import assert from 'node:assert/strict';
import definition from '../runtime/worker.mjs';
import { createWorkerHarness } from '@vibex/plugin-sdk/testing';

test('registers hello', async () => {
  const worker = await createWorkerHarness(definition);
  assert.deepEqual(worker.handlers, ['hello']);
  await assert.rejects(() => worker.invoke('undeclared', null), /not registered/);
  await worker.dispose();
});

init --template node-worker generates a test that imports ../dist/worker.mjs. After splitting sources, change the import to ../runtime/worker.mjs. vibex-plugin test runs build first. The host.service template also uses a JavaScript Worker plus intervalSeconds.

Using TypeScript in the same package

JavaScript templates fit a pure Worker with ESM sources. ts-worker writes the definition in runtime/main.ts. One product package may contain a .mjs Worker and a TypeScript App when build output paths match the manifest.

Python SDK

Package name vibex-plugin, source sdk/python. Author environments and the Host Isolated interpreter require CPython 3.12 or newer. The Host locks CPython 3.12.11 (python-build-standalone install_only). Template python-worker writes runtime/worker.py and pyproject.toml. entrypoints.worker.runtime is python, protocol 1.1.

The Host launches the locked CPython executable plus entrypoints.worker.path (default runtime/worker.py). That file must call run_stdio_plugin_worker under __main__.

toml
[project]
name = "my-plugin"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["vibex-plugin>=1.0.0"]

[tool.vibex.plugin]
worker = "runtime/worker.py"

Constants: PLUGIN_API_VERSION = "1.0", PLUGIN_PROTOCOL_VERSION = "1.1", PLUGIN_SDK_VERSION = "1.0.0".

Worker

python
from vibex_plugin import define_plugin_worker, run_stdio_plugin_worker


def setup(registrar, environment):
    def hello(value, env):
        env.log.info("hello", {"input": value})
        return {"ok": True, "input": value}

    registrar.handle("hello", hello)


if __name__ == "__main__":
    run_stdio_plugin_worker(define_plugin_worker(setup))

The async entry is run_stdio_plugin_worker_async. define_plugin_worker(setup) accepts a synchronous setup(registrar, environment). A handler may be a plain function or a coroutine; the SDK uses inspect to decide whether to await.

Handler id regex matches TypeScript. Duplicate registrar.handle(id, fn) throws PluginSdkError("handler_duplicate"). registrar.on_dispose(fn) runs in reverse.

environment.context is an attribute dict: plugin_id, plugin_version, generation, package_class, granted_capabilities. Underlying keys are camelCase (pluginId and the rest), aligned with the protocol.

environment.host.call(capability, operation, input=None) is async. Use an async handler, or put I/O in Full Trust helpers.

environment.log provides debug / info / warn / error. environment carries a cancellation flag and aborts on dispose.

activate_plugin_worker is for tests. Error type: PluginSdkError(code, message, details=None).

Full Trust local I/O

HostClient.call is the only Host RPC. files.* returns files_root_denied until a workspace root is bound. Full Trust also ships local helpers:

python
from vibex_plugin import fetch_url, read_local_file, write_local_file

raw = read_local_file("/path/on/host")
write_local_file("/path/on/host", "text")
result = fetch_url("https://example.com", method="GET", timeout=30.0)

Isolated Worker:

python
from vibex_plugin.isolated import define_plugin_worker

Under Isolated builds, the OS sandbox denies filesystem, network, and subprocess. Manifest version and packageClass: Plugin architecture.

Testing

python
from vibex_plugin import (
    create_worker_harness,
    create_generation_harness,
    MemoryHostClient,
    define_plugin_worker,
)


async def test_hello():
    worker = await create_worker_harness(define_plugin_worker(setup))
    result = await worker.invoke("hello", {"n": 1})
    await worker.dispose()

create_worker_harness, invoke, and dispose are coroutines and must be awaited. MemoryHostClient records call. create_generation_harness checks candidate switches. In-package tests live under sdk/python/tests/: stdio, protocol fixtures, worker, testing.

init --template python-worker default tests check manifestVersion on plugin.json. Authors add business handler tests.

Rust SDK

Crate name vibex-plugin-sdk, path crates/plugin-sdk. MSRV 1.85. stdio uses a tokio current-thread runtime. publish = false in Cargo.toml; plugins depend via path or the crate bundled in the Host family. Template rust-worker writes runtime/Cargo.toml and runtime/src/main.rs. entrypoints.worker.runtime is native, protocol 1.1.

toml
[package]
name = "my-plugin-worker"
version = "0.1.0"
edition = "2021"
rust-version = "1.85"

[dependencies]
serde_json = "1"
vibex-plugin-sdk = { path = "../../../crates/plugin-sdk" }

Constants PLUGIN_API_VERSION, PLUGIN_PROTOCOL_VERSION, and PLUGIN_SDK_VERSION match JS and Python.

Worker

rust
use serde_json::json;
use vibex_plugin_sdk::{define_plugin_worker, run_stdio_plugin_worker_blocking};

fn main() {
    let definition = define_plugin_worker(|registrar, _env| {
        registrar.handle_sync("hello", |input, _env| {
            Ok(json!({
                "message": "Hello from VibeX",
                "input": input,
            }))
        });
        registrar.on_dispose(|| async { Ok(()) });
    });
    if let Err(error) = run_stdio_plugin_worker_blocking(definition) {
        eprintln!("{error}");
        std::process::exit(1);
    }
}

PluginRegistrar:

  • handle(id, async_fn): async handler, Result<Value, PluginSdkError>
  • handle_sync(id, fn): sync wrapper
  • on_dispose(async_fn): cleanup, reverse order

WorkerEnv: context() / replace_context(), host (HostClient), log.debug/info/warn/error, is_cancelled() / cancel(), async call(capability, operation, input).

run_stdio_plugin_worker is async. run_stdio_plugin_worker_blocking is for main. The panic hook writes protocol error worker_panic then dispose.

PluginSdkError is the error type. Handler regex matches the other languages. hello_plugin_worker is an in-crate sample used by protocol fixtures.

Compile and manifest path

The Host spawns entrypoints.worker.path as an executable: Command::new(path), working directory the package root. That path must be a compiled binary.

Sequence:

  1. Run cargo build --release inside runtime/ on the same OS / CPU triple as the Host.
  2. Copy the artifact to dist/, for example dist/my-plugin-worker.
  3. Set the manifest to "path": "dist/my-plugin-worker", "runtime": "native", "protocol": "1.1".
  4. vibex-plugin validate / pack.

vibex-plugin build compiles JavaScript Workers (runtime/main.mjsdist/worker.mjs) and managed MCP sources. Authors supply the native binary. init --template rust-worker writes path runtime/src/main.rs; change it to the compiled binary before release.

Isolated

bash
cargo build --release --no-default-features --features isolated

Default feature std includes filesystem and network helpers. The Isolated feature omits those helpers and relies on Host spawn plus the OS sandbox. define_plugin_worker stays the same. v5 manifest fields: Plugin architecture.

Testing

The crate exports create_worker_harness, create_generation_harness, and MemoryHost. WorkerHarness::invoke is async. dispose runs on_dispose in reverse.

bash
cargo test -p vibex-plugin-sdk

Protocol fixtures read packages/plugin-contract/fixtures/protocol/*.jsonl, shared across the three language SDKs. Plugin-package tests are author-maintained. init --template rust-worker ships a Node test that checks the manifest; authors add Rust-side tests.

Platform architecture

This track modifies VibeX source: new Host capabilities, Application Core, the desktop shell, or the remote protocol. Plugins consume those capabilities as described in Plugin architecture.

Before work, read root CONTEXT.md, the ADRs in docs/adr/ that apply, and apply maiden-skill in full. Then load the smallest set of SKILL.md files matched by the Agent Skill rules in CONTEXT.md. Cross-layer changes load every matching skill together. A new Tauri command also covers IPC, frontend integration, and tests.

Layout

The repo is a pnpm workspace plus a Cargo workspace. The Rust toolchain is pinned in rust-toolchain.toml (currently nightly-2025-12-04).

Path Role
frontend/src/ React + TypeScript UI. @frontend/src, sharedshared/
src-tauri/ Desktop shell, invoke_handler, windows, AppState
crates/ Tauri-agnostic domain logic
shared/types.ts TS types from generate_types.rs. Generated; refresh with pnpm run generate-types
packages/plugin-sdk, packages/plugin-cli Public plugin contract
docs/adr/ Architecture decisions
assets/plugins/ Official bundled plugin sources

Three process layers

  1. The frontend talks to the backend only through invoke and event subscriptions.
  2. The Tauri shell registers commands and holds AppState. Commands live under src-tauri/src/commands/ by domain.
  3. crates/* implement the business. Services are injected through the Deployment trait. The desktop concrete type is LocalDeployment.

Agent subsystem

New agent, conversation, and turn work goes through crates/agents (AgentRuntime, AgentConnectionManager) and the event-sourced conversation core. The CLI-executor path has been removed. ExecutorActionType keeps only ScriptRequest. crates/executors still owns script execution, the executor config schema, and log normalization. Agent execution stays in crates/agents.

Conversation events append to conversation_event and project into the timeline. The frontend renders AgentTimelineConversation only. A conversation has at most one in-flight turn. See Conversation and Turn state machine.

Application Core and remote

Desktop commands, Web routes, and the remote-desktop adapter authenticate and map DTOs/errors, then call the same Application Core. The frontend uses BackendTransport: TauriTransport locally, WebTransport in the browser and on a workstation. One window binds one Host. One data directory has one Host occupant at a time. See Application Core.

Maiden principles

User-visible completeness outranks keeping a wrong abstraction. Defects are fixed at the origin. Names state behavior. Comments record why a decision exists. Backup files, commented experiments, and incorrect intermediate states are removed. Local source and deployed source are the same artifact. UI copy helps act, decide, understand state, recover, or judge a consequence.

Build environment

Environment

  • Node 22, pnpm 10.x (CI uses pnpm 10.13.1).
  • Rust nightly, see rust-toolchain.toml. The first cargo install follows that file.
  • SQLx CLI: cargo install sqlx-cli --no-default-features --features sqlite. After query changes run pnpm run prepare-db.
  • Optional cargo install cargo-watch.
  • Secrets stay in a local .env. .dev-ports.json and generated Tauri dev config are local runtime artifacts.

Startup installs a single rustls crypto provider (install_rustls_crypto_provider). reqwest is built in no-provider mode. Construct TLS clients after that function runs.

Dev ports are allocated dynamically into .dev-ports.json. scripts/run-tauri-dev-desktop.js writes src-tauri/tauri.dev.generated.conf.json per run.

Commands

From the repository root:

bash
pnpm install                 # JS deps; required before any pnpm script
pnpm run dev                 # Tauri desktop + Vite HMR
pnpm run check               # frontend tsc --noEmit + cargo check
pnpm run lint                # eslint max-warnings 0; clippy -D warnings --features qa-mode
pnpm run format              # cargo fmt --all + prettier

Frontend (frontend/ or pnpm --filter ./frontend):

bash
pnpm test
pnpm exec vitest run src/path/file.test.ts
pnpm exec vitest run -t "renders tool card"
pnpm run check
pnpm run lint

Backend:

bash
cargo test --workspace
cargo test -p agents
cargo test -p agents acp_session_resume
cargo clippy --workspace --all-targets --features qa-mode -- -D warnings

Codegen. Re-run when inputs change. CI fails :check on stale artifacts:

bash
pnpm run generate-types
pnpm run generate-types:check
pnpm run prepare-db
pnpm run prepare-db:check

generate-types runs with SQLX_OFFLINE=true. The generator merges: it keeps declarations outside its replacement list, replaces replacement_declarations(), and drops removed_declarations(). To export a new #[derive(TS)] type, add insert_declaration::<T>() in src-tauri/src/bin/generate_types.rs, then generate.

  1. Read CONTEXT.md, relevant ADRs, maiden-skill, and directly matching SKILL.md files.
  2. Isolate the branch with a git worktree (using-git-worktrees skill in-repo).
  3. For a behavior change, write a failing test first (tdd skill), then the smallest implementation.
  4. Run targeted tests, then the matching check / lint.
  5. After type, SQL, or agent-schema changes, run the matching generate/prepare.
  6. pnpm run format.
  7. Open a PR per PR and security.

Engineering conventions

Frontend

Prettier: 2 spaces, semicolons, single quotes, ES5 trailing commas, 80 columns. ESLint forbids unused imports and requires exhaustive switches. React component files are PascalCase .tsx. Hooks start with use. Utilities and config are camelCase.

Visual design follows root DESIGN.md: macOS Tahoe target. Liquid Glass is reserved for navigation and control chrome. Content surfaces stay opaque. Every route is wrapped in LegacyDesignScope (historical name; treat it as the active design scope). Tokens live in frontend/src/styles/legacy/index.css. Tailwind config is tailwind.legacy.config.js. Use role classes such as --surface-*, --text-*, .settings-surface. Product color uses tokens and role classes. Radii go through --radius (14px).

Visible copy helps act, decide, understand state, recover from an error, or judge a consequence. UI copy omits implementation notes such as “settings live in settings.json”.

Rust

Edition 2024, rustfmt.toml. Grouped imports. CI clippy runs --features qa-mode -- -D warnings. Local pnpm run lint enables qa-mode (including the QaMock executor) the same way.

Domain logic belongs in a crate and is reached through Deployment. Command handlers stay thin. New conversation capability goes in crates/agents. Scripts, config schema, and normalized logs stay in crates/executors.

Generated artifacts

These files are generated; refresh them with the matching command before commit:

  • shared/types.ts
  • crates/db/.sqlx offline query cache
  • src-tauri/tauri.dev.generated.conf.json
  • built packages/*/dist

After a SQL query! / query_as! or a migration, run pnpm run prepare-db. After exporting a #[derive(TS)] type, run pnpm run generate-types. Name those generated files in the PR.

Module boundaries

shared/types.ts is the frontend/backend contract. The frontend talks through invoke and shared/types.ts. Plugins import the public SDK only. Official bundled plugins are reference packages on that same contract. A Host special case keyed by plugin ID is debt to delete.

Test strategy

A behavior change or regression fix starts with a failing test, then the smallest implementation. Tests ship with the change.

Frontend

Unit tests sit beside sources: *.test.ts, *.test.tsx, *.spec.ts, *.spec.tsx. Vitest + jsdom. IPC is mocked in unit tests. Broader regressions live in frontend/tests/. E2E lives in frontend/tests-e2e/, chosen by target-platform feasibility.

bash
cd frontend
pnpm exec vitest run src/pages/settings/AgentSettings.test.tsx

UI changes (layout, style, routing, client state) are verified with real interaction before merge: click, type, submit, navigate; visit every route that shares the state; cover empty and error states; check desktop and narrow viewports for layout work. Without browser tools, use unit tests, the dev server, or a render script, and state in the PR what was left unverified.

Rust

In-crate src unit tests and tests/ integration tests. Prefer:

bash
cargo test -p agents acp_session_resume
cargo test -p plugins bundled_office

Then cargo test --workspace when the blast radius warrants it. SQLx tests follow offline cache or the test-database convention. Keep SQLX_OFFLINE aligned with CI.

Plugin contract

Changes to packages/plugin-sdk or plugin-cli run that package’s pnpm test and build. Changes to Host parsing or the contribution registry add crate tests and at least one real linked-install path (official Office or the workflow-creator fixture). Reference packages must keep using only the public SDK.

CI

.github/workflows/test.yml runs on pull_request and push to master. It includes dependency licenses and advisories, frontend checks, Rust clippy (qa-mode), and tests. generate-types:check and prepare-db:check fail on stale artifacts. Run the checks you touched before push.

Review and security

Commits

History uses Conventional Commits: feat:, fix(scope):, chore(scope):, docs(scope):, plus explicit merge commits. One commit, one change. Titles are imperative.

Pull request

The description includes:

  • A short summary of the user-visible result.
  • Linked issue, PRD, or ADR.
  • Test commands and results.
  • Screenshots or recordings for visible UI.
  • Generated files: shared/types.ts, .sqlx, plugin locks.
  • UI paths left unverified in a browser, if any.

Keep the diff small. Split refactors from features. Agent refactors finish on the ACP path.

Security

  • Secrets, tokens, and pairing codes live in a local .env, the OS keychain, or the Host token store. They live only in those stores.
  • Error envelopes on the remote protocol and plugin Workers strip secrets. Main token and device token travel in protected headers or the keychain.
  • Plugin packages are Full Trust. Official plugins merged into the Host, and APIs merged into the SDK, run at local rights. A new Host capability needs a schema, tests, and docs before plugins may call it.
  • Public Host exposure terminates TLS on a reverse proxy. Cross-origin allow lists use exact Origins.
  • CI runs pnpm audit --prod --audit-level high and rustsec/audit-check. Licenses: pnpm run dependency:licenses.
  • Crash reports stay in the local data directory by default. Content leaves the machine when the user chooses Submit on GitHub.
  • Docs and UI use placeholders. Samples use placeholders.

Review axes

Code review checks standards and spec together. Over-engineering review deletes reinvented stdlib, speculative abstraction, and flexibility with no caller. Correctness review covers failure paths, occupancy, event sequence, exclusive permission resolution, and freshness of generated artifacts.