From 6fc875534f59d29030f58131ecdc95ec4782f4fa Mon Sep 17 00:00:00 2001 From: Michael Averto Date: Fri, 27 Mar 2026 07:33:15 -0400 Subject: [PATCH 1/2] fix: thread command option through listAdapterModels for opencode model discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The models listing endpoint called listOpenCodeModels() with no arguments, so it always fell back to the default "opencode" command. When the default binary was unavailable (e.g. stale nix store path), the endpoint returned [] even though agents had a working custom command in their adapterConfig. Thread an optional { command } through the call chain. The route accepts an optional agentId query param and resolves the command from the agent's persisted adapterConfig — never from raw user input — to avoid command injection. Fixes #1904 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/adapter-utils/src/types.ts | 2 +- .../adapters/opencode-local/src/server/models.test.ts | 6 ++++++ packages/adapters/opencode-local/src/server/models.ts | 4 ++-- server/src/adapters/registry.ts | 4 ++-- server/src/routes/agents.ts | 10 +++++++++- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index ce89e0e80b..345583e4c2 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -271,7 +271,7 @@ export interface ServerAdapterModule { sessionManagement?: import("./session-compaction.js").AdapterSessionManagement; supportsLocalAgentJwt?: boolean; models?: AdapterModel[]; - listModels?: () => Promise; + listModels?: (opts?: { command?: string }) => Promise; agentConfigurationDoc?: string; /** * Optional lifecycle hook when an agent is approved/hired (join-request or hire_agent approval). diff --git a/packages/adapters/opencode-local/src/server/models.test.ts b/packages/adapters/opencode-local/src/server/models.test.ts index cd49e4a274..6d12fe9629 100644 --- a/packages/adapters/opencode-local/src/server/models.test.ts +++ b/packages/adapters/opencode-local/src/server/models.test.ts @@ -16,6 +16,12 @@ describe("openCode models", () => { await expect(listOpenCodeModels()).resolves.toEqual([]); }); + it("uses command option when provided", async () => { + await expect( + listOpenCodeModels({ command: "__paperclip_missing_opencode_command__" }), + ).resolves.toEqual([]); + }); + it("rejects when model is missing", async () => { await expect( ensureOpenCodeModelConfiguredAndAvailable({ model: "" }), diff --git a/packages/adapters/opencode-local/src/server/models.ts b/packages/adapters/opencode-local/src/server/models.ts index 95cb1fc943..fc64393875 100644 --- a/packages/adapters/opencode-local/src/server/models.ts +++ b/packages/adapters/opencode-local/src/server/models.ts @@ -197,9 +197,9 @@ export async function ensureOpenCodeModelConfiguredAndAvailable(input: { return models; } -export async function listOpenCodeModels(): Promise { +export async function listOpenCodeModels(opts?: { command?: string }): Promise { try { - return await discoverOpenCodeModelsCached(); + return await discoverOpenCodeModelsCached({ command: opts?.command }); } catch { return []; } diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 67a8e95ba2..041ab860f8 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -205,11 +205,11 @@ export function getServerAdapter(type: string): ServerAdapterModule { return adapter; } -export async function listAdapterModels(type: string): Promise<{ id: string; label: string }[]> { +export async function listAdapterModels(type: string, opts?: { command?: string }): Promise<{ id: string; label: string }[]> { const adapter = adaptersByType.get(type); if (!adapter) return []; if (adapter.listModels) { - const discovered = await adapter.listModels(); + const discovered = await adapter.listModels(opts); if (discovered.length > 0) return discovered; } return adapter.models ?? []; diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index f642eb10f5..bc9c75661c 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -667,7 +667,15 @@ export function agentRoutes(db: Db) { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const type = req.params.type as string; - const models = await listAdapterModels(type); + const agentId = typeof req.query.agentId === "string" ? req.query.agentId : undefined; + let command: string | undefined; + if (agentId) { + const agent = await svc.getById(companyId, agentId); + if (agent?.adapterConfig && typeof agent.adapterConfig.command === "string") { + command = agent.adapterConfig.command; + } + } + const models = await listAdapterModels(type, command ? { command } : undefined); res.json(models); }); From f41a8d3869b5bd9369687c39204107c5d22119a9 Mon Sep 17 00:00:00 2001 From: Michael Averto Date: Fri, 27 Mar 2026 07:50:00 -0400 Subject: [PATCH 2/2] fix: correct getById call signature and add cross-company guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getById takes a single id param — was incorrectly called with (companyId, agentId) which silently passed companyId as the id, making the feature a no-op. Added explicit companyId ownership check to prevent cross-company data leakage. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/routes/agents.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index bc9c75661c..6da1ad5195 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -670,8 +670,8 @@ export function agentRoutes(db: Db) { const agentId = typeof req.query.agentId === "string" ? req.query.agentId : undefined; let command: string | undefined; if (agentId) { - const agent = await svc.getById(companyId, agentId); - if (agent?.adapterConfig && typeof agent.adapterConfig.command === "string") { + const agent = await svc.getById(agentId); + if (agent && agent.companyId === companyId && agent.adapterConfig && typeof agent.adapterConfig.command === "string") { command = agent.adapterConfig.command; } }