VibeX

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.