/** * Which agent CLIs are actually INSTALLED on this machine. * * ⚠ This is a different question from every other detection in this repo, and the distinction is * the whole reason the module exists. `lane-probe.ts` asks "what does a lane the operator ALREADY * CONFIGURED serve?" — it walks `routing.ladders` and can say nothing about a tool nobody has * written into `config.json`. `authEnv.ts` asks "is the credential this provider DECLARED present?" * Neither can answer "the operator has Codex; should we offer to use it?", which is what an * onboarding conversation needs. * * ⚠ **Detection requires POSITIVE evidence, and reports its own basis.** A host is `installed` * only when a binary resolves on PATH, or a config path it owns AND llm-relay never writes exists. * Anything else is `installed: false`, which means "no evidence found" and never "this tool is * absent" — the same rule `lane-manifest.ts` applies to eviction and `key-checker.ts` to * `unverified`. Callers must not turn a negative into an assertion; the honest phrasing is * "not detected". * * ⚠ **THE FOOTPRINT RULE, and it is the sharpest thing here.** A config path counts as evidence * ONLY if llm-relay never creates it. An adversarial review caught the first version of this file * failing its own stated guard: it excluded the bare `~/.codex/` directory as circular but then * keyed Codex on `~/.codex/config.toml` — which **every llm-relay before v0.62.0 wrote * unconditionally**. So on every existing install that path exists regardless of Codex, and the * detection gate built on it would have been permanently open: a no-op dressed as a check. The * same held for `~/.claude` and `~/.config/opencode`, both of which `install-skill.mjs` creates * with `mkdirSync(..., { recursive: true })` when it copies the skill. * * Therefore only ONE host keeps a config path: `agy`, whose * `~/.gemini/antigravity-cli/settings.json` llm-relay has never written and does not manage. For * `claude`, `codex` and `opencode`, `installed` is exactly `onPath` — a binary is the only * evidence that is not our own footprint. * * ⚠ It never spawns. `commandExistsOnPath` is a pure `accessSync` walk, so unlike `winenv.ts`, * `os-keyring.ts` and `secret-file-acl.ts` this module needs no vitest spawn guard — but every * environment input is injectable so a test never reads the developer's real machine. */ export declare const AGENT_HOST_IDS: readonly ["claude", "codex", "agy", "opencode"]; export type AgentHostId = (typeof AGENT_HOST_IDS)[number]; /** What was found for one host, with the evidence kept separate from the verdict. */ export interface AgentHostDetection { id: AgentHostId; label: string; /** A binary resolved on PATH. The strong signal — never llm-relay's own footprint. */ onPath: boolean; /** The binary name that resolved, so a caller can report WHICH spelling was found. */ binary: string | null; /** An owned config path that exists, admitted only under the footprint rule above. */ configPath: string | null; /** `onPath || configPath !== null`. False means NO EVIDENCE, never "absent". */ installed: boolean; } /** Injectable environment, so a test never reads the developer's real machine. */ export interface DetectHostsOptions { env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform; home?: string; /** Seam for path existence. Defaults to `existsSync`. */ exists?: (path: string) => boolean; /** Seam for PATH lookup. Defaults to the shared `commandExistsOnPath`. */ onPath?: (command: string, env: NodeJS.ProcessEnv, platform: NodeJS.Platform) => boolean; } /** Detect one host. Never throws — a detection failure must never break a caller's start-up. */ export declare function detectHost(id: AgentHostId, opts?: DetectHostsOptions): AgentHostDetection; /** Detect every known host, in a stable order. Never throws. */ export declare function detectHosts(opts?: DetectHostsOptions): AgentHostDetection[];