diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index 50d32b9c1b..3033be77d5 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -609,7 +609,7 @@ export async function createPaseoDaemon( }); const browserToolsPolicy = new DaemonConfigBrowserToolsPolicy(daemonConfigStore); const browserToolsBroker = new BrowserToolsBroker({}); - const pluginRuntime = new PluginService(logger, daemonConfigStore); + const pluginRuntime = new PluginService(logger, daemonConfigStore, daemonVersion); const serverId = getOrCreateServerId(config.paseoHome, { logger }); const daemonKeyPair = await loadOrCreateDaemonKeyPair(config.paseoHome, logger); diff --git a/packages/server/src/server/plugins/index.posix.test.ts b/packages/server/src/server/plugins/index.posix.test.ts index fc5c83d12e..755ddb054d 100644 --- a/packages/server/src/server/plugins/index.posix.test.ts +++ b/packages/server/src/server/plugins/index.posix.test.ts @@ -36,10 +36,10 @@ function createStore( function createService( home: string, plugins: Record = {}, - runtime?: ConstructorParameters[2], + runtime?: ConstructorParameters[3], ): PluginService { return bindTestSessionHost( - new PluginService(pino({ level: "silent" }), createStore(home, plugins), runtime), + new PluginService(pino({ level: "silent" }), createStore(home, plugins), "0.4.0", runtime), ); } @@ -225,7 +225,9 @@ describe("PluginService", () => { `export default function contribute(plugin: unknown) { void plugin; return () => undefined; }`, ); const store = createStore(home); - const service = bindTestSessionHost(new PluginService(pino({ level: "silent" }), store)); + const service = bindTestSessionHost( + new PluginService(pino({ level: "silent" }), store, "0.4.0"), + ); await service.start(); await expect( @@ -266,7 +268,9 @@ export default function contribute(plugin: unknown) { }`, ); const store = createStore(home); - const service = bindTestSessionHost(new PluginService(pino({ level: "silent" }), store)); + const service = bindTestSessionHost( + new PluginService(pino({ level: "silent" }), store, "0.4.0"), + ); await service.start(); await service.installDirectory({ path: directory }); @@ -293,7 +297,9 @@ export default function contribute(plugin: unknown) { `export default function contribute(plugin: unknown) { void plugin; return () => undefined; }`, ); const store = createStore(home); - const service = bindTestSessionHost(new PluginService(pino({ level: "silent" }), store)); + const service = bindTestSessionHost( + new PluginService(pino({ level: "silent" }), store, "0.4.0"), + ); await service.start(); await service.installDirectory({ path: first }); await service.installDirectory({ path: second }); @@ -321,7 +327,7 @@ export default function contribute(plugin: unknown) { }); store.patch({ pluginsEnabled: false }); const paused = createPausedRuntime(); - const service = new PluginService(pino({ level: "silent" }), store, paused.runtime); + const service = new PluginService(pino({ level: "silent" }), store, "0.4.0", paused.runtime); await service.start(); store.patch({ pluginsEnabled: true }); @@ -343,7 +349,7 @@ export default function contribute(plugin: unknown) { slow: { source: "directory", path: "/plugins/slow", enabled: false }, }); const paused = createPausedRuntime(); - const service = new PluginService(pino({ level: "silent" }), store, paused.runtime); + const service = new PluginService(pino({ level: "silent" }), store, "0.4.0", paused.runtime); await service.start(); const enabling = expect(service.enablePlugin("slow")).rejects.toThrow( @@ -366,7 +372,7 @@ export default function contribute(plugin: unknown) { slow: { source: "directory", path: "/plugins/slow", enabled: false }, }); const paused = createPluginSelectivePausedRuntime("occupier"); - const service = new PluginService(pino({ level: "silent" }), store, paused.runtime); + const service = new PluginService(pino({ level: "silent" }), store, "0.4.0", paused.runtime); await service.start(); const occupying = service.enablePlugin("occupier"); diff --git a/packages/server/src/server/plugins/index.ts b/packages/server/src/server/plugins/index.ts index 4b3d5dd9e6..513531b34f 100644 --- a/packages/server/src/server/plugins/index.ts +++ b/packages/server/src/server/plugins/index.ts @@ -34,7 +34,8 @@ export class PluginService { constructor( logger: pino.Logger, private readonly configStore: DaemonConfigStore, - runtime: PluginRuntimePort = new PluginRuntime(logger), + daemonVersion: string, + runtime: PluginRuntimePort = new PluginRuntime(logger, daemonVersion), ) { this.logger = logger.child({ module: "plugin-service" }); this.runtime = runtime; diff --git a/packages/server/src/server/plugins/plugin-paseo-api.e2e.test.ts b/packages/server/src/server/plugins/plugin-paseo-api.e2e.test.ts index a9e3fe3560..d01ff4d411 100644 --- a/packages/server/src/server/plugins/plugin-paseo-api.e2e.test.ts +++ b/packages/server/src/server/plugins/plugin-paseo-api.e2e.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { afterEach, expect, test } from "vitest"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; +import { createTestAgentClient, createTestAgentClients } from "../test-utils/fake-agent-client.js"; const roots: string[] = []; @@ -30,6 +31,12 @@ const create = defineRpc({ output: z.object({ workspaceId: z.string(), agentId: z.string() }), }); +const list = defineRpc({ + name: "list", + input: z.object({}), + output: z.object({ agentIds: z.array(z.string()) }), +}); + export default function contribute(plugin: PluginContext) { plugin.handle(create, async ({ path }, { paseo }) => { const workspace = await paseo.workspaces.create({ @@ -37,16 +44,22 @@ export default function contribute(plugin: PluginContext) { title: "Plugin workspace", }); const agent = await workspace.agents.create({ - config: { provider: "codex/test" }, + config: { provider: "pi/test" }, prompt: "Created by a plugin handler", }); return { workspaceId: workspace.id, agentId: agent.id }; }); + plugin.handle(list, async (_input, { paseo }) => { + const result = await paseo.agents.list({ page: { limit: 100 } }); + return { agentIds: result.entries.map((entry) => entry.agent.id) }; + }); return () => undefined; }`, ); - const daemon = await createTestPaseoDaemon(); + const daemon = await createTestPaseoDaemon({ + agentClients: { ...createTestAgentClients(), pi: createTestAgentClient("pi") }, + }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws`, appVersion: "0.4.0", @@ -71,6 +84,10 @@ export default function contribute(plugin: PluginContext) { if (typeof created !== "object" || created === null) { throw new Error("Plugin returned an invalid creation result"); } + const listed = await client.invokePluginRpc("paseo-api", "list", {}); + expect(listed).toEqual({ + agentIds: expect.arrayContaining([Reflect.get(created, "agentId")]), + }); await client.removePlugin("paseo-api"); const workspaces = await client.fetchWorkspaces(); const agents = await client.fetchAgents(); diff --git a/packages/server/src/server/plugins/plugin-process-protocol.ts b/packages/server/src/server/plugins/plugin-process-protocol.ts index 2d953bd9a9..c9d61336c0 100644 --- a/packages/server/src/server/plugins/plugin-process-protocol.ts +++ b/packages/server/src/server/plugins/plugin-process-protocol.ts @@ -1,5 +1,5 @@ export type PluginProcessRequest = - | { type: "initialize"; pluginId: string; bundle: string } + | { type: "initialize"; pluginId: string; bundle: string; appVersion: string } | { type: "invoke"; requestId: string; method: string; input: unknown } | { type: "shutdown" } | { type: "paseo_frame"; data: string | Uint8Array; isBinary: boolean } diff --git a/packages/server/src/server/plugins/plugin-process.ts b/packages/server/src/server/plugins/plugin-process.ts index 7fe29aef26..89c848f957 100644 --- a/packages/server/src/server/plugins/plugin-process.ts +++ b/packages/server/src/server/plugins/plugin-process.ts @@ -99,6 +99,7 @@ async function initialize(message: Extract[1]> = {}, + dependencies: NonNullable[2]> = {}, logger = pino({ level: "silent" }), ): PluginRuntime { - return new PluginRuntime(logger, { + return new PluginRuntime(logger, "0.4.0", { ...dependencies, sessionHost: dependencies.sessionHost ?? { async attachPluginSocket(_pluginId, socket) { diff --git a/packages/server/src/server/plugins/runtime.ts b/packages/server/src/server/plugins/runtime.ts index f786dbed52..c6edb0bb7c 100644 --- a/packages/server/src/server/plugins/runtime.ts +++ b/packages/server/src/server/plugins/runtime.ts @@ -204,7 +204,11 @@ export class PluginRuntime { private sessionHost: PluginPaseoSessionHost | null; private readonly listeners = new Set<(pluginId: string, error?: string) => void>(); - constructor(logger: pino.Logger, dependencies: PluginRuntimeDependencies = {}) { + constructor( + logger: pino.Logger, + private readonly daemonVersion: string, + dependencies: PluginRuntimeDependencies = {}, + ) { this.logger = logger.child({ module: "plugins" }); this.spawnChild = dependencies.spawnChild ?? spawnPluginChild; this.sessionHost = dependencies.sessionHost ?? null; @@ -363,6 +367,7 @@ export class PluginRuntime { void send(child, { type: "initialize", pluginId, + appVersion: this.daemonVersion, bundle: bundles.serverBundle, }).catch(fail); }); diff --git a/packages/server/src/server/test-utils/fake-agent-client.ts b/packages/server/src/server/test-utils/fake-agent-client.ts index 2b60ab424d..4d16f0302e 100644 --- a/packages/server/src/server/test-utils/fake-agent-client.ts +++ b/packages/server/src/server/test-utils/fake-agent-client.ts @@ -1281,3 +1281,10 @@ export function createTestAgentClients( opencode: new FakeAgentClient("opencode", options), }; } + +export function createTestAgentClient( + provider: string, + options: TestAgentClientOptions = {}, +): AgentClient { + return new FakeAgentClient(provider, options); +}