From 6c11a082c5e458efe75fa573e152f2b6c5f2f0d9 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 18 Mar 2026 10:08:48 +0700 Subject: [PATCH] Fix agent spawn PATH resolution and add provider binary checks to status Shell env PATH was being overridden by process.env PATH in applyProviderEnv, causing agent spawns to fail with ENOENT when the daemon runs from the Tauri desktop app (minimal GUI PATH). Flip the merge order so login shell env wins. Add a Providers section to `paseo daemon status` that resolves each agent binary (claude, codex, opencode) and runs --version using the same applyProviderEnv environment the daemon uses to spawn agents. --- packages/cli/src/commands/daemon/status.ts | 72 ++++++++++++++++--- .../agent/provider-launch-config.test.ts | 19 +++++ .../server/agent/provider-launch-config.ts | 2 +- packages/server/src/server/exports.ts | 3 + 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/commands/daemon/status.ts b/packages/cli/src/commands/daemon/status.ts index 87edfe4b7b..60a07cc2c1 100644 --- a/packages/cli/src/commands/daemon/status.ts +++ b/packages/cli/src/commands/daemon/status.ts @@ -1,11 +1,18 @@ import type { Command } from 'commander' +import { execFileSync } from 'node:child_process' import { createRequire } from 'node:module' -import { getOrCreateServerId } from '@getpaseo/server' +import { getOrCreateServerId, findExecutable, applyProviderEnv } from '@getpaseo/server' import { tryConnectToDaemon } from '../../utils/client.js' import type { CommandOptions, ListResult, OutputSchema } from '../../output/index.js' import { resolveLocalDaemonState, resolveTcpHostFromListen } from './local-daemon.js' import { resolveNodePathFromPid } from './runtime-toolchain.js' +interface ProviderBinaryStatus { + label: string + path: string | null + version: string | null +} + interface DaemonStatus { serverId: string | null status: 'running' | 'stopped' | 'unresponsive' @@ -21,6 +28,7 @@ interface DaemonStatus { daemonNode: string cliNode: string cliVersion: string + providers: ProviderBinaryStatus[] note?: string } @@ -77,16 +85,17 @@ function createStatusSchema(status: DaemonStatus): OutputSchema { header: 'VALUE', field: 'value', color: (_, item) => { - if (item.key !== 'Status') { - return undefined + if (item.key === 'Status') { + if (item.value === 'running') return 'green' + if (item.value === 'unresponsive') return 'yellow' + return 'red' } - if (item.value === 'running') { + if (item.key.startsWith(' ')) { + if (item.value === 'not found') return 'red' + if (item.value.endsWith('(--version failed)')) return 'yellow' return 'green' } - if (item.value === 'unresponsive') { - return 'yellow' - } - return 'red' + return undefined }, }, ], @@ -126,9 +135,53 @@ function toStatusRows(status: DaemonStatus): StatusRow[] { rows.push({ key: 'Note', value: status.note }) } + rows.push({ key: '', value: '' }) + rows.push({ key: 'Providers', value: '' }) + for (const provider of status.providers) { + if (!provider.path) { + rows.push({ key: ` ${provider.label}`, value: 'not found' }) + } else if (!provider.version) { + rows.push({ key: ` ${provider.label}`, value: `${provider.path} (--version failed)` }) + } else { + rows.push({ key: ` ${provider.label}`, value: `${provider.path} (${provider.version})` }) + } + } + return rows } +const PROVIDER_BINARIES: { label: string; binary: string }[] = [ + { label: 'Claude', binary: 'claude' }, + { label: 'Codex', binary: 'codex' }, + { label: 'OpenCode', binary: 'opencode' }, +] + +function checkProviderBinary(binary: string): { path: string | null; version: string | null } { + const binaryPath = findExecutable(binary) + if (!binaryPath) { + return { path: null, version: null } + } + const env = applyProviderEnv(process.env) + try { + const output = execFileSync(binaryPath, ['--version'], { + encoding: 'utf8', + timeout: 5000, + stdio: ['ignore', 'pipe', 'pipe'], + env, + }).trim() + return { path: binaryPath, version: output || null } + } catch { + return { path: binaryPath, version: null } + } +} + +function checkProviderBinaries(): ProviderBinaryStatus[] { + return PROVIDER_BINARIES.map(({ label, binary }) => { + const result = checkProviderBinary(binary) + return { label, ...result } + }) +} + function resolveOwnerLabel(uid: number | undefined, hostname: string | undefined): string | null { if (uid === undefined && !hostname) { return null @@ -201,6 +254,8 @@ export async function runStatusCommand( note = appendNote(note, `serverId unavailable: ${shortenMessage(normalizeError(error))}`) } + const providers = checkProviderBinaries() + const daemonStatus: DaemonStatus = { serverId, status, @@ -216,6 +271,7 @@ export async function runStatusCommand( daemonNode, cliNode, cliVersion, + providers, note, } diff --git a/packages/server/src/server/agent/provider-launch-config.test.ts b/packages/server/src/server/agent/provider-launch-config.test.ts index e415b714f0..0113f92943 100644 --- a/packages/server/src/server/agent/provider-launch-config.test.ts +++ b/packages/server/src/server/agent/provider-launch-config.test.ts @@ -92,6 +92,25 @@ describe("applyProviderEnv", () => { expect(env.FOO).toBe("bar"); expect(Object.keys(env).length).toBeGreaterThanOrEqual(3); }); + + test("shell env PATH wins over base env PATH", () => { + const base = { PATH: "/usr/bin:/bin" }; + const shellEnv = { PATH: "/usr/local/bin:/usr/bin:/bin:/home/user/.nvm/bin" }; + + const env = applyProviderEnv(base, undefined, shellEnv); + + expect(env.PATH).toBe("/usr/local/bin:/usr/bin:/bin:/home/user/.nvm/bin"); + }); + + test("runtimeSettings env wins over shell env", () => { + const base = { PATH: "/usr/bin" }; + const shellEnv = { PATH: "/usr/local/bin:/usr/bin" }; + const runtime: ProviderRuntimeSettings = { env: { PATH: "/custom/path" } }; + + const env = applyProviderEnv(base, runtime, shellEnv); + + expect(env.PATH).toBe("/custom/path"); + }); }); describe("findExecutable", () => { diff --git a/packages/server/src/server/agent/provider-launch-config.ts b/packages/server/src/server/agent/provider-launch-config.ts index b50f714e30..b3018bb6a9 100644 --- a/packages/server/src/server/agent/provider-launch-config.ts +++ b/packages/server/src/server/agent/provider-launch-config.ts @@ -131,8 +131,8 @@ export function applyProviderEnv( shellEnv?: Record ): Record { return { - ...(shellEnv ?? resolveShellEnv()), ...baseEnv, + ...(shellEnv ?? resolveShellEnv()), ...(runtimeSettings?.env ?? {}), }; } diff --git a/packages/server/src/server/exports.ts b/packages/server/src/server/exports.ts index df7ea34110..9c46370736 100644 --- a/packages/server/src/server/exports.ts +++ b/packages/server/src/server/exports.ts @@ -24,6 +24,9 @@ export { type SherpaLoaderEnvResolution, } from "./speech/providers/local/sherpa/sherpa-runtime-env.js"; +// Provider binary resolution +export { findExecutable, applyProviderEnv } from "./agent/provider-launch-config.js"; + // Agent SDK types for CLI commands export type { AgentMode,