@phnx-labs/agents-cli 1.22.102 → 1.22.104
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/CHANGELOG.md +31 -0
- package/README.md +1 -1
- package/dist/bootstrap.js +15 -0
- package/dist/commands/accounts.js +2 -2
- package/dist/commands/computer.d.ts +100 -79
- package/dist/commands/computer.js +290 -830
- package/dist/commands/setup-computer.js +20 -1
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/view.js +4 -6
- package/dist/lib/account-catalog.d.ts +22 -1
- package/dist/lib/account-catalog.js +72 -38
- package/dist/lib/accounting/usage.js +18 -14
- package/dist/lib/agent-spec/agents.d.ts +1 -0
- package/dist/lib/agent-spec/agents.js +1 -1
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/computer/context.d.ts +83 -0
- package/dist/lib/computer/context.js +91 -0
- package/dist/lib/computer/policy.d.ts +46 -0
- package/dist/lib/computer/policy.js +160 -0
- package/dist/lib/computer/record.d.ts +38 -0
- package/dist/lib/computer/record.js +86 -0
- package/dist/lib/computer/sessions-list.js +6 -6
- package/dist/lib/computer-client.d.ts +150 -0
- package/dist/lib/computer-client.js +222 -0
- package/dist/lib/exec.js +35 -1
- package/dist/lib/harness/adapters/claude.d.ts +37 -0
- package/dist/lib/harness/adapters/claude.js +69 -0
- package/dist/lib/helper-download.d.ts +1 -1
- package/dist/lib/helper-download.js +1 -1
- package/dist/lib/helper-versions.d.ts +9 -4
- package/dist/lib/helper-versions.js +9 -9
- package/dist/lib/installations/shims.js +8 -4
- package/dist/lib/menubar/download-menubar.d.ts +2 -1
- package/dist/lib/menubar/download-menubar.js +2 -1
- package/dist/lib/menubar/install-menubar.d.ts +56 -0
- package/dist/lib/menubar/install-menubar.js +133 -17
- package/dist/lib/menubar/resolve-version.d.ts +82 -0
- package/dist/lib/menubar/resolve-version.js +133 -0
- package/dist/lib/profiles.js +13 -2
- package/dist/lib/secrets-client.d.ts +23 -3
- package/dist/lib/secrets-client.js +57 -57
- package/dist/lib/self-heal/checks/menubar-helper.d.ts +2 -0
- package/dist/lib/self-heal/checks/menubar-helper.js +21 -0
- package/dist/lib/self-heal/registry.js +3 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/db.js +1 -1
- package/dist/lib/sha256-asset.d.ts +2 -1
- package/dist/lib/sha256-asset.js +2 -1
- package/dist/lib/ssh-tunnel.d.ts +61 -0
- package/dist/lib/ssh-tunnel.js +105 -0
- package/dist/lib/summarizer/summarize.d.ts +2 -2
- package/dist/lib/summarizer/summarize.js +10 -3
- package/package.json +2 -3
- package/dist/commands/computer-actions.d.ts +0 -55
- package/dist/commands/computer-actions.js +0 -594
- package/dist/computer.d.ts +0 -2
- package/dist/computer.js +0 -7
- package/dist/lib/computer/actions.d.ts +0 -36
- package/dist/lib/computer/actions.js +0 -162
- package/dist/lib/computer/computer-rpc.d.ts +0 -39
- package/dist/lib/computer/computer-rpc.js +0 -447
- package/dist/lib/computer/des.d.ts +0 -1
- package/dist/lib/computer/des.js +0 -114
- package/dist/lib/computer/dispatch.d.ts +0 -10
- package/dist/lib/computer/dispatch.js +0 -133
- package/dist/lib/computer/download.d.ts +0 -54
- package/dist/lib/computer/download.js +0 -83
- package/dist/lib/computer/loop.d.ts +0 -62
- package/dist/lib/computer/loop.js +0 -98
- package/dist/lib/computer/model.d.ts +0 -44
- package/dist/lib/computer/model.js +0 -157
- package/dist/lib/computer/rfb-client.d.ts +0 -53
- package/dist/lib/computer/rfb-client.js +0 -562
- package/dist/lib/computer/ssh-tunnel.d.ts +0 -189
- package/dist/lib/computer/ssh-tunnel.js +0 -584
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* computer-client.ts — the ONE process client through which agents-cli talks to
|
|
3
|
+
* the standalone `computer` CLI (PHNX-4075).
|
|
4
|
+
*
|
|
5
|
+
* This is the agents-owned half of the computer extraction, and it is
|
|
6
|
+
* deliberately small. agents-cli no longer carries a helper daemon, an RPC
|
|
7
|
+
* transport, an element cache, an RFB client, or an autonomous loop — the
|
|
8
|
+
* standalone engine owns all of it, exactly as `secrets` took the keychain
|
|
9
|
+
* engine (PHNX-3989) and `sessions` took the transcript engine (PHNX-4012).
|
|
10
|
+
* What stays here is what only the fleet CLI can know: which apps the
|
|
11
|
+
* permissions layer allows, which device a `--device` name resolves to, who the
|
|
12
|
+
* acting session is, and where an action must be recorded.
|
|
13
|
+
*
|
|
14
|
+
* THERE IS NO FALLBACK. A missing executable throws `COMPUTER_BIN_MISSING` with
|
|
15
|
+
* install guidance (DIST-1) rather than silently driving a bundled engine —
|
|
16
|
+
* agents-cli has none to drive, and a fallback would re-couple the two release
|
|
17
|
+
* trains this extraction exists to separate.
|
|
18
|
+
*
|
|
19
|
+
* Transport — inherited-fd passthrough, not request/response:
|
|
20
|
+
*
|
|
21
|
+
* The engine's ENVIRONMENT is inherited verbatim — no overlay. Transport
|
|
22
|
+
* selection (`COMPUTER_HELPER_TCP`, `COMPUTER_HELPER_VNC`,
|
|
23
|
+
* `COMPUTER_HELPER_SOCKET`) is the engine's: it opens the `--device` tunnel and
|
|
24
|
+
* hydrates its own endpoint AND the auth token that goes with it. Publishing a
|
|
25
|
+
* bare endpoint from here would hand the daemon a connection it then rejects
|
|
26
|
+
* with `auth_failed`.
|
|
27
|
+
*
|
|
28
|
+
* - stdio 0/1/2 are INHERITED. The engine owns the user's terminal: its
|
|
29
|
+
* stdout is the command's stdout, its `--json` is the command's `--json`,
|
|
30
|
+
* its prompts reach a real tty. agents-cli never re-formats engine output,
|
|
31
|
+
* which is what keeps the surface honest as the engine evolves.
|
|
32
|
+
* - fd 3 (`COMPUTER_CONTEXT_FD`) carries ONE JSON object — the consumer
|
|
33
|
+
* context built by `lib/computer/context.ts` — written and closed
|
|
34
|
+
* immediately, so the engine reads to EOF and proceeds.
|
|
35
|
+
* - fd 4 (`COMPUTER_EVENTS_FD`) carries NDJSON action events back: one JSON
|
|
36
|
+
* object per line, each an action the engine actually performed. agents-cli
|
|
37
|
+
* turns those into feed events and `sessions --computer` history
|
|
38
|
+
* (`lib/computer/record.ts`). The engine may emit none; it must never block
|
|
39
|
+
* on this pipe.
|
|
40
|
+
*
|
|
41
|
+
* Both fds are anonymous pipes on the child's side, the same shape
|
|
42
|
+
* `secrets-client.ts` settled on after a named FIFO wedged macOS reads. The
|
|
43
|
+
* context is pushed rather than pulled so the engine needs no callback into
|
|
44
|
+
* agents-cli — one direction each way, no reentrancy.
|
|
45
|
+
*/
|
|
46
|
+
import { spawn } from 'node:child_process';
|
|
47
|
+
import { realpathSync, existsSync } from 'node:fs';
|
|
48
|
+
import * as path from 'node:path';
|
|
49
|
+
import { findInPath } from './agent-spec/agents.js';
|
|
50
|
+
const INSTALL_HINT = 'npm i -g @phnx-labs/computer-cli';
|
|
51
|
+
/** fd the engine reads its one-shot JSON context from. */
|
|
52
|
+
export const COMPUTER_CONTEXT_FD = 3;
|
|
53
|
+
/** fd the engine writes NDJSON action events to. */
|
|
54
|
+
export const COMPUTER_EVENTS_FD = 4;
|
|
55
|
+
export class ComputerClientError extends Error {
|
|
56
|
+
code;
|
|
57
|
+
constructor(code, message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.code = code;
|
|
60
|
+
this.name = 'ComputerClientError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function isComputerClientError(err) {
|
|
64
|
+
return err instanceof ComputerClientError;
|
|
65
|
+
}
|
|
66
|
+
let cachedBin;
|
|
67
|
+
function computerEntrypoint(bin) {
|
|
68
|
+
if (!/\.(cmd|ps1)$/i.test(bin))
|
|
69
|
+
return bin;
|
|
70
|
+
const launcher = path.join(path.dirname(bin), 'node_modules', '@phnx-labs', 'computer-cli', 'bin', 'computer.cjs');
|
|
71
|
+
return existsSync(launcher) ? launcher : bin;
|
|
72
|
+
}
|
|
73
|
+
export function isStandaloneComputer(bin) {
|
|
74
|
+
let real = computerEntrypoint(bin);
|
|
75
|
+
try {
|
|
76
|
+
real = realpathSync(real);
|
|
77
|
+
}
|
|
78
|
+
catch { /* spawn reports missing explicit paths */ }
|
|
79
|
+
return !/\.(cmd|ps1)$/i.test(real) && !real.endsWith(path.join('dist', 'computer.js'));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the standalone executable. `COMPUTER_BIN` wins so a dev build can be
|
|
83
|
+
* driven without touching PATH.
|
|
84
|
+
*
|
|
85
|
+
* Resolution uses `findInPath`, which skips `~/.agents/.cache/shims`. That skip
|
|
86
|
+
* is load-bearing here for the same reason it is in `sessions-client.ts`: a
|
|
87
|
+
* leftover `computer` alias shim execs `agents computer`, and resolving it would
|
|
88
|
+
* recurse into this process (the 1.22.85 secrets fork bomb, agi-cli#3532).
|
|
89
|
+
*/
|
|
90
|
+
export function resolveComputerBin() {
|
|
91
|
+
if (cachedBin)
|
|
92
|
+
return cachedBin;
|
|
93
|
+
const explicit = process.env.COMPUTER_BIN?.trim();
|
|
94
|
+
const resolved = explicit && explicit.length > 0 ? (isStandaloneComputer(explicit) ? explicit : null) : findInPath('computer', { accept: isStandaloneComputer });
|
|
95
|
+
if (!resolved) {
|
|
96
|
+
throw new ComputerClientError('COMPUTER_BIN_MISSING', 'The standalone `computer` CLI was not found. Install it with:\n' +
|
|
97
|
+
` ${INSTALL_HINT}\n` +
|
|
98
|
+
'or point $COMPUTER_BIN at its executable.');
|
|
99
|
+
}
|
|
100
|
+
cachedBin = computerEntrypoint(resolved);
|
|
101
|
+
return cachedBin;
|
|
102
|
+
}
|
|
103
|
+
/** A `.js` bin is run through this runtime; a real executable is exec'd directly. */
|
|
104
|
+
export function invocation(bin) {
|
|
105
|
+
if (/\.[mc]?js$/.test(bin))
|
|
106
|
+
return { command: process.execPath, prefix: [bin] };
|
|
107
|
+
return { command: bin, prefix: [] };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Split a growing buffer into complete NDJSON lines. Pure so the framing rules —
|
|
111
|
+
* blank lines skipped, a non-JSON line dropped rather than crashing the CLI, a
|
|
112
|
+
* trailing partial line carried forward — are unit-testable without a spawn.
|
|
113
|
+
*
|
|
114
|
+
* A malformed line is dropped, not thrown: these events are telemetry riding
|
|
115
|
+
* alongside a user-visible action that already happened. Failing the command
|
|
116
|
+
* because its receipt was unreadable would be strictly worse than losing the
|
|
117
|
+
* receipt. The action itself already failed loud on its own channel if it failed.
|
|
118
|
+
*/
|
|
119
|
+
export function parseEventLines(buffer) {
|
|
120
|
+
const events = [];
|
|
121
|
+
const parts = buffer.split('\n');
|
|
122
|
+
const rest = parts.pop() ?? '';
|
|
123
|
+
for (const line of parts) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
if (!trimmed)
|
|
126
|
+
continue;
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(trimmed);
|
|
129
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.command === 'string') {
|
|
130
|
+
events.push(parsed);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Unreadable receipt — see above.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { events, rest };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Run the standalone engine with the consumer context on fd 3 and the action
|
|
141
|
+
* event stream on fd 4. Resolves with the engine's exit code; the caller
|
|
142
|
+
* propagates it so `agents computer` exits exactly as the engine did.
|
|
143
|
+
*
|
|
144
|
+
* Throws `COMPUTER_BIN_MISSING` when the standalone is not installed. Every
|
|
145
|
+
* other failure is the engine's own, reported on the inherited stderr.
|
|
146
|
+
*/
|
|
147
|
+
export async function runComputer(opts) {
|
|
148
|
+
const bin = resolveComputerBin();
|
|
149
|
+
const { command, prefix } = invocation(bin);
|
|
150
|
+
const child = spawn(command, [...prefix, ...opts.argv], {
|
|
151
|
+
stdio: ['inherit', opts.capture ? 'pipe' : 'inherit', 'inherit', 'pipe', 'pipe'],
|
|
152
|
+
env: {
|
|
153
|
+
...process.env,
|
|
154
|
+
COMPUTER_CONTEXT_FD: String(COMPUTER_CONTEXT_FD),
|
|
155
|
+
COMPUTER_EVENTS_FD: String(COMPUTER_EVENTS_FD),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const contextPipe = child.stdio[COMPUTER_CONTEXT_FD];
|
|
159
|
+
const eventsPipe = child.stdio[COMPUTER_EVENTS_FD];
|
|
160
|
+
if (contextPipe) {
|
|
161
|
+
// An engine that exits before reading the context (bad argv, `--help`)
|
|
162
|
+
// closes fd 3 and we get EPIPE. That is a normal race, not a failure.
|
|
163
|
+
contextPipe.on('error', () => { });
|
|
164
|
+
contextPipe.end(JSON.stringify(opts.context) + '\n');
|
|
165
|
+
}
|
|
166
|
+
let stdout = '';
|
|
167
|
+
if (opts.capture && child.stdout) {
|
|
168
|
+
child.stdout.setEncoding('utf-8');
|
|
169
|
+
child.stdout.on('data', (chunk) => {
|
|
170
|
+
stdout += chunk;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
let pending = '';
|
|
174
|
+
const drained = new Promise((resolve) => {
|
|
175
|
+
if (!eventsPipe)
|
|
176
|
+
return resolve();
|
|
177
|
+
eventsPipe.setEncoding('utf-8');
|
|
178
|
+
eventsPipe.on('data', (chunk) => {
|
|
179
|
+
const { events, rest } = parseEventLines(pending + chunk);
|
|
180
|
+
pending = rest;
|
|
181
|
+
for (const event of events)
|
|
182
|
+
opts.onEvent?.(event);
|
|
183
|
+
});
|
|
184
|
+
eventsPipe.on('error', () => resolve());
|
|
185
|
+
eventsPipe.on('end', () => {
|
|
186
|
+
// A final line with no trailing newline still counts.
|
|
187
|
+
const { events } = parseEventLines(pending.endsWith('\n') ? pending : pending + '\n');
|
|
188
|
+
pending = '';
|
|
189
|
+
for (const event of events)
|
|
190
|
+
opts.onEvent?.(event);
|
|
191
|
+
resolve();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
195
|
+
child.on('error', (err) => {
|
|
196
|
+
reject(new ComputerClientError('COMPUTER_SPAWN_FAILED', `Could not run \`${bin}\`: ${err.message}`));
|
|
197
|
+
});
|
|
198
|
+
child.on('close', (code, signal) => {
|
|
199
|
+
// A signalled child has no exit code; report the conventional 128+n so
|
|
200
|
+
// callers and shells see a non-zero status instead of a false success.
|
|
201
|
+
if (code == null)
|
|
202
|
+
return resolve(signal ? 128 + (osSignalNumber(signal) ?? 0) : 1);
|
|
203
|
+
resolve(code);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
await drained;
|
|
207
|
+
return { exitCode, stdout };
|
|
208
|
+
}
|
|
209
|
+
/** Signal name → number for the 128+n exit convention. Only the ones a tunnelled
|
|
210
|
+
* or interrupted engine realistically dies on; anything else contributes 0 and
|
|
211
|
+
* still yields a non-zero status. */
|
|
212
|
+
function osSignalNumber(signal) {
|
|
213
|
+
const table = {
|
|
214
|
+
SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGPIPE: 13, SIGTERM: 15,
|
|
215
|
+
};
|
|
216
|
+
return table[signal];
|
|
217
|
+
}
|
|
218
|
+
/** Test seam: drop the memoized bin so PATH fixtures can re-resolve. */
|
|
219
|
+
export function _resetComputerClientForTest() {
|
|
220
|
+
cachedBin = undefined;
|
|
221
|
+
}
|
|
222
|
+
export const COMPUTER_INSTALL_HINT = INSTALL_HINT;
|
package/dist/lib/exec.js
CHANGED
|
@@ -18,7 +18,7 @@ import { emit, createTimer, redactPrompt, redactArgs } from './feed/events.js';
|
|
|
18
18
|
import { sanitizeProcessEnv } from './secrets-client.js';
|
|
19
19
|
import { resolveActor, actorEnv } from './actor.js';
|
|
20
20
|
import { expandLocalHome } from './project-root.js';
|
|
21
|
-
import { getShimsDir, getHistoryDir, getRuntimeStateDir } from './state.js';
|
|
21
|
+
import { getShimsDir, getHistoryDir, getUserAgentsDir, getRuntimeStateDir } from './state.js';
|
|
22
22
|
import { readCodexConfiguredModel } from './installations/shims.js';
|
|
23
23
|
import { withInstallationLease } from './installations/launch-gate.js';
|
|
24
24
|
import { getCliLaunch, getAgentsBinPath } from './cli-entry.js';
|
|
@@ -41,6 +41,7 @@ import { applyAddDirs } from './add-dir.js';
|
|
|
41
41
|
import { applyActiveRulesPresetAtRun } from './rules/run-sync.js';
|
|
42
42
|
import { applySystemResourcesAtRun } from './system-run-sync.js';
|
|
43
43
|
import { resolveHarnessAdapter, stripForeignConfigDir } from './harness/index.js';
|
|
44
|
+
import { claudeWorkerLoginTrapPreflight } from './harness/adapters/claude.js';
|
|
44
45
|
import { resolveConfigVersion } from './harness/exec-config-version.js';
|
|
45
46
|
import { getAccountInfo } from './agents.js';
|
|
46
47
|
import { getUsageLookupKey, noteClaudeSessionLimit, noteClaudeOutOfCredits, clearClaudeAccountRefusal, parseClaudeSessionLimitReset } from './accounting/usage.js';
|
|
@@ -399,6 +400,13 @@ export function buildExecEnv(options) {
|
|
|
399
400
|
result.AGENTS_PARENT_SESSION_ID = spawnerSessionId;
|
|
400
401
|
}
|
|
401
402
|
result.AGENTS_RUNTIME = resolveInteractive(options) ? 'terminal' : 'headless';
|
|
403
|
+
// The agent's own `secrets …` calls must hit the same store agents-cli reads
|
|
404
|
+
// for it: the process client points the standalone at the user agents dir
|
|
405
|
+
// (buildServeEnv, MIG-1), while a bare `secrets` defaults to ~/.secrets. Left
|
|
406
|
+
// unset, an agent's `secrets exec` ran against a second broker and a second
|
|
407
|
+
// event log, and an unlock in one was invisible in the other. An explicit
|
|
408
|
+
// SECRETS_HOME in the launching environment wins, matching buildServeEnv.
|
|
409
|
+
result.SECRETS_HOME = result.SECRETS_HOME ?? getUserAgentsDir();
|
|
402
410
|
// Durable SessionStart metadata. The hook joins these launch facts to the
|
|
403
411
|
// harness-provided real session id and writes them under the shared history
|
|
404
412
|
// directory, so a later resume can restore the permission boundary without
|
|
@@ -1797,6 +1805,32 @@ async function spawnAgentLeased(options) {
|
|
|
1797
1805
|
}
|
|
1798
1806
|
}
|
|
1799
1807
|
}
|
|
1808
|
+
// Fail loud before an INTERACTIVE claude run on a worker with no synced
|
|
1809
|
+
// credential falls through to Claude Code's "Select login method" screen — an
|
|
1810
|
+
// interactive OAuth on a headless box that never persists, the 10-minute
|
|
1811
|
+
// re-login loop (PHNX-3502 sibling). A worker authenticates from the
|
|
1812
|
+
// setup-token; when none resolves for the selected account, refuse with the
|
|
1813
|
+
// real fix instead of the useless login prompt. Headless already fails loud
|
|
1814
|
+
// (401), so the gate is interactive-only (the preflight enforces that).
|
|
1815
|
+
if (options.agent === 'claude') {
|
|
1816
|
+
const { versionHome } = resolveExecConfigHome(options);
|
|
1817
|
+
// A credential reaches the child if the worker setup-token resolves for the
|
|
1818
|
+
// selected account OR the caller passed an explicit --env override
|
|
1819
|
+
// (buildExecEnv merges options.env last, so it wins even the worker strip).
|
|
1820
|
+
const hasWorkerCredential = Boolean(options.env?.CLAUDE_CODE_OAUTH_TOKEN) ||
|
|
1821
|
+
(versionHome ? resolveClaudeSetupToken(versionHome) !== null : false);
|
|
1822
|
+
const loginTrap = claudeWorkerLoginTrapPreflight({
|
|
1823
|
+
agent: options.agent,
|
|
1824
|
+
interactive,
|
|
1825
|
+
deviceRole: selfConfiguredDeviceRole(),
|
|
1826
|
+
hasWorkerCredential,
|
|
1827
|
+
machine: machineId(),
|
|
1828
|
+
});
|
|
1829
|
+
if (loginTrap) {
|
|
1830
|
+
process.stderr.write(`\x1b[31m${loginTrap}\x1b[0m\n`);
|
|
1831
|
+
return { exitCode: 1, stdout: '', stderr: loginTrap };
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1800
1834
|
// Budget live kill-switch (issue #346). For headless runs we incrementally
|
|
1801
1835
|
// parse stream-json usage off stdout, accumulate cost, and kill the child the
|
|
1802
1836
|
// moment a configured cap is crossed — exactly like the --timeout path, but
|
|
@@ -1,2 +1,39 @@
|
|
|
1
|
+
import type { AgentId } from '../../types.js';
|
|
1
2
|
import type { HarnessAdapter } from '../adapter.js';
|
|
3
|
+
import { type ConfiguredDeviceRole } from '../../device-config.js';
|
|
2
4
|
export declare const claudeAdapter: HarnessAdapter;
|
|
5
|
+
/**
|
|
6
|
+
* Fail-loud preflight for the worker login-screen trap — the sibling of the
|
|
7
|
+
* PHNX-3502 fix. On an EXPLICIT `role: worker` device every Claude run
|
|
8
|
+
* authenticates from the synced `setup-token`, never an interactive login (owner
|
|
9
|
+
* rule / credential-management invariant 7). When NO setup-token resolves for the
|
|
10
|
+
* account this run selected, `applyExecConfigEnv` strips any ambient token and
|
|
11
|
+
* the harness launches with no credential. A HEADLESS run then fails loud with a
|
|
12
|
+
* 401 — but an INTERACTIVE dispatched TUI (`agents run claude --interactive
|
|
13
|
+
* --device <worker>`, the usual `--device auto` landing) instead drops to Claude
|
|
14
|
+
* Code's own "Select login method" screen. Answering it does an interactive OAuth
|
|
15
|
+
* on a headless box, minting a native login the worker path never reads and never
|
|
16
|
+
* syncs; Anthropic later expires it and the next run repeats the prompt — the
|
|
17
|
+
* 10-minute re-login loop the operator sees.
|
|
18
|
+
*
|
|
19
|
+
* This gate refuses that run BEFORE spawn with the real fix (pin or mint a
|
|
20
|
+
* durable account) instead of the useless login prompt. It is interactive-only
|
|
21
|
+
* because the headless 401 is already loud. Pure: the caller (spawnAgentLeased)
|
|
22
|
+
* resolves the inputs and renders the returned message, exactly like
|
|
23
|
+
* codexSandboxPreflight. Returns null when the run may proceed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function claudeWorkerLoginTrapPreflight(args: {
|
|
26
|
+
agent: AgentId;
|
|
27
|
+
interactive: boolean;
|
|
28
|
+
deviceRole?: ConfiguredDeviceRole;
|
|
29
|
+
/**
|
|
30
|
+
* Will a Claude credential actually reach the child at spawn? The caller ORs
|
|
31
|
+
* the resolved worker setup-token with an explicit `--env
|
|
32
|
+
* CLAUDE_CODE_OAUTH_TOKEN=…` override — `buildExecEnv` merges `options.env`
|
|
33
|
+
* LAST and unconditionally, so that override wins even the worker branch's
|
|
34
|
+
* strip and authenticates the run. Gating on the setup-token alone would
|
|
35
|
+
* falsely refuse that sanctioned escape hatch (forwarded across `--device`).
|
|
36
|
+
*/
|
|
37
|
+
hasWorkerCredential: boolean;
|
|
38
|
+
machine?: string;
|
|
39
|
+
}): string | null;
|
|
@@ -111,6 +111,11 @@ if [ "\$(uname -s)" = "Linux" ] && [ -z "\${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ -
|
|
|
111
111
|
fi
|
|
112
112
|
`;
|
|
113
113
|
},
|
|
114
|
+
// NOTE: the worker branch above strips the token and returns; a MISSING worker
|
|
115
|
+
// credential is caught before spawn by claudeWorkerLoginTrapPreflight (below),
|
|
116
|
+
// which fails loud for an interactive run instead of letting Claude Code fall
|
|
117
|
+
// through to its "Select login method" screen. A headless run keeps the
|
|
118
|
+
// strip-and-401 behavior.
|
|
114
119
|
routineModeArgs(cmd, ctx) {
|
|
115
120
|
const mode = ctx.mode;
|
|
116
121
|
if (mode === 'edit') {
|
|
@@ -131,3 +136,67 @@ fi
|
|
|
131
136
|
}
|
|
132
137
|
},
|
|
133
138
|
};
|
|
139
|
+
/**
|
|
140
|
+
* Fail-loud preflight for the worker login-screen trap — the sibling of the
|
|
141
|
+
* PHNX-3502 fix. On an EXPLICIT `role: worker` device every Claude run
|
|
142
|
+
* authenticates from the synced `setup-token`, never an interactive login (owner
|
|
143
|
+
* rule / credential-management invariant 7). When NO setup-token resolves for the
|
|
144
|
+
* account this run selected, `applyExecConfigEnv` strips any ambient token and
|
|
145
|
+
* the harness launches with no credential. A HEADLESS run then fails loud with a
|
|
146
|
+
* 401 — but an INTERACTIVE dispatched TUI (`agents run claude --interactive
|
|
147
|
+
* --device <worker>`, the usual `--device auto` landing) instead drops to Claude
|
|
148
|
+
* Code's own "Select login method" screen. Answering it does an interactive OAuth
|
|
149
|
+
* on a headless box, minting a native login the worker path never reads and never
|
|
150
|
+
* syncs; Anthropic later expires it and the next run repeats the prompt — the
|
|
151
|
+
* 10-minute re-login loop the operator sees.
|
|
152
|
+
*
|
|
153
|
+
* This gate refuses that run BEFORE spawn with the real fix (pin or mint a
|
|
154
|
+
* durable account) instead of the useless login prompt. It is interactive-only
|
|
155
|
+
* because the headless 401 is already loud. Pure: the caller (spawnAgentLeased)
|
|
156
|
+
* resolves the inputs and renders the returned message, exactly like
|
|
157
|
+
* codexSandboxPreflight. Returns null when the run may proceed.
|
|
158
|
+
*/
|
|
159
|
+
export function claudeWorkerLoginTrapPreflight(args) {
|
|
160
|
+
if (args.agent !== 'claude')
|
|
161
|
+
return null;
|
|
162
|
+
// A headless run with no token fails loud with a 401 already; only an
|
|
163
|
+
// interactive run falls through to Claude Code's login screen.
|
|
164
|
+
if (!args.interactive)
|
|
165
|
+
return null;
|
|
166
|
+
// Gate ONLY an EXPLICIT `role: worker` box — not a headed box, and NOT an
|
|
167
|
+
// UNMARKED one. The owner rule guarantees a real worker holds no native login
|
|
168
|
+
// (it authenticates from the synced setup-token), so no token there genuinely
|
|
169
|
+
// means the login screen with nothing behind it. A headed box authenticates
|
|
170
|
+
// from its own native login (its login prompt is the correct first-run flow).
|
|
171
|
+
// An UNMARKED box is deliberately spared: it never receives a synced setup-token
|
|
172
|
+
// (auth-sync pushes only to `role=worker` peers), so it is typically an ordinary
|
|
173
|
+
// machine authenticating from a native login the operator just never marked —
|
|
174
|
+
// gating it would false-refuse every such laptop, a far larger surface than the
|
|
175
|
+
// marked-worker case this closes. `--device auto` lands on an explicit worker
|
|
176
|
+
// once any worker is marked in the fleet (filterAutoPool), which is the primary
|
|
177
|
+
// trap; a directly-named `--device <unmarked-box>` is left as it was on main
|
|
178
|
+
// (unprotected — a bootstrap-window residual, not a regression).
|
|
179
|
+
if (args.deviceRole !== 'worker')
|
|
180
|
+
return null;
|
|
181
|
+
// A credential will reach the child (worker setup-token, or an explicit
|
|
182
|
+
// --env CLAUDE_CODE_OAUTH_TOKEN override) — proceed.
|
|
183
|
+
if (args.hasWorkerCredential)
|
|
184
|
+
return null;
|
|
185
|
+
const where = args.machine ? `worker '${args.machine}'` : 'this worker';
|
|
186
|
+
return [
|
|
187
|
+
`No Claude worker credential is available on ${where} for the account this run selected.`,
|
|
188
|
+
`A worker authenticates from a synced setup-token, never an interactive login — so`,
|
|
189
|
+
`Claude Code's "Select login method" screen here would not persist (it is the source`,
|
|
190
|
+
`of the repeated re-login). Do one of these instead:`,
|
|
191
|
+
``,
|
|
192
|
+
` • Pin a live account for this run: agents run claude#<name> … (e.g. claude#work)`,
|
|
193
|
+
` • Or set the fleet-wide default: agents accounts default claude <name>`,
|
|
194
|
+
` • If that account has no token yet, mint it on a HEADED box (e.g. your laptop):`,
|
|
195
|
+
` agents accounts login claude#<name>`,
|
|
196
|
+
``,
|
|
197
|
+
// NB: keep this text clear of RATE_LIMIT_PATTERNS (exec.ts) — this string is
|
|
198
|
+
// returned as spawn stderr, which runWithFallback scans; a stray "rate limit"
|
|
199
|
+
// / "quota" phrase here would spuriously trigger an account-rotation fallback.
|
|
200
|
+
`See which accounts are LIVE (signed in, with headroom) with: agents accounts list`,
|
|
201
|
+
].join('\n');
|
|
202
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* then verifies the code signature (Developer ID Team + notarization — and, for
|
|
11
11
|
* the menu-bar helper, its designated requirement) before it is ever installed.
|
|
12
12
|
*
|
|
13
|
-
* `
|
|
13
|
+
* `menubar/download-menubar.ts`
|
|
14
14
|
* (MenubarHelper) are thin per-helper wrappers over the primitives here — one
|
|
15
15
|
* download+verify machinery, two specs — so a fix to the verify/download logic
|
|
16
16
|
* lands for both helpers at once.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* then verifies the code signature (Developer ID Team + notarization — and, for
|
|
11
11
|
* the menu-bar helper, its designated requirement) before it is ever installed.
|
|
12
12
|
*
|
|
13
|
-
* `
|
|
13
|
+
* `menubar/download-menubar.ts`
|
|
14
14
|
* (MenubarHelper) are thin per-helper wrappers over the primitives here — one
|
|
15
15
|
* download+verify machinery, two specs — so a fix to the verify/download logic
|
|
16
16
|
* lands for both helpers at once.
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* never be derived from `getCliVersion()`.
|
|
28
28
|
*/
|
|
29
29
|
/** The helpers that have their own release train. */
|
|
30
|
-
export type HelperName = 'menubar'
|
|
30
|
+
export type HelperName = 'menubar';
|
|
31
31
|
/** One helper's release identity. */
|
|
32
32
|
export interface HelperRelease {
|
|
33
33
|
/** Tag prefix — the release is `<tagPrefix>/v<version>`. */
|
|
@@ -43,9 +43,14 @@ export interface HelperRelease {
|
|
|
43
43
|
*
|
|
44
44
|
* `menubar` starts at 1.0.0 — the first build published under its own tag
|
|
45
45
|
* rather than the CLI's. It is not "version 1 of the helper"; it is version 1
|
|
46
|
-
* of its independent release train.
|
|
47
|
-
*
|
|
48
|
-
* helper
|
|
46
|
+
* of its independent release train.
|
|
47
|
+
*
|
|
48
|
+
* Two helper families have since left this table entirely, both for the same
|
|
49
|
+
* reason: their engine became a standalone CLI that resolves and verifies its
|
|
50
|
+
* OWN helper release. The keychain helper went with `secrets` (PHNX-3989); the
|
|
51
|
+
* macOS and Windows computer helpers went with `computer` (PHNX-4075). A floor
|
|
52
|
+
* recorded here for a helper this CLI never downloads would be a lying table —
|
|
53
|
+
* it would claim a distribution responsibility agents-cli no longer has.
|
|
49
54
|
*/
|
|
50
55
|
export declare const HELPER_RELEASES: Readonly<Record<HelperName, HelperRelease>>;
|
|
51
56
|
/** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
|
|
@@ -31,17 +31,17 @@
|
|
|
31
31
|
*
|
|
32
32
|
* `menubar` starts at 1.0.0 — the first build published under its own tag
|
|
33
33
|
* rather than the CLI's. It is not "version 1 of the helper"; it is version 1
|
|
34
|
-
* of its independent release train.
|
|
35
|
-
*
|
|
36
|
-
* helper
|
|
34
|
+
* of its independent release train.
|
|
35
|
+
*
|
|
36
|
+
* Two helper families have since left this table entirely, both for the same
|
|
37
|
+
* reason: their engine became a standalone CLI that resolves and verifies its
|
|
38
|
+
* OWN helper release. The keychain helper went with `secrets` (PHNX-3989); the
|
|
39
|
+
* macOS and Windows computer helpers went with `computer` (PHNX-4075). A floor
|
|
40
|
+
* recorded here for a helper this CLI never downloads would be a lying table —
|
|
41
|
+
* it would claim a distribution responsibility agents-cli no longer has.
|
|
37
42
|
*/
|
|
38
43
|
export const HELPER_RELEASES = {
|
|
39
|
-
menubar: { tagPrefix: 'menubar', floor: '1.
|
|
40
|
-
'computer-mac': { tagPrefix: 'computer-mac', floor: '1.0.0' },
|
|
41
|
-
// The Windows helper is a bare .exe, not an .app bundle, so it does not share
|
|
42
|
-
// helper-download.ts's zip/codesign/notarize machinery -- but it has the same
|
|
43
|
-
// URL problem, so it shares the tag namespace and the floor.
|
|
44
|
-
'computer-win': { tagPrefix: 'computer-win', floor: '1.0.0' },
|
|
44
|
+
menubar: { tagPrefix: 'menubar', floor: '1.3.0' },
|
|
45
45
|
};
|
|
46
46
|
/** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
|
|
47
47
|
export function helperTag(helper, version) {
|
|
@@ -2189,11 +2189,15 @@ export function listShimFileNames() {
|
|
|
2189
2189
|
}
|
|
2190
2190
|
/**
|
|
2191
2191
|
* Legacy command shims that are wrong even when their baked install is alive.
|
|
2192
|
-
* `secrets` (PHNX-3989)
|
|
2193
|
-
* `agents <name>`, which is a passthrough to the STANDALONE binary —
|
|
2194
|
-
* shim re-enters itself (the 1.22.85 fork bomb). Pruned unconditionally.
|
|
2192
|
+
* `secrets` (PHNX-3989), `sessions` (PHNX-4012) and `computer` (PHNX-4075): the
|
|
2193
|
+
* shim `exec`s `agents <name>`, which is a passthrough to the STANDALONE binary —
|
|
2194
|
+
* so the shim re-enters itself (the 1.22.85 fork bomb). Pruned unconditionally.
|
|
2195
|
+
*
|
|
2196
|
+
* `computer` is the sharpest of the three, because agents-cli USED to publish a
|
|
2197
|
+
* `computer` bin of its own: a machine that installed an older CLI has a real
|
|
2198
|
+
* shim on PATH under the name the standalone engine now owns.
|
|
2195
2199
|
*/
|
|
2196
|
-
const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets', 'sessions']);
|
|
2200
|
+
const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets', 'sessions', 'computer']);
|
|
2197
2201
|
/**
|
|
2198
2202
|
* Prune a stale, orphaned shim: one that is NOT a managed agent shim and NOT a user
|
|
2199
2203
|
* alias, whose baked `AGENTS_BIN` points at an install that no longer exists. These
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* On-demand download + verification of the macOS menu-bar helper
|
|
3
3
|
* ("MenubarHelper.app").
|
|
4
4
|
*
|
|
5
|
-
* Mirrors the ComputerHelper download model
|
|
5
|
+
* Mirrors the ComputerHelper download model the computer subsystem used before
|
|
6
|
+
* it was extracted (PHNX-4075): the
|
|
6
7
|
* helper ships as a signed + notarized `.app` zipped as a GitHub release asset
|
|
7
8
|
* on the helper's own `menubar/v<x.y.z>` tag, NOT the CLI's tag (see
|
|
8
9
|
* `helper-versions.ts`). A fresh `npm i -g` machine whose tarball lacks a
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* On-demand download + verification of the macOS menu-bar helper
|
|
3
3
|
* ("MenubarHelper.app").
|
|
4
4
|
*
|
|
5
|
-
* Mirrors the ComputerHelper download model
|
|
5
|
+
* Mirrors the ComputerHelper download model the computer subsystem used before
|
|
6
|
+
* it was extracted (PHNX-4075): the
|
|
6
7
|
* helper ships as a signed + notarized `.app` zipped as a GitHub release asset
|
|
7
8
|
* on the helper's own `menubar/v<x.y.z>` tag, NOT the CLI's tag (see
|
|
8
9
|
* `helper-versions.ts`). A fresh `npm i -g` machine whose tarball lacks a
|
|
@@ -70,6 +70,20 @@ export declare function menubarDisabledByUser(): boolean;
|
|
|
70
70
|
export declare function menubarServiceInstalled(): boolean;
|
|
71
71
|
/** Where a downloaded copy of the floor release sits once fetched and verified. */
|
|
72
72
|
export declare function cachedFloorBundlePath(): string;
|
|
73
|
+
/**
|
|
74
|
+
* Where the downloaded copy of the RESOLVED release sits (`cachedMenubarVersion`,
|
|
75
|
+
* so the floor's cache dir until this machine has resolved something newer).
|
|
76
|
+
* The startup self-heal installs from here, network-free.
|
|
77
|
+
*/
|
|
78
|
+
export declare function cachedReleaseBundlePath(): string;
|
|
79
|
+
/**
|
|
80
|
+
* The release version an explicit install should fetch: the newest published
|
|
81
|
+
* build, but never below what this Mac already runs — a release deleted after
|
|
82
|
+
* it was installed must not roll the helper back through `setup`/`enable`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function menubarVersionToInstall(opts?: {
|
|
85
|
+
force?: boolean;
|
|
86
|
+
}): Promise<string>;
|
|
73
87
|
/** True when the bundle carries a signature the kernel will accept at launch. */
|
|
74
88
|
export declare function codesignVerifies(appPath: string): boolean;
|
|
75
89
|
/**
|
|
@@ -511,3 +525,45 @@ export declare function isMenubarProcessStaleAgainstBundle(pidStartedAtMs: numbe
|
|
|
511
525
|
* remains the command that fixes what this reports.
|
|
512
526
|
*/
|
|
513
527
|
export declare function buildMenubarDoctorReport(): MenubarDoctorReport;
|
|
528
|
+
/** Outcome of one auto-update pass (`updateMenubarHelperIfNewer`). */
|
|
529
|
+
export interface MenubarUpdateResult {
|
|
530
|
+
outcome: 'updated' | 'current' | 'skipped' | 'failed';
|
|
531
|
+
installed: string | null;
|
|
532
|
+
available: string;
|
|
533
|
+
detail: string;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Bring an installed release helper up to the newest published build, without
|
|
537
|
+
* a human running `agents menubar setup`.
|
|
538
|
+
*
|
|
539
|
+
* Runs from the daemon's self-heal tick and right after `agents upgrade`. It
|
|
540
|
+
* only ever moves a RELEASE install forward: a machine whose helper came from a
|
|
541
|
+
* local build (a dev checkout) keeps it, a machine that never enabled the menu
|
|
542
|
+
* bar or opted out is left alone, and the ownership contest that bounds
|
|
543
|
+
* multi-install churn (`mayInstallMenubarHelper`) still applies. The bundle is
|
|
544
|
+
* downloaded and verified by the same path every install uses (sha256,
|
|
545
|
+
* codesign, Team, designated-requirement pin, notarization), swapped
|
|
546
|
+
* atomically under the install lock at the same path and identity — which is
|
|
547
|
+
* what keeps the Accessibility grant — and the running helper is restarted
|
|
548
|
+
* from the new binary (`restartMenubarHelperAfterSwap`; launchd's KeepAlive
|
|
549
|
+
* relaunches it within seconds). `dryRun` reports what would happen. Never
|
|
550
|
+
* throws.
|
|
551
|
+
*/
|
|
552
|
+
/**
|
|
553
|
+
* Pure (no I/O): whether an auto-update pass should proceed, and why not.
|
|
554
|
+
* `shipped` is a bundle that ships WITH this install (never a downloaded
|
|
555
|
+
* release cache) — CLI upgrades own that one. Returns null to proceed.
|
|
556
|
+
*/
|
|
557
|
+
export declare function menubarUpdateSkipReason(opts: {
|
|
558
|
+
darwin: boolean;
|
|
559
|
+
disabledByUser: boolean;
|
|
560
|
+
serviceInstalled: boolean;
|
|
561
|
+
shipped: boolean;
|
|
562
|
+
installed: MenubarStamp | null;
|
|
563
|
+
}): string | null;
|
|
564
|
+
/** Pure (no I/O): what the pass does given the installed and available versions. */
|
|
565
|
+
export declare function menubarUpdateOutcome(installed: string, available: string): 'current' | 'updated';
|
|
566
|
+
export declare function updateMenubarHelperIfNewer(opts?: {
|
|
567
|
+
dryRun?: boolean;
|
|
568
|
+
force?: boolean;
|
|
569
|
+
}): Promise<MenubarUpdateResult>;
|