From 303a82d1c79722f16ce7ca5a27421f8770293218 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 2 Feb 2026 21:35:50 +0700 Subject: [PATCH] feat: add provider availability check before creating sessions --- packages/server/src/server/agent/agent-manager.ts | 4 ++++ packages/server/src/server/agent/agent-sdk-types.ts | 5 +++++ .../server/src/server/agent/providers/claude-agent.ts | 10 ++++++++++ .../server/agent/providers/codex-app-server-agent.ts | 9 +++++++++ .../src/server/agent/providers/codex-mcp-agent.ts | 9 +++++++++ .../src/server/agent/providers/opencode-agent.ts | 11 ++++++++++- .../server/src/server/test-utils/fake-agent-client.ts | 4 ++++ 7 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 6ebb45e29..b09a08a9b 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -340,6 +340,10 @@ export class AgentManager { agentId: resolvedAgentId, }); const client = this.requireClient(normalizedConfig.provider); + const available = await client.isAvailable(); + if (!available) { + throw new Error(`Provider '${normalizedConfig.provider}' is not available. Please ensure the CLI is installed.`); + } const session = await client.createSession(normalizedConfig); return this.registerSession( session, diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 4d2e1d622..0ed5931a3 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -287,4 +287,9 @@ export interface AgentClient { resumeSession(handle: AgentPersistenceHandle, overrides?: Partial): Promise; listModels(options?: ListModelsOptions): Promise; listPersistedAgents?(options?: ListPersistedAgentsOptions): Promise; + /** + * Check if this provider is available (CLI binary is installed). + * Returns true if available, false otherwise. + */ + isAvailable(): Promise; } diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 9133c9afb..91b81720a 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -1,3 +1,4 @@ +import { execSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import fs from "node:fs"; import { promises } from "node:fs"; @@ -403,6 +404,15 @@ export class ClaudeAgentClient implements AgentClient { return descriptors; } + async isAvailable(): Promise { + try { + const claudePath = execSync("which claude", { encoding: "utf8" }).trim(); + return Boolean(claudePath); + } catch { + return false; + } + } + private assertConfig(config: AgentSessionConfig): ClaudeAgentConfig { if (config.provider !== "claude") { throw new Error(`ClaudeAgentClient received config for provider '${config.provider}'`); diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 285da074f..0467a4906 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -1729,4 +1729,13 @@ export class CodexAppServerAgentClient implements AgentClient { await client.dispose(); } } + + async isAvailable(): Promise { + try { + const codexPath = execSync("which codex", { encoding: "utf8" }).trim(); + return Boolean(codexPath); + } catch { + return false; + } + } } diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.ts index 5ddc5d3b1..019a0822b 100644 --- a/packages/server/src/server/agent/providers/codex-mcp-agent.ts +++ b/packages/server/src/server/agent/providers/codex-mcp-agent.ts @@ -4800,6 +4800,15 @@ export class CodexMcpAgentClient implements AgentClient { await client.dispose(); } } + + async isAvailable(): Promise { + try { + const codexPath = execSync("which codex", { encoding: "utf8" }).trim(); + return Boolean(codexPath); + } catch { + return false; + } + } } export const __test__ = { diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index dd0a21e96..58c935a07 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -1,4 +1,4 @@ -import { spawn, type ChildProcess } from "node:child_process"; +import { execSync, spawn, type ChildProcess } from "node:child_process"; import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client"; import net from "node:net"; import type { Logger } from "pino"; @@ -318,6 +318,15 @@ export class OpenCodeAgentClient implements AgentClient { return []; } + async isAvailable(): Promise { + try { + const opencodePath = execSync("which opencode", { encoding: "utf8" }).trim(); + return Boolean(opencodePath); + } catch { + return false; + } + } + private assertConfig(config: AgentSessionConfig): OpenCodeAgentConfig { if (config.provider !== "opencode") { throw new Error(`OpenCodeAgentClient received config for provider '${config.provider}'`); 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 b3df89ecd..fd7740e0e 100644 --- a/packages/server/src/server/test-utils/fake-agent-client.ts +++ b/packages/server/src/server/test-utils/fake-agent-client.ts @@ -664,6 +664,10 @@ class FakeAgentClient implements AgentClient { { provider: this.provider, id: "test-model", label: "Test Model", isDefault: true }, ]; } + + async isAvailable(): Promise { + return true; + } } export function createTestAgentClients(): Record {