# Phase A Appendices — Source File Bodies

These are the complete contents of the larger files in the Phase A plan. Copy them verbatim into the target files indicated in each task.

---

## Appendix A — `packages/plugin-ui-bridge/src/index.test.ts`

```ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import {
  PluginBridge,
  HostBridge,
  type HostToPlugin,
  type PluginToHost,
} from "./index.js";

class MockWindow {
  listeners: Array<(e: MessageEvent) => void> = [];
  posted: Array<{ data: unknown; origin: string }> = [];
  addEventListener = vi.fn((_: string, fn: (e: MessageEvent) => void) => {
    this.listeners.push(fn);
  });
  removeEventListener = vi.fn();
  postMessage = vi.fn((data: unknown, targetOrigin: string) => {
    this.posted.push({ data, origin: targetOrigin });
  });
  emit(data: unknown, origin: string) {
    const ev = { data, origin } as MessageEvent;
    for (const l of this.listeners) l(ev);
  }
}

describe("PluginBridge", () => {
  let pluginWin: MockWindow;
  let parentWin: MockWindow;
  let bridge: PluginBridge;

  beforeEach(() => {
    pluginWin = new MockWindow();
    parentWin = new MockWindow();
    bridge = new PluginBridge({
      self: pluginWin as unknown as Window,
      parent: parentWin as unknown as Window,
      expectedHostOrigin: "https://hub.example.com",
    });
  });

  it("sends plugin:ready to parent on notifyReady()", () => {
    bridge.notifyReady();
    expect(parentWin.posted).toHaveLength(1);
    expect(parentWin.posted[0]?.data).toEqual({ type: "plugin:ready" });
    expect(parentWin.posted[0]?.origin).toBe("https://hub.example.com");
  });

  it("invokes onHello when host:hello arrives from expected origin", () => {
    const onHello = vi.fn();
    bridge.onHello(onHello);
    pluginWin.emit(
      {
        type: "host:hello",
        theme: "dark",
        tokens: { foo: "bar" },
        gatewayUrl: "wss://gw",
        authToken: "tok",
      } satisfies HostToPlugin,
      "https://hub.example.com",
    );
    expect(onHello).toHaveBeenCalledOnce();
  });

  it("ignores host:hello from wrong origin", () => {
    const onHello = vi.fn();
    bridge.onHello(onHello);
    pluginWin.emit(
      {
        type: "host:hello",
        theme: "dark",
        tokens: {},
        gatewayUrl: "wss://gw",
        authToken: "tok",
      } satisfies HostToPlugin,
      "https://evil.example.com",
    );
    expect(onHello).not.toHaveBeenCalled();
  });

  it("ignores malformed payloads", () => {
    const onHello = vi.fn();
    bridge.onHello(onHello);
    pluginWin.emit({ type: "garbage" }, "https://hub.example.com");
    pluginWin.emit(null, "https://hub.example.com");
    pluginWin.emit("string", "https://hub.example.com");
    expect(onHello).not.toHaveBeenCalled();
  });

  it("notifyResize emits plugin:resize", () => {
    bridge.notifyResize(420);
    expect(parentWin.posted[0]?.data).toEqual({ type: "plugin:resize", height: 420 });
  });
});

describe("HostBridge", () => {
  let hostWin: MockWindow;
  let iframeWin: MockWindow;
  let bridge: HostBridge;

  beforeEach(() => {
    hostWin = new MockWindow();
    iframeWin = new MockWindow();
    bridge = new HostBridge({
      self: hostWin as unknown as Window,
      target: iframeWin as unknown as Window,
      pluginOrigin: "https://gateway.example.com",
    });
  });

  it("sends host:hello when plugin:ready arrives from plugin origin", () => {
    bridge.sendHelloOnReady({
      theme: "light",
      tokens: { bg: "#fff" },
      gatewayUrl: "wss://gw",
      authToken: "tok",
    });
    hostWin.emit({ type: "plugin:ready" } satisfies PluginToHost, "https://gateway.example.com");
    expect(iframeWin.posted).toHaveLength(1);
    expect((iframeWin.posted[0]?.data as HostToPlugin).type).toBe("host:hello");
  });

  it("does NOT send host:hello if plugin:ready comes from wrong origin", () => {
    bridge.sendHelloOnReady({ theme: "light", tokens: {}, gatewayUrl: "wss://gw", authToken: "tok" });
    hostWin.emit({ type: "plugin:ready" } satisfies PluginToHost, "https://evil.example.com");
    expect(iframeWin.posted).toHaveLength(0);
  });

  it("invokes onResize for plugin:resize", () => {
    const onResize = vi.fn();
    bridge.onResize(onResize);
    hostWin.emit({ type: "plugin:resize", height: 600 } satisfies PluginToHost, "https://gateway.example.com");
    expect(onResize).toHaveBeenCalledWith(600);
  });

  it("sendThemeChange posts to iframe", () => {
    bridge.sendThemeChange({ theme: "dark", tokens: { bg: "#000" } });
    expect(iframeWin.posted).toHaveLength(1);
    expect((iframeWin.posted[0]?.data as HostToPlugin).type).toBe("host:theme-change");
  });
});
```

---

## Appendix B — `packages/plugin-ui-bridge/src/index.ts` (complete)

```ts
export type Theme = "light" | "dark";

export type HostToPlugin =
  | {
      type: "host:hello";
      theme: Theme;
      tokens: Record<string, string>;
      gatewayUrl: string;
      authToken: string;
    }
  | {
      type: "host:theme-change";
      theme: Theme;
      tokens: Record<string, string>;
    };

export type PluginToHost =
  | { type: "plugin:ready" }
  | { type: "plugin:resize"; height: number }
  | { type: "plugin:notify"; level: "info" | "warn" | "error"; message: string };

function isObject(v: unknown): v is Record<string, unknown> {
  return typeof v === "object" && v !== null;
}

function isHostToPlugin(v: unknown): v is HostToPlugin {
  if (!isObject(v)) return false;
  const t = v.type;
  return t === "host:hello" || t === "host:theme-change";
}

function isPluginToHost(v: unknown): v is PluginToHost {
  if (!isObject(v)) return false;
  const t = v.type;
  return t === "plugin:ready" || t === "plugin:resize" || t === "plugin:notify";
}

export interface PluginBridgeOptions {
  self: Window;
  parent: Window;
  expectedHostOrigin: string;
}

export class PluginBridge {
  private helloHandlers: Array<(msg: Extract<HostToPlugin, { type: "host:hello" }>) => void> = [];
  private themeHandlers: Array<
    (msg: Extract<HostToPlugin, { type: "host:theme-change" }>) => void
  > = [];

  constructor(private opts: PluginBridgeOptions) {
    opts.self.addEventListener("message", this.handle);
  }

  private handle = (ev: MessageEvent): void => {
    if (ev.origin !== this.opts.expectedHostOrigin) return;
    if (!isHostToPlugin(ev.data)) return;
    if (ev.data.type === "host:hello") {
      for (const h of this.helloHandlers) h(ev.data);
    } else if (ev.data.type === "host:theme-change") {
      for (const h of this.themeHandlers) h(ev.data);
    }
  };

  notifyReady(): void {
    const msg: PluginToHost = { type: "plugin:ready" };
    this.opts.parent.postMessage(msg, this.opts.expectedHostOrigin);
  }

  notifyResize(height: number): void {
    const msg: PluginToHost = { type: "plugin:resize", height };
    this.opts.parent.postMessage(msg, this.opts.expectedHostOrigin);
  }

  notify(level: "info" | "warn" | "error", message: string): void {
    const msg: PluginToHost = { type: "plugin:notify", level, message };
    this.opts.parent.postMessage(msg, this.opts.expectedHostOrigin);
  }

  onHello(fn: (msg: Extract<HostToPlugin, { type: "host:hello" }>) => void): void {
    this.helloHandlers.push(fn);
  }

  onThemeChange(fn: (msg: Extract<HostToPlugin, { type: "host:theme-change" }>) => void): void {
    this.themeHandlers.push(fn);
  }

  dispose(): void {
    this.opts.self.removeEventListener("message", this.handle);
    this.helloHandlers = [];
    this.themeHandlers = [];
  }
}

export interface HostBridgeOptions {
  self: Window;
  target: Window;
  pluginOrigin: string;
}

export class HostBridge {
  private pendingHelloPayload: Omit<Extract<HostToPlugin, { type: "host:hello" }>, "type"> | null =
    null;
  private resizeHandlers: Array<(height: number) => void> = [];
  private notifyHandlers: Array<(level: "info" | "warn" | "error", message: string) => void> = [];

  constructor(private opts: HostBridgeOptions) {
    opts.self.addEventListener("message", this.handle);
  }

  private handle = (ev: MessageEvent): void => {
    if (ev.origin !== this.opts.pluginOrigin) return;
    if (!isPluginToHost(ev.data)) return;
    if (ev.data.type === "plugin:ready") {
      if (this.pendingHelloPayload) {
        const msg: HostToPlugin = { type: "host:hello", ...this.pendingHelloPayload };
        this.opts.target.postMessage(msg, this.opts.pluginOrigin);
      }
    } else if (ev.data.type === "plugin:resize") {
      const { height } = ev.data;
      for (const h of this.resizeHandlers) h(height);
    } else if (ev.data.type === "plugin:notify") {
      const { level, message } = ev.data;
      for (const h of this.notifyHandlers) h(level, message);
    }
  };

  sendHelloOnReady(
    payload: Omit<Extract<HostToPlugin, { type: "host:hello" }>, "type">,
  ): void {
    this.pendingHelloPayload = payload;
  }

  sendThemeChange(payload: { theme: Theme; tokens: Record<string, string> }): void {
    const msg: HostToPlugin = { type: "host:theme-change", ...payload };
    this.opts.target.postMessage(msg, this.opts.pluginOrigin);
  }

  onResize(fn: (height: number) => void): void {
    this.resizeHandlers.push(fn);
  }

  onNotify(fn: (level: "info" | "warn" | "error", message: string) => void): void {
    this.notifyHandlers.push(fn);
  }

  dispose(): void {
    this.opts.self.removeEventListener("message", this.handle);
    this.pendingHelloPayload = null;
    this.resizeHandlers = [];
    this.notifyHandlers = [];
  }
}
```

---

## Appendix C — `src/gateway/server-methods/plugins.test.ts` (ui.list cases only — appended in later tasks)

```ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { pluginsUiList } from "./plugins.js";
import type { PluginRegistry } from "../../plugins/registry.js";

function makeRegistry(plugins: Array<{ id: string; enabled: boolean; manifest?: any }>): PluginRegistry {
  return {
    list: () => plugins,
    get: (id: string) => plugins.find((p) => p.id === id),
  } as unknown as PluginRegistry;
}

describe("plugins.ui.list", () => {
  it("returns empty array when no plugin declares ui", async () => {
    const registry = makeRegistry([
      { id: "p1", enabled: true, manifest: { id: "p1" } },
      { id: "p2", enabled: false, manifest: { id: "p2", ui: [{ slot: "settings.plugins", title: "p2", description: "d", entrypoint: "ui/dist/index.html" }] } },
    ]);
    const result = await pluginsUiList({ registry, ctx: { isAdmin: true } });
    expect(result).toEqual([]);
  });

  it("returns one entry per (plugin, ui-entry) for enabled plugins only", async () => {
    const registry = makeRegistry([
      {
        id: "p1",
        enabled: true,
        manifest: {
          id: "p1",
          ui: [{ slot: "settings.plugins", title: "P1", description: "d", entrypoint: "ui/dist/index.html" }],
        },
      },
      {
        id: "p2",
        enabled: false,
        manifest: {
          id: "p2",
          ui: [{ slot: "settings.plugins", title: "P2", description: "d", entrypoint: "ui/dist/index.html" }],
        },
      },
    ]);
    const result = await pluginsUiList({ registry, ctx: { isAdmin: true } });
    expect(result).toHaveLength(1);
    expect(result[0]?.pluginId).toBe("p1");
  });

  it("rejects non-admin caller", async () => {
    const registry = makeRegistry([]);
    await expect(pluginsUiList({ registry, ctx: { isAdmin: false } })).rejects.toThrow(/admin/i);
  });
});
```

---

## Appendix D — `src/gateway/server-methods/plugins.ts` v1 (ui.list only)

```ts
import type { PluginRegistry } from "../../plugins/registry.js";
import type { PluginUiManifestEntry } from "../../plugins/types.js";

export interface PluginsMethodCtx {
  isAdmin: boolean;
}

export interface PluginsUiListEntry {
  pluginId: string;
  slot: string;
  title: string;
  description: string;
  entrypoint: string;
  icon?: string;
}

export async function pluginsUiList(deps: {
  registry: PluginRegistry;
  ctx: PluginsMethodCtx;
}): Promise<PluginsUiListEntry[]> {
  if (!deps.ctx.isAdmin) {
    throw new Error("plugins.ui.list: admin only");
  }
  const out: PluginsUiListEntry[] = [];
  for (const p of deps.registry.list()) {
    if (!p.enabled) continue;
    const ui = (p.manifest as { ui?: PluginUiManifestEntry[] }).ui ?? [];
    for (const entry of ui) {
      out.push({
        pluginId: p.id,
        slot: entry.slot,
        title: entry.title,
        description: entry.description,
        entrypoint: entry.entrypoint,
        icon: entry.icon,
      });
    }
  }
  return out;
}
```

---

## Appendix E — `plugins.test.ts` additional cases for `config.get`

```ts
import { pluginsConfigGet } from "./plugins.js";

describe("plugins.config.get", () => {
  function makeFullRegistry() {
    return makeRegistry([
      {
        id: "p1",
        enabled: true,
        manifest: { id: "p1", configSchema: { type: "object", additionalProperties: false, properties: {} } },
        config: { foo: "bar" },
      },
    ]);
  }

  it("returns config for known plugin", async () => {
    const registry = makeFullRegistry();
    const channels = [{ channel: "telegram", accountId: "default", label: "@snifflebottom_bot" }];
    const agents = [{ id: "panik", name: "PANIK" }];
    const result = await pluginsConfigGet({
      registry,
      channels,
      agents,
      ctx: { isAdmin: true },
      params: { pluginId: "p1" },
    });
    expect(result.config).toEqual({ foo: "bar" });
    expect(result.channels).toEqual(channels);
    expect(result.agents).toEqual(agents);
  });

  it("returns empty config object if plugin has none yet", async () => {
    const registry = makeRegistry([{ id: "p1", enabled: true, manifest: { id: "p1", configSchema: {} } }]);
    const result = await pluginsConfigGet({
      registry,
      channels: [],
      agents: [],
      ctx: { isAdmin: true },
      params: { pluginId: "p1" },
    });
    expect(result.config).toEqual({});
  });

  it("rejects unknown pluginId", async () => {
    const registry = makeRegistry([]);
    await expect(
      pluginsConfigGet({
        registry,
        channels: [],
        agents: [],
        ctx: { isAdmin: true },
        params: { pluginId: "ghost" },
      }),
    ).rejects.toThrow(/not found/i);
  });

  it("rejects non-admin", async () => {
    const registry = makeFullRegistry();
    await expect(
      pluginsConfigGet({
        registry,
        channels: [],
        agents: [],
        ctx: { isAdmin: false },
        params: { pluginId: "p1" },
      }),
    ).rejects.toThrow(/admin/i);
  });
});
```

---

## Appendix F — `plugins.ts` additions for `config.get`

Append to the file:

```ts
export interface PluginsConfigGetParams {
  pluginId: string;
}

export interface PluginsConfigGetResult {
  config: Record<string, unknown>;
  channels: Array<{ channel: string; accountId: string; label?: string }>;
  agents: Array<{ id: string; name?: string }>;
}

export async function pluginsConfigGet(deps: {
  registry: PluginRegistry;
  channels: PluginsConfigGetResult["channels"];
  agents: PluginsConfigGetResult["agents"];
  ctx: PluginsMethodCtx;
  params: PluginsConfigGetParams;
}): Promise<PluginsConfigGetResult> {
  if (!deps.ctx.isAdmin) {
    throw new Error("plugins.config.get: admin only");
  }
  const p = deps.registry.get(deps.params.pluginId);
  if (!p) {
    throw new Error(`plugins.config.get: plugin '${deps.params.pluginId}' not found`);
  }
  return {
    config: (p.config as Record<string, unknown>) ?? {},
    channels: deps.channels,
    agents: deps.agents,
  };
}
```

---

## Appendix G — `plugins.test.ts` cases for `config.set`

```ts
import { pluginsConfigSet } from "./plugins.js";
import { promises as fs } from "node:fs";
import path from "node:path";
import os from "node:os";

describe("plugins.config.set", () => {
  let tmpDir: string;
  let gatewayJsonPath: string;

  beforeEach(async () => {
    tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "plugins-set-"));
    gatewayJsonPath = path.join(tmpDir, "gateway.json");
    await fs.writeFile(
      gatewayJsonPath,
      JSON.stringify({ plugins: { entries: { p1: { enabled: true, config: { old: 1 } } } } }, null, 2),
    );
  });

  it("validates against plugin TypeBox schema and writes config", async () => {
    const schema = { type: "object", properties: { foo: { type: "string" } }, required: [], additionalProperties: false };
    const registry = makeRegistry([{ id: "p1", enabled: true, manifest: { id: "p1", configSchema: schema } }]);
    const result = await pluginsConfigSet({
      registry,
      gatewayJsonPath,
      ctx: { isAdmin: true },
      params: { pluginId: "p1", config: { foo: "hello" } },
    });
    expect(result.ok).toBe(true);
    const persisted = JSON.parse(await fs.readFile(gatewayJsonPath, "utf8"));
    expect(persisted.plugins.entries.p1.config).toEqual({ foo: "hello" });
  });

  it("rejects payload that fails TypeBox validation", async () => {
    const schema = { type: "object", properties: { foo: { type: "string" } }, required: ["foo"], additionalProperties: false };
    const registry = makeRegistry([{ id: "p1", enabled: true, manifest: { id: "p1", configSchema: schema } }]);
    const result = await pluginsConfigSet({
      registry,
      gatewayJsonPath,
      ctx: { isAdmin: true },
      params: { pluginId: "p1", config: {} },
    });
    expect(result.ok).toBe(false);
    expect(result.errors).toBeDefined();
  });

  it("returns restartRequired=true when enabling/disabling", async () => {
    const registry = makeRegistry([
      {
        id: "p1",
        enabled: false,
        manifest: {
          id: "p1",
          configSchema: { type: "object", additionalProperties: false, properties: {} },
        },
      },
    ]);
    const result = await pluginsConfigSet({
      registry,
      gatewayJsonPath,
      ctx: { isAdmin: true },
      params: { pluginId: "p1", config: {}, enabled: true },
    });
    expect(result.ok).toBe(true);
    expect(result.restartRequired).toBe(true);
  });

  it("uses atomic temp+rename write", async () => {
    const schema = { type: "object", additionalProperties: false, properties: {} };
    const registry = makeRegistry([{ id: "p1", enabled: true, manifest: { id: "p1", configSchema: schema } }]);
    await pluginsConfigSet({
      registry,
      gatewayJsonPath,
      ctx: { isAdmin: true },
      params: { pluginId: "p1", config: {} },
    });
    const tmpExists = await fs
      .access(`${gatewayJsonPath}.tmp`)
      .then(() => true)
      .catch(() => false);
    expect(tmpExists).toBe(false);
  });

  it("rejects non-admin", async () => {
    const registry = makeRegistry([]);
    await expect(
      pluginsConfigSet({
        registry,
        gatewayJsonPath,
        ctx: { isAdmin: false },
        params: { pluginId: "p1", config: {} },
      }),
    ).rejects.toThrow(/admin/i);
  });
});
```

---

## Appendix H — `plugins.ts` additions for `config.set`

```ts
import { promises as fs } from "node:fs";
import { Value } from "@sinclair/typebox/value";

// Per-plugin write lock (single-node, not cluster-safe; gateway is single-instance).
const writeLocks = new Map<string, Promise<void>>();

export interface PluginsConfigSetParams {
  pluginId: string;
  config: Record<string, unknown>;
  enabled?: boolean;
}

export interface PluginsConfigSetResult {
  ok: boolean;
  restartRequired: boolean;
  errors?: string[];
}

export async function pluginsConfigSet(deps: {
  registry: PluginRegistry;
  gatewayJsonPath: string;
  ctx: PluginsMethodCtx;
  params: PluginsConfigSetParams;
}): Promise<PluginsConfigSetResult> {
  if (!deps.ctx.isAdmin) {
    throw new Error("plugins.config.set: admin only");
  }
  const p = deps.registry.get(deps.params.pluginId);
  if (!p) throw new Error(`plugins.config.set: plugin '${deps.params.pluginId}' not found`);

  const schema = (p.manifest as { configSchema?: unknown }).configSchema;
  if (schema && !Value.Check(schema as any, deps.params.config)) {
    const errors = [...Value.Errors(schema as any, deps.params.config)].map(
      (e) => `${e.path}: ${e.message}`,
    );
    return { ok: false, restartRequired: false, errors };
  }

  // Per-plugin lock to serialize writes.
  const prev = writeLocks.get(deps.params.pluginId) ?? Promise.resolve();
  let release!: () => void;
  const next = new Promise<void>((r) => {
    release = r;
  });
  writeLocks.set(deps.params.pluginId, prev.then(() => next));
  try {
    await prev;
    const raw = await fs.readFile(deps.gatewayJsonPath, "utf8");
    const obj = JSON.parse(raw) as { plugins?: { entries?: Record<string, any> } };
    obj.plugins = obj.plugins ?? {};
    obj.plugins.entries = obj.plugins.entries ?? {};
    const entry = obj.plugins.entries[deps.params.pluginId] ?? {};
    const wasEnabled = entry.enabled === true;
    if (deps.params.enabled !== undefined) entry.enabled = deps.params.enabled;
    entry.config = deps.params.config;
    obj.plugins.entries[deps.params.pluginId] = entry;

    const tmp = `${deps.gatewayJsonPath}.tmp`;
    await fs.writeFile(tmp, JSON.stringify(obj, null, 2));
    await fs.rename(tmp, deps.gatewayJsonPath);

    const restartRequired = deps.params.enabled !== undefined && deps.params.enabled !== wasEnabled;
    return { ok: true, restartRequired };
  } finally {
    release();
    if (writeLocks.get(deps.params.pluginId) === next) {
      writeLocks.delete(deps.params.pluginId);
    }
  }
}
```

---

## Appendix I — `src/gateway/http/plugin-ui-static.test.ts`

```ts
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { createPluginUiStaticHandler } from "./plugin-ui-static.js";
import { promises as fs } from "node:fs";
import path from "node:path";
import os from "node:os";

function makeRes() {
  const headers: Record<string, string> = {};
  let statusCode = 200;
  let body = "";
  return {
    setHeader: (k: string, v: string) => {
      headers[k] = v;
    },
    writeHead(code: number, hh?: Record<string, string>) {
      statusCode = code;
      if (hh) Object.assign(headers, hh);
    },
    end(b?: string | Buffer) {
      if (b) body = b.toString();
    },
    get statusCode() {
      return statusCode;
    },
    get headers() {
      return headers;
    },
    get body() {
      return body;
    },
  };
}

describe("plugin-ui-static", () => {
  let root: string;
  let handler: ReturnType<typeof createPluginUiStaticHandler>;

  beforeEach(async () => {
    root = await fs.mkdtemp(path.join(os.tmpdir(), "plugin-ui-"));
    const enabledDir = path.join(root, "extensions", "p1", "ui", "dist");
    await fs.mkdir(enabledDir, { recursive: true });
    await fs.writeFile(path.join(enabledDir, "index.html"), "<html>OK</html>");
    await fs.writeFile(path.join(enabledDir, "app.js"), "console.log('hi');");
    handler = createPluginUiStaticHandler({
      pluginInstallRoot: root,
      registry: {
        list: () => [
          { id: "p1", enabled: true, manifest: { id: "p1", ui: [{ slot: "settings.plugins", title: "P1", description: "d", entrypoint: "ui/dist/index.html" }] } },
          { id: "p2", enabled: false, manifest: { id: "p2", ui: [{ slot: "settings.plugins", title: "P2", description: "d", entrypoint: "ui/dist/index.html" }] } },
        ],
        get: (id: string) => undefined as any,
      } as any,
      allowedFrameAncestors: ["https://hub.example.com"],
    });
  });

  afterEach(async () => {
    await fs.rm(root, { recursive: true, force: true });
  });

  it("serves enabled plugin index.html", async () => {
    const res = makeRes();
    await handler({ url: "/plugins/p1/ui/" } as any, res as any);
    expect(res.statusCode).toBe(200);
    expect(res.body).toContain("OK");
    expect(res.headers["Content-Security-Policy"]).toContain("frame-ancestors https://hub.example.com");
  });

  it("serves enabled plugin asset", async () => {
    const res = makeRes();
    await handler({ url: "/plugins/p1/ui/app.js" } as any, res as any);
    expect(res.statusCode).toBe(200);
    expect(res.headers["Content-Type"]).toContain("javascript");
  });

  it("404 on disabled plugin", async () => {
    const res = makeRes();
    await handler({ url: "/plugins/p2/ui/" } as any, res as any);
    expect(res.statusCode).toBe(404);
  });

  it("404 on unknown plugin", async () => {
    const res = makeRes();
    await handler({ url: "/plugins/ghost/ui/" } as any, res as any);
    expect(res.statusCode).toBe(404);
  });

  it("blocks path traversal", async () => {
    const res = makeRes();
    await handler({ url: "/plugins/p1/ui/../../../etc/passwd" } as any, res as any);
    expect(res.statusCode).toBe(404);
  });

  it("sets MIME by extension", async () => {
    const res = makeRes();
    await handler({ url: "/plugins/p1/ui/app.js" } as any, res as any);
    expect(res.headers["Content-Type"]).toMatch(/javascript|ecmascript/);
  });
});
```

---

## Appendix J — `src/gateway/http/plugin-ui-static.ts`

```ts
import { promises as fs } from "node:fs";
import path from "node:path";
import type { IncomingMessage, ServerResponse } from "node:http";
import type { PluginRegistry } from "../../plugins/registry.js";

const MIME: Record<string, string> = {
  ".html": "text/html; charset=utf-8",
  ".js": "application/javascript; charset=utf-8",
  ".mjs": "application/javascript; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".svg": "image/svg+xml",
  ".png": "image/png",
  ".jpg": "image/jpeg",
  ".jpeg": "image/jpeg",
  ".webp": "image/webp",
  ".woff": "font/woff",
  ".woff2": "font/woff2",
};

export function createPluginUiStaticHandler(deps: {
  pluginInstallRoot: string;
  registry: PluginRegistry;
  allowedFrameAncestors: string[];
}) {
  const ancestors = deps.allowedFrameAncestors.length
    ? deps.allowedFrameAncestors.join(" ")
    : "'none'";
  return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
    const url = req.url ?? "";
    const m = /^\/plugins\/([^\/]+)\/ui\/(.*)$/.exec(url);
    if (!m) {
      res.writeHead(404);
      res.end();
      return;
    }
    const pluginId = m[1]!;
    const subpathRaw = m[2] || "index.html";
    const plugin = deps.registry.list().find((p) => p.id === pluginId);
    if (!plugin || !plugin.enabled) {
      res.writeHead(404);
      res.end();
      return;
    }
    const hasUi = ((plugin.manifest as { ui?: unknown[] }).ui ?? []).length > 0;
    if (!hasUi) {
      res.writeHead(404);
      res.end();
      return;
    }
    const baseDir = path.resolve(
      deps.pluginInstallRoot,
      "extensions",
      pluginId,
      "ui",
      "dist",
    );
    const requestedPath = path.resolve(baseDir, subpathRaw);
    if (!requestedPath.startsWith(baseDir + path.sep) && requestedPath !== baseDir) {
      res.writeHead(404);
      res.end();
      return;
    }
    let filePath = requestedPath;
    try {
      const stat = await fs.stat(filePath);
      if (stat.isDirectory()) filePath = path.join(filePath, "index.html");
    } catch {
      res.writeHead(404);
      res.end();
      return;
    }
    let body: Buffer;
    try {
      body = await fs.readFile(filePath);
    } catch {
      res.writeHead(404);
      res.end();
      return;
    }
    const ext = path.extname(filePath).toLowerCase();
    const mime = MIME[ext] ?? "application/octet-stream";
    res.writeHead(200, {
      "Content-Type": mime,
      "Content-Security-Policy": `frame-ancestors ${ancestors}`,
      "X-Content-Type-Options": "nosniff",
    });
    res.end(body);
  };
}
```

---

## Appendix K — `minion_hub/src/lib/plugins/bridge-host.test.ts`

```ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mountHostBridge } from "./bridge-host";

class FakeWindow {
  listeners: Array<(e: MessageEvent) => void> = [];
  posted: Array<{ data: unknown; origin: string }> = [];
  addEventListener = vi.fn((_: string, fn: (e: MessageEvent) => void) => {
    this.listeners.push(fn);
  });
  removeEventListener = vi.fn();
  postMessage = vi.fn((data: unknown, targetOrigin: string) => {
    this.posted.push({ data, origin: targetOrigin });
  });
  emit(data: unknown, origin: string) {
    for (const l of this.listeners) l({ data, origin } as MessageEvent);
  }
}

describe("mountHostBridge", () => {
  let hostWin: FakeWindow;
  let iframeWin: FakeWindow;

  beforeEach(() => {
    hostWin = new FakeWindow();
    iframeWin = new FakeWindow();
  });

  it("sends host:hello after plugin:ready from correct origin", () => {
    const bridge = mountHostBridge({
      self: hostWin as unknown as Window,
      target: iframeWin as unknown as Window,
      pluginOrigin: "https://gw.example.com",
      hello: {
        theme: "dark",
        tokens: { bg: "#000" },
        gatewayUrl: "wss://gw",
        authToken: "tok",
      },
    });
    hostWin.emit({ type: "plugin:ready" }, "https://gw.example.com");
    expect(iframeWin.posted).toHaveLength(1);
    bridge.dispose();
  });

  it("ignores plugin:ready from wrong origin", () => {
    const bridge = mountHostBridge({
      self: hostWin as unknown as Window,
      target: iframeWin as unknown as Window,
      pluginOrigin: "https://gw.example.com",
      hello: { theme: "dark", tokens: {}, gatewayUrl: "wss://gw", authToken: "tok" },
    });
    hostWin.emit({ type: "plugin:ready" }, "https://evil.example.com");
    expect(iframeWin.posted).toHaveLength(0);
    bridge.dispose();
  });

  it("relays resize via onResize", () => {
    const onResize = vi.fn();
    const bridge = mountHostBridge({
      self: hostWin as unknown as Window,
      target: iframeWin as unknown as Window,
      pluginOrigin: "https://gw.example.com",
      hello: { theme: "dark", tokens: {}, gatewayUrl: "wss://gw", authToken: "tok" },
      onResize,
    });
    hostWin.emit({ type: "plugin:resize", height: 420 }, "https://gw.example.com");
    expect(onResize).toHaveBeenCalledWith(420);
    bridge.dispose();
  });

  it("dispose removes listeners", () => {
    const bridge = mountHostBridge({
      self: hostWin as unknown as Window,
      target: iframeWin as unknown as Window,
      pluginOrigin: "https://gw.example.com",
      hello: { theme: "dark", tokens: {}, gatewayUrl: "wss://gw", authToken: "tok" },
    });
    bridge.dispose();
    expect(hostWin.removeEventListener).toHaveBeenCalled();
  });
});
```

---

## Appendix L — `minion_hub/src/lib/plugins/bridge-host.ts`

```ts
import { HostBridge, type Theme } from "@minion-stack/plugin-ui-bridge";

export interface MountHostBridgeOptions {
  self: Window;
  target: Window;
  pluginOrigin: string;
  hello: {
    theme: Theme;
    tokens: Record<string, string>;
    gatewayUrl: string;
    authToken: string;
  };
  onResize?: (height: number) => void;
  onNotify?: (level: "info" | "warn" | "error", message: string) => void;
}

export interface MountedHostBridge {
  bridge: HostBridge;
  dispose: () => void;
  sendThemeChange: (theme: Theme, tokens: Record<string, string>) => void;
}

export function mountHostBridge(opts: MountHostBridgeOptions): MountedHostBridge {
  const bridge = new HostBridge({
    self: opts.self,
    target: opts.target,
    pluginOrigin: opts.pluginOrigin,
  });
  bridge.sendHelloOnReady(opts.hello);
  if (opts.onResize) bridge.onResize(opts.onResize);
  if (opts.onNotify) bridge.onNotify(opts.onNotify);
  return {
    bridge,
    dispose: () => bridge.dispose(),
    sendThemeChange: (theme, tokens) => bridge.sendThemeChange({ theme, tokens }),
  };
}
```

---

## Appendix M — `minion_hub/src/lib/plugins/PluginIframe.test.ts`

```ts
import { describe, it, expect, vi } from "vitest";
import { render } from "@testing-library/svelte";
import PluginIframe from "./PluginIframe.svelte";

vi.mock("./bridge-host", () => ({
  mountHostBridge: vi.fn(() => ({ dispose: vi.fn(), sendThemeChange: vi.fn() })),
}));

describe("PluginIframe", () => {
  it("renders iframe with src derived from gatewayUrl + pluginId", () => {
    const { container } = render(PluginIframe, {
      props: {
        pluginId: "p1",
        gatewayUrl: "https://gw.example.com",
        authToken: "tok",
        theme: "light",
        tokens: {},
      },
    });
    const iframe = container.querySelector("iframe");
    expect(iframe?.src).toContain("/plugins/p1/ui/");
  });

  it("mounts host bridge on iframe load", async () => {
    const { mountHostBridge } = await import("./bridge-host");
    render(PluginIframe, {
      props: {
        pluginId: "p1",
        gatewayUrl: "https://gw.example.com",
        authToken: "tok",
        theme: "light",
        tokens: {},
      },
    });
    expect(mountHostBridge).toHaveBeenCalled();
  });
});
```

---

## Appendix N — `minion_hub/src/lib/plugins/PluginIframe.svelte`

```svelte
<script lang="ts">
  import { onDestroy } from "svelte";
  import { mountHostBridge, type MountedHostBridge } from "./bridge-host";
  import type { Theme } from "@minion-stack/plugin-ui-bridge";

  interface Props {
    pluginId: string;
    gatewayUrl: string;
    authToken: string;
    theme: Theme;
    tokens: Record<string, string>;
  }

  let { pluginId, gatewayUrl, authToken, theme, tokens }: Props = $props();

  let iframeEl: HTMLIFrameElement | null = $state(null);
  let mounted: MountedHostBridge | null = null;
  let height = $state(600);

  const src = `${gatewayUrl}/plugins/${pluginId}/ui/`;
  const pluginOrigin = new URL(gatewayUrl).origin;

  function handleLoad(): void {
    if (!iframeEl?.contentWindow) return;
    mounted?.dispose();
    mounted = mountHostBridge({
      self: window,
      target: iframeEl.contentWindow,
      pluginOrigin,
      hello: { theme, tokens, gatewayUrl, authToken },
      onResize: (h) => {
        height = h;
      },
    });
  }

  $effect(() => {
    mounted?.sendThemeChange(theme, tokens);
  });

  onDestroy(() => mounted?.dispose());
</script>

<iframe
  bind:this={iframeEl}
  title="Plugin: {pluginId}"
  {src}
  onload={handleLoad}
  style:height="{height}px"
  class="w-full border-0"
></iframe>
```

---

## Appendix O — `minion_hub/src/lib/plugins/PluginSlotHost.test.ts`

```ts
import { describe, it, expect } from "vitest";
import { render } from "@testing-library/svelte";
import PluginSlotHost from "./PluginSlotHost.svelte";

describe("PluginSlotHost", () => {
  it("shows empty state when no occupants", () => {
    const { getByText } = render(PluginSlotHost, {
      props: { slot: "settings.plugins", occupants: [], gatewayUrl: "https://gw", authToken: "t" },
    });
    expect(getByText(/no plugin uis configured/i)).toBeTruthy();
  });

  it("renders one tab per occupant", () => {
    const { container, getAllByRole } = render(PluginSlotHost, {
      props: {
        slot: "settings.plugins",
        occupants: [
          { pluginId: "p1", slot: "settings.plugins", title: "P1", description: "d", entrypoint: "ui/dist/index.html" },
          { pluginId: "p2", slot: "settings.plugins", title: "P2", description: "d", entrypoint: "ui/dist/index.html" },
        ],
        gatewayUrl: "https://gw",
        authToken: "t",
      },
    });
    const tabs = getAllByRole("tab");
    expect(tabs).toHaveLength(2);
    expect(container.textContent).toContain("P1");
    expect(container.textContent).toContain("P2");
  });

  it("filters occupants to only those matching the slot", () => {
    const { container } = render(PluginSlotHost, {
      props: {
        slot: "settings.plugins",
        occupants: [
          { pluginId: "p1", slot: "settings.plugins", title: "Match", description: "d", entrypoint: "ui/dist/index.html" },
          { pluginId: "p2", slot: "dashboard.widget", title: "NoMatch", description: "d", entrypoint: "ui/dist/index.html" },
        ],
        gatewayUrl: "https://gw",
        authToken: "t",
      },
    });
    expect(container.textContent).toContain("Match");
    expect(container.textContent).not.toContain("NoMatch");
  });

  it("first tab is selected by default", () => {
    const { container } = render(PluginSlotHost, {
      props: {
        slot: "settings.plugins",
        occupants: [
          { pluginId: "p1", slot: "settings.plugins", title: "P1", description: "d", entrypoint: "ui/dist/index.html" },
        ],
        gatewayUrl: "https://gw",
        authToken: "t",
      },
    });
    const tab = container.querySelector('[role="tab"][aria-selected="true"]');
    expect(tab).toBeTruthy();
  });
});
```

---

## Appendix P — `minion_hub/src/lib/plugins/PluginSlotHost.svelte`

```svelte
<script lang="ts">
  import PluginIframe from "./PluginIframe.svelte";
  import type { PluginSlot } from "./slots";
  import type { Theme } from "@minion-stack/plugin-ui-bridge";

  interface Occupant {
    pluginId: string;
    slot: string;
    title: string;
    description: string;
    entrypoint: string;
    icon?: string;
  }

  interface Props {
    slot: PluginSlot;
    occupants: Occupant[];
    gatewayUrl: string;
    authToken: string;
    theme?: Theme;
    tokens?: Record<string, string>;
  }

  let {
    slot,
    occupants,
    gatewayUrl,
    authToken,
    theme = "light",
    tokens = {},
  }: Props = $props();

  const filtered = $derived(occupants.filter((o) => o.slot === slot));
  let selectedId = $state(filtered[0]?.pluginId ?? "");

  $effect(() => {
    if (filtered.length && !filtered.some((o) => o.pluginId === selectedId)) {
      selectedId = filtered[0]?.pluginId ?? "";
    }
  });
</script>

{#if filtered.length === 0}
  <div class="rounded border border-border bg-muted/30 p-6 text-muted-foreground">
    No plugin UIs configured for this slot yet.
  </div>
{:else}
  <div class="flex gap-4">
    <nav class="w-56 shrink-0" role="tablist" aria-label="Plugins">
      {#each filtered as occupant (occupant.pluginId)}
        <button
          type="button"
          role="tab"
          aria-selected={selectedId === occupant.pluginId}
          class="block w-full rounded px-3 py-2 text-left text-sm hover:bg-muted aria-selected:bg-accent aria-selected:text-accent-foreground"
          onclick={() => (selectedId = occupant.pluginId)}
        >
          <div class="font-medium">{occupant.title}</div>
          <div class="text-xs text-muted-foreground">{occupant.description}</div>
        </button>
      {/each}
    </nav>
    <div class="flex-1">
      {#each filtered as occupant (occupant.pluginId)}
        {#if selectedId === occupant.pluginId}
          <PluginIframe
            pluginId={occupant.pluginId}
            {gatewayUrl}
            {authToken}
            {theme}
            {tokens}
          />
        {/if}
      {/each}
    </div>
  </div>
{/if}
```
