Skip to content

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:

typescript
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.

typescript
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

typescript
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.channelId

BusMessage shape

typescript
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/*.ts loader was retired in ADR-0005. First-party plugins now live in lib/plugins/ and are statically imported + compiled into the image (register them in src/index.ts). For runtime, no-rebuild extension use an A2A agent or MCP server via the control plane instead.

typescript
// 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)

protoWorkstacean — a switchboard, not an agent.