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.
This commit is contained in:
Mohamed Boudra
2026-03-18 10:08:48 +07:00
parent e2f20f0e24
commit 0bc903fa21
4 changed files with 87 additions and 9 deletions
+64 -8
View File
@@ -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<StatusRow> {
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,
}
@@ -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", () => {
@@ -131,8 +131,8 @@ export function applyProviderEnv(
shellEnv?: Record<string, string>
): Record<string, string | undefined> {
return {
...(shellEnv ?? resolveShellEnv()),
...baseEnv,
...(shellEnv ?? resolveShellEnv()),
...(runtimeSettings?.env ?? {}),
};
}
+3
View File
@@ -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,