This is a reference doc. It covers the Plugin interface contract and how to write workspace bus plugins.
See also: explanation/plugin-lifecycle.md for how plugins register, subscribe, and reload.
Plugin interface
All workspace bus plugins must implement:
interface Plugin {
readonly name: string;
readonly description: string;
readonly capabilities: string[];
install(bus: EventBus): void;
uninstall(): void;
}install(bus: EventBus)
Called once on container startup after the bus is created. Subscribe to topics and set up HTTP servers here.
install(bus: EventBus): void {
bus.subscribe("message.inbound.#", this.name, (msg: BusMessage) => {
// handle message
});
}uninstall()
Called on graceful shutdown. Clean up timers, close HTTP servers, release resources.
EventBus interface
interface EventBus {
subscribe(topic: string, subscriberId: string, handler: (msg: BusMessage) => void): void;
publish(topic: string, message: BusMessage): void;
unsubscribe(subscriberId: string): void;
}Topic patterns use # as a wildcard matching any number of segments (MQTT-style):
message.inbound.# matches message.inbound.github.owner.repo.event.123
message.inbound.discord.# matches message.inbound.discord.channelIdBusMessage shape
interface BusMessage {
id: string; // UUID
topic: string; // the topic this message was published on
timestamp: number; // Unix milliseconds
correlationId?: string; // links request → response → chain
payload: {
sender?: string;
content?: string;
channel?: string;
skillHint?: string;
reply?: string;
[key: string]: unknown;
};
reply?: string; // outbound topic for responses
source?: {
interface: string; // "discord" | "github" | "linear" | "cron" | ...
channelId: string;
userId: string;
};
}Writing a first-party bus plugin
The dynamic
workspace/plugins/*.tsloader was retired in ADR-0005. First-party plugins now live inlib/plugins/and are statically imported + compiled into the image (register them insrc/index.ts). For runtime, no-rebuild extension use an A2A agent or MCP server via the control plane instead.
// lib/plugins/my-plugin.ts — register it in src/index.ts's plugin registry
import type { Plugin, EventBus, BusMessage } from "../types";
export default {
name: "my-plugin",
description: "Does something useful on the bus",
capabilities: ["custom"],
install(bus: EventBus) {
bus.subscribe("message.inbound.#", "my-plugin", (msg: BusMessage) => {
bus.publish("message.outbound.custom", {
id: msg.id,
topic: "message.outbound.custom",
timestamp: Date.now(),
payload: { content: "Custom response" },
});
});
},
uninstall() {},
} satisfies Plugin;Capabilities
- Subscribe to any bus topic
- Publish messages to any topic
- Bridge external services (APIs, webhooks, databases)
- Transform or route messages between channels
- Implement custom command handlers
Limitations
- Cannot modify the agent's system prompt directly
- Cannot intercept tool calls at the LLM layer
- Compiled-in: adding/changing one requires an image build + deploy (runtime hot-swap is for A2A agents and MCP servers, not in-process code)