@peakflo/agent-harness 0.2.8-rc.1 → 0.2.9-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,6 +36,41 @@ Optional flags include `--health-port <port>`, `--runtime <desktop|self-hosted>`
36
36
  `--workspace-root <path>`, and `--github-proxy-port <port>`. Run
37
37
  `npx @peakflo/agent-harness@rc --help` for the full list.
38
38
 
39
+ ## Harnesses found on your machine
40
+
41
+ On a desktop or self-hosted machine the runner looks for Codex (`codex`),
42
+ Claude Code (`claude`), Cursor (`cursor-agent`), Pi (`pi`) and OpenCode
43
+ (`opencode`). For each one it reports whether it is installed, its version,
44
+ whether it is signed in, and the models it offers. Peakflo shows this list in
45
+ the agent editor and in the chat details.
46
+
47
+ Finding a harness does not mean the runner can use it. The runner runs turns
48
+ with Pi only; the other harnesses are shown as "Detected, not yet runnable by
49
+ this runner".
50
+
51
+ **When it looks.** Once after start, without delaying registration, and then
52
+ every five minutes while no turn is running. A harness you install or sign in
53
+ to later appears without a restart.
54
+
55
+ **What it runs.** To do this the runner starts those programs, as your user,
56
+ with `--version` and a status or model-list command. It looks in the absolute
57
+ directories of your `PATH` and in the usual install directories
58
+ (`~/.local/bin`, `~/.opencode/bin`, `/opt/homebrew/bin`, `/usr/local/bin`,
59
+ `~/.npm-global/bin`, `~/.volta/bin`, `~/.bun/bin`, and the directory of the
60
+ running `node`). Relative `PATH` entries are ignored. The programs are started
61
+ without the runner's own `PF_*` variables, so they do not receive the
62
+ enrolment token.
63
+
64
+ **What it sends.** Names, versions, a signed-in yes/no with a fixed reason,
65
+ and model names. It does not send file paths, account names, tokens, API keys
66
+ or the content of credential files. Model names that look like an email
67
+ address or a key are dropped; that check is a safety net, not a guarantee
68
+ about what a third-party program prints.
69
+
70
+ **Turning it off.** Start with `--no-harness-discovery`, or set
71
+ `PF_HARNESS_DISCOVERY=0`. The runner then starts none of these programs and
72
+ reports no list. Running turns with Pi is not affected.
73
+
39
74
  ## Releasing
40
75
 
41
76
  `.github/scripts/agent-harness-release.mjs` publishes this package
package/dist/cli.js CHANGED
@@ -75,6 +75,11 @@ Options:
75
75
  --max-sessions <n> concurrent sessions (env PF_MAX_CONCURRENT_SESSIONS,
76
76
  default 10 for desktop/self-hosted; sandbox is configured by control plane)
77
77
  --labels <a,b> routing labels (env PF_RUNNER_LABELS)
78
+ --no-harness-discovery do not look for Codex, Claude Code, Cursor,
79
+ Pi and OpenCode on this machine, and report
80
+ none (env PF_HARNESS_DISCOVERY=0). On by
81
+ default for desktop/self-hosted; never runs
82
+ in a sandbox.
78
83
 
79
84
  In a managed sandbox pf-bootstrap supplies these through the environment, so
80
85
  the enrolment token never appears in /proc/<pid>/cmdline.
@@ -0,0 +1,142 @@
1
+ /**
2
+ * HARNESS DISCOVERY — which coding harnesses are installed on this machine.
3
+ *
4
+ * Modelled on 20x, where the harness code came from:
5
+ * - `src/main/agent-installer/detect.js` finds the binaries (`claude`,
6
+ * `codex`, `cursor-agent`, `opencode`, `pi`) with `--version`, after it
7
+ * puts the well-known install directories on PATH;
8
+ * - `src/renderer/src/types/index.ts` holds the static model lists the 20x
9
+ * agent form shows for Claude Code, Codex and Cursor.
10
+ *
11
+ * WHAT THIS IS NOT. It is not the list of backends this runner can run.
12
+ * `capabilities.backends` stays that list and the control plane routes on it.
13
+ * An entry here says "this CLI is on the machine", and `runnable` says whether
14
+ * the runner can also drive it. Detection never implies turn support.
15
+ *
16
+ * RULES THIS MODULE KEEPS:
17
+ *
18
+ * 1. NEVER THROWS, NEVER REJECTS. One broken CLI must not hide the others, so
19
+ * every harness is probed on its own, inside its own try/catch and its own
20
+ * deadline.
21
+ * 2. TIME-LIMITED. Each command has a timeout and each harness has a total
22
+ * deadline. A CLI that hangs is killed and reported as timed out.
23
+ * 3. NO SECRETS LEAVE THIS FILE. Sign-in is decided from an exit code, one
24
+ * boolean of a status reply, or the existence of an environment variable or
25
+ * a file. No status text is kept. Every `reason` is a constant from
26
+ * `REASONS` below, never something a CLI printed. Model ids and names are
27
+ * the only CLI text that is reported, and each one is checked first.
28
+ * 4. NO GLOBAL SIDE EFFECTS. `process.env.PATH` is not changed; the search path
29
+ * is local and is handed to each child process.
30
+ */
31
+ import type { RunnerHarness, RunnerHarnessModel } from './types';
32
+ /** One command: `--version`, a status check, or a model listing. */
33
+ export declare const DISCOVERY_COMMAND_TIMEOUT_MS = 8000;
34
+ /** Everything for one harness. Harnesses run concurrently, so this is also the bound for the whole pass. */
35
+ export declare const DISCOVERY_HARNESS_TIMEOUT_MS = 20000;
36
+ /**
37
+ * Every sentence a `reason` can be. A closed set on purpose: text printed by a
38
+ * CLI can hold an account email or a key, and a constant cannot.
39
+ */
40
+ export declare const REASONS: {
41
+ readonly notInstalled: "Not installed on this machine.";
42
+ readonly versionTimedOut: "The version check timed out.";
43
+ readonly versionFailed: "The version check failed.";
44
+ readonly discoveryTimedOut: "Discovery timed out.";
45
+ readonly discoveryFailed: "Discovery failed.";
46
+ readonly signInTimedOut: "The sign-in check timed out.";
47
+ readonly signInUnknown: "Sign-in could not be confirmed.";
48
+ readonly codexSignIn: "Not signed in. Run `codex login` on this machine.";
49
+ readonly claudeSignIn: "Not signed in. Run `claude auth login` on this machine.";
50
+ readonly cursorSignIn: "Not signed in. Run `cursor-agent login` on this machine.";
51
+ };
52
+ /** 20x `CLAUDE_MODELS`. Claude Code has no command that lists models. */
53
+ export declare const CLAUDE_STATIC_MODELS: ReadonlyArray<RunnerHarnessModel>;
54
+ /** 20x `CODEX_MODELS`. Used only when Codex's own model cache cannot be read. */
55
+ export declare const CODEX_STATIC_MODELS: ReadonlyArray<RunnerHarnessModel>;
56
+ /** 20x `CURSOR_MODELS`. Used only when `cursor-agent models` gives nothing. */
57
+ export declare const CURSOR_STATIC_MODELS: ReadonlyArray<RunnerHarnessModel>;
58
+ export interface DiscoveryExecResult {
59
+ /** Exit code, or null when the process did not exit by itself. */
60
+ code: number | null;
61
+ stdout: string;
62
+ stderr: string;
63
+ timedOut: boolean;
64
+ }
65
+ /** Everything that touches the machine. Replaced in tests. */
66
+ export interface DiscoveryDeps {
67
+ /** Run one command. MUST NOT reject. */
68
+ exec(command: string, args: string[], timeoutMs: number): Promise<DiscoveryExecResult>;
69
+ /** Absolute path of a command, or null when it is not installed. */
70
+ resolveCommand(command: string): string | null;
71
+ realpath(path: string): string;
72
+ /** File text, or null when it cannot be read. */
73
+ readFile(path: string): string | null;
74
+ exists(path: string): boolean;
75
+ env: NodeJS.ProcessEnv;
76
+ home: string;
77
+ now(): Date;
78
+ }
79
+ export interface DiscoverHarnessesOptions {
80
+ /** Backend names this runner can run turns with (`capabilities.backends`). */
81
+ runnableBackends?: readonly string[];
82
+ commandTimeoutMs?: number;
83
+ harnessTimeoutMs?: number;
84
+ deps?: Partial<DiscoveryDeps>;
85
+ }
86
+ /**
87
+ * Where to look for a harness binary: PATH first, then the directories the
88
+ * harness installers use. The well-known directories come from 20x
89
+ * `detect.js` (`ensureAgentPaths` and `getPiCommandCandidates`). The directory
90
+ * of the running `node` is added because a runner started by launchd or
91
+ * systemd has a short PATH, and an npm-installed CLI sits beside that `node`.
92
+ */
93
+ export declare function discoverySearchPath(env: NodeJS.ProcessEnv, home: string, platform?: NodeJS.Platform): string[];
94
+ /**
95
+ * The environment a harness CLI is started with.
96
+ *
97
+ * EVERY `PF_*` VARIABLE IS REMOVED. Those are this runner's own settings and
98
+ * include `PF_ENROLMENT_TOKEN`; a third-party CLI has no use for them and
99
+ * must not be handed them. The rest is kept, because each CLI finds its own
100
+ * login through it (`HOME`, `CODEX_HOME`, `ANTHROPIC_API_KEY`, ...).
101
+ */
102
+ export declare function discoveryChildEnv(env: NodeJS.ProcessEnv, searchPath: readonly string[]): NodeJS.ProcessEnv;
103
+ /**
104
+ * Keep only entries that look like a model. Anything that looks like a secret
105
+ * is dropped whole: a model list has no reason to hold one, so the safe
106
+ * reading of such a line is that it is not a model.
107
+ */
108
+ export declare function sanitizeHarnessModels(models: ReadonlyArray<{
109
+ id: unknown;
110
+ name?: unknown;
111
+ default?: unknown;
112
+ }>): RunnerHarnessModel[];
113
+ /** The first version-like token, as 20x `detect.js` reads it. */
114
+ export declare function parseVersion(output: string): string | null;
115
+ export declare function saysSignedOut(text: string): boolean;
116
+ /**
117
+ * Codex keeps the models its account can use in `models_cache.json`. This is a
118
+ * model cache, not a credential file; only `slug`, `display_name`,
119
+ * `visibility` and `priority` are read from it.
120
+ */
121
+ export declare function parseCodexModelCache(text: string | null): RunnerHarnessModel[];
122
+ /** The top-level `model = "..."` of `config.toml`. Nothing else is read. */
123
+ export declare function parseCodexDefaultModel(text: string | null): string | null;
124
+ /**
125
+ * `cursor-agent models` prints one `id - Name` line per model and marks the
126
+ * default with `(current, default)` or `(default)`.
127
+ */
128
+ export declare function parseCursorModels(output: string): RunnerHarnessModel[];
129
+ /** `opencode models` prints one `provider/model` id per line. */
130
+ export declare function parseOpencodeModels(output: string): RunnerHarnessModel[];
131
+ /** Every harness this build knows how to look for, in display order. */
132
+ export declare const KNOWN_HARNESSES: ReadonlyArray<{
133
+ name: string;
134
+ label: string;
135
+ }>;
136
+ /**
137
+ * Look for every known harness. ALWAYS resolves, with one entry per known
138
+ * harness in a fixed order — a harness that is missing is reported as
139
+ * `installed: false`, so the reader can tell "not installed" from "this runner
140
+ * does not report harnesses".
141
+ */
142
+ export declare function discoverHarnesses(options?: DiscoverHarnessesOptions): Promise<RunnerHarness[]>;