From 0bc903fa21005308cb830116bdf95522b9a05d42 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 87edfe4b7..60a07cc2c 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 e415b714f..0113f9294 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 b50f714e3..b3018bb6a 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 df7ea3411..9c4637073 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,