@phnx-labs/agents-cli 1.22.53 → 1.22.54
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 +190 -0
- package/README.md +41 -8
- package/dist/bootstrap.js +55 -154
- package/dist/cli/command-registry.d.ts +5 -0
- package/dist/cli/command-registry.js +8 -1
- package/dist/commands/accounts.js +219 -173
- package/dist/commands/apply.js +6 -3
- package/dist/commands/auth-mint.d.ts +8 -0
- package/dist/commands/auth-mint.js +96 -0
- package/dist/commands/auth.js +5 -1
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cost.js +8 -2
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/exec.js +10 -8
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +2 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +7 -7
- package/dist/commands/insights.js +9 -0
- package/dist/commands/monitors.js +85 -30
- package/dist/commands/output.js +8 -2
- package/dist/commands/repo.js +18 -0
- package/dist/commands/secrets.js +33 -14
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +64 -20
- package/dist/commands/setup-accounts.d.ts +8 -0
- package/dist/commands/setup-accounts.js +47 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +11 -2
- package/dist/commands/share.d.ts +52 -3
- package/dist/commands/share.js +262 -18
- package/dist/commands/ssh.d.ts +7 -0
- package/dist/commands/ssh.js +18 -2
- package/dist/commands/status.js +14 -0
- package/dist/commands/view.d.ts +3 -1
- package/dist/commands/view.js +5 -4
- package/dist/lib/account-registry.js +15 -3
- package/dist/lib/accounting/rotate.d.ts +20 -6
- package/dist/lib/accounting/rotate.js +38 -7
- package/dist/lib/accounting/usage.d.ts +37 -1
- package/dist/lib/accounting/usage.js +71 -6
- package/dist/lib/agent-spec/agents.d.ts +5 -2
- package/dist/lib/agent-spec/agents.js +25 -7
- package/dist/lib/analytics/mix-commands.js +12 -6
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- package/dist/lib/claude-account-token.d.ts +10 -0
- package/dist/lib/claude-account-token.js +14 -4
- package/dist/lib/config-drift.d.ts +37 -0
- package/dist/lib/config-drift.js +72 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
- package/dist/lib/daemon/auth-sync-service.js +34 -0
- package/dist/lib/daemon/daemon.js +30 -4
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +3 -3
- package/dist/lib/device-config.js +5 -5
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/exec.d.ts +28 -0
- package/dist/lib/exec.js +73 -7
- package/dist/lib/feed/feed.d.ts +1 -1
- package/dist/lib/feed/feed.js +23 -1
- package/dist/lib/feed-broadcast.js +1 -1
- package/dist/lib/fleet/apply.d.ts +11 -0
- package/dist/lib/fleet/apply.js +23 -3
- package/dist/lib/fleet/auth-sync.js +5 -3
- package/dist/lib/help.d.ts +9 -0
- package/dist/lib/help.js +29 -1
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/installations/versions.js +9 -1
- package/dist/lib/linux-userns.d.ts +58 -0
- package/dist/lib/linux-userns.js +116 -0
- package/dist/lib/memory.d.ts +26 -0
- package/dist/lib/memory.js +80 -1
- package/dist/lib/monitors/config.d.ts +11 -0
- package/dist/lib/monitors/config.js +8 -0
- package/dist/lib/monitors/engine.js +8 -1
- package/dist/lib/monitors/state.d.ts +37 -1
- package/dist/lib/monitors/state.js +79 -4
- package/dist/lib/permissions-registry.d.ts +2 -0
- package/dist/lib/permissions-registry.js +116 -14
- package/dist/lib/permissions.d.ts +5 -3
- package/dist/lib/permissions.js +25 -27
- package/dist/lib/profiles.d.ts +8 -7
- package/dist/lib/profiles.js +12 -0
- package/dist/lib/project-key.d.ts +9 -0
- package/dist/lib/project-key.js +11 -0
- package/dist/lib/secrets/bundles.d.ts +35 -0
- package/dist/lib/secrets/bundles.js +78 -1
- package/dist/lib/secrets/push.d.ts +3 -8
- package/dist/lib/secrets/push.js +18 -14
- package/dist/lib/secrets/remote.d.ts +9 -18
- package/dist/lib/secrets/remote.js +11 -26
- package/dist/lib/secrets/reserved-sync.d.ts +65 -0
- package/dist/lib/secrets/reserved-sync.js +129 -0
- package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
- package/dist/lib/self-heal/registry.js +4 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/active.js +1 -4
- package/dist/lib/session/db.d.ts +39 -4
- package/dist/lib/session/db.js +130 -29
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +119 -25
- package/dist/lib/session/insights.d.ts +14 -0
- package/dist/lib/session/insights.js +25 -2
- package/dist/lib/session/linear.js +1 -1
- package/dist/lib/session/shell-programs.d.ts +17 -0
- package/dist/lib/session/shell-programs.js +21 -0
- package/dist/lib/session/state.js +2 -1
- package/dist/lib/session/stream-render.js +2 -1
- package/dist/lib/session/tool-calls.js +2 -5
- package/dist/lib/session/trajectory-html.js +2 -1
- package/dist/lib/session/trajectory.js +3 -12
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/share/publish.d.ts +53 -5
- package/dist/lib/share/publish.js +99 -17
- package/dist/lib/share/worker-template.js +582 -57
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +16 -0
- package/dist/lib/state.js +178 -46
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +2 -0
- package/package.json +2 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved `auth` bundle fleet sync as a `PeriodicService` (PHNX-2371).
|
|
3
|
+
*
|
|
4
|
+
* Each daemon that has a local file-backed `auth` bundle pushes it to pinned
|
|
5
|
+
* fleet devices that do not yet have it. The destination auto-provisions its
|
|
6
|
+
* own machine-local key; no passphrase is forwarded. Single-executor per
|
|
7
|
+
* destination: this process only writes the remote's own store.
|
|
8
|
+
*/
|
|
9
|
+
import { BasePeriodicService, type DaemonContext } from './service.js';
|
|
10
|
+
import type { DaemonServiceId } from '../daemon-services.js';
|
|
11
|
+
export declare class AuthSyncService extends BasePeriodicService {
|
|
12
|
+
readonly id: DaemonServiceId;
|
|
13
|
+
readonly intervalMs: number;
|
|
14
|
+
readonly deadlineMs: number;
|
|
15
|
+
readonly startupDelayMs = 60000;
|
|
16
|
+
protected onStart(_ctx: DaemonContext): Promise<void>;
|
|
17
|
+
protected onStop(): Promise<void>;
|
|
18
|
+
protected onTick(ctx: DaemonContext): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved `auth` bundle fleet sync as a `PeriodicService` (PHNX-2371).
|
|
3
|
+
*
|
|
4
|
+
* Each daemon that has a local file-backed `auth` bundle pushes it to pinned
|
|
5
|
+
* fleet devices that do not yet have it. The destination auto-provisions its
|
|
6
|
+
* own machine-local key; no passphrase is forwarded. Single-executor per
|
|
7
|
+
* destination: this process only writes the remote's own store.
|
|
8
|
+
*/
|
|
9
|
+
import { BasePeriodicService } from './service.js';
|
|
10
|
+
const AUTH_SYNC_TICK_MS = 15 * 60_000;
|
|
11
|
+
const AUTH_SYNC_DEADLINE_MS = 2 * 60_000;
|
|
12
|
+
const AUTH_SYNC_KICKOFF_MS = 60_000;
|
|
13
|
+
export class AuthSyncService extends BasePeriodicService {
|
|
14
|
+
id = 'auth-sync';
|
|
15
|
+
intervalMs = AUTH_SYNC_TICK_MS;
|
|
16
|
+
deadlineMs = AUTH_SYNC_DEADLINE_MS;
|
|
17
|
+
startupDelayMs = AUTH_SYNC_KICKOFF_MS;
|
|
18
|
+
async onStart(_ctx) {
|
|
19
|
+
// No connections to open — each tick re-reads the local bundle + registry.
|
|
20
|
+
}
|
|
21
|
+
async onStop() {
|
|
22
|
+
// Nothing to release — the supervisor's timer teardown is the only cleanup.
|
|
23
|
+
}
|
|
24
|
+
async onTick(ctx) {
|
|
25
|
+
const { syncReservedAuthBundle } = await import('../secrets/reserved-sync.js');
|
|
26
|
+
const result = syncReservedAuthBundle();
|
|
27
|
+
if (result.pushed.length > 0) {
|
|
28
|
+
ctx.log('INFO', `auth-sync: pushed auth to ${result.pushed.join(', ')}`);
|
|
29
|
+
}
|
|
30
|
+
for (const err of result.errors) {
|
|
31
|
+
ctx.log('WARN', `auth-sync: ${err.device}: ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -28,6 +28,7 @@ import { startHostedWebhookReceivers } from '../daemon-webhooks.js';
|
|
|
28
28
|
import { secretsBrokerSocketPath, brokerPidAlive } from '../secrets/agent.js';
|
|
29
29
|
import { redactSecrets } from '../redact.js';
|
|
30
30
|
import { getAgentsBinPath, getCliLaunch, BUN_VIRTUAL_ROOT } from '../cli-entry.js';
|
|
31
|
+
import { localBinDir } from '../platform/posixpath.js';
|
|
31
32
|
import { isSchedulerEnabled, assertSchedulerEnabled, isDaemonEnabled, resolveBrowserTaskIdleMs } from '../device-config.js';
|
|
32
33
|
import { reapTerminalRoutineProcesses } from '../routine-process-cleanup.js';
|
|
33
34
|
import { recordSubsystemOk, recordSubsystemError, recordSubsystemErrorReason, readSubsystemHealth, SUBSYSTEM_DAEMON_START } from '../daemon-health.js';
|
|
@@ -43,6 +44,7 @@ import { WatchdogService } from './watchdog-service.js';
|
|
|
43
44
|
import { DeviceProbeService } from './device-probe-service.js';
|
|
44
45
|
import { SelfHealService } from './self-heal-service.js';
|
|
45
46
|
import { KeychainReapService } from './keychain-reap-service.js';
|
|
47
|
+
import { AuthSyncService } from './auth-sync-service.js';
|
|
46
48
|
import { StateDirCheckService } from './state-dir-check-service.js';
|
|
47
49
|
import { emit, emitRoutineEnd } from '../feed/events.js';
|
|
48
50
|
import { readDaemonServicesConfig, drainDaemonServiceRestartQueue } from '../daemon-services.js';
|
|
@@ -934,6 +936,10 @@ export async function runDaemon() {
|
|
|
934
936
|
supervisor.register(new KeychainReapService());
|
|
935
937
|
else
|
|
936
938
|
log('INFO', 'Keychain-reap service disabled');
|
|
939
|
+
if (isEnabled('auth-sync'))
|
|
940
|
+
supervisor.register(new AuthSyncService());
|
|
941
|
+
else
|
|
942
|
+
log('INFO', 'Auth-sync service disabled');
|
|
937
943
|
await supervisor.startAll({ log });
|
|
938
944
|
activeServiceSupervisor = supervisor;
|
|
939
945
|
// scheduler.enabled=false in this machine's device doc means NO routines fire
|
|
@@ -1800,10 +1806,23 @@ export function getDaemonLaunch(agentsBin = getAgentsBinPath()) {
|
|
|
1800
1806
|
function daemonNodeBinDir() {
|
|
1801
1807
|
return path.dirname(process.execPath);
|
|
1802
1808
|
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Login-shell user-bin dirs a service-manager-started daemon would otherwise
|
|
1811
|
+
* miss. systemd/launchd pin PATH and never source `~/.profile`, so they never
|
|
1812
|
+
* see `~/.rush/bin` (where `rush` lands) or `~/.local/bin` (XDG user-bin).
|
|
1813
|
+
* Monitor `notify` and dispatched `agents run` children inherit this PATH;
|
|
1814
|
+
* without these dirs the rush-backed owner channel fails with
|
|
1815
|
+
* `rush CLI not found on PATH` (PHNX-3075) while an interactive shell on the
|
|
1816
|
+
* same box succeeds. Uses the same HOME the manifest bakes (RUSH-2639).
|
|
1817
|
+
*/
|
|
1818
|
+
function daemonUserBinDirs() {
|
|
1819
|
+
const home = serviceManifestHomeEnv().HOME;
|
|
1820
|
+
return [path.join(home, '.rush', 'bin'), localBinDir(home)];
|
|
1821
|
+
}
|
|
1803
1822
|
/**
|
|
1804
1823
|
* The full PATH value the daemon service manifest pins, in order: the directory
|
|
1805
|
-
* of the `agents` shim itself FIRST, then the Node runtime dir, then
|
|
1806
|
-
* platform's system dirs.
|
|
1824
|
+
* of the `agents` shim itself FIRST, then the Node runtime dir, then login-shell
|
|
1825
|
+
* user-bin dirs (`~/.rush/bin`, `~/.local/bin`), then the platform's system dirs.
|
|
1807
1826
|
*
|
|
1808
1827
|
* The shim's own dir must lead so a scheduled `command` routine that shells out
|
|
1809
1828
|
* to the bare name `agents` (`/bin/sh -c 'agents repo pull system'`) resolves the
|
|
@@ -1814,12 +1833,19 @@ function daemonNodeBinDir() {
|
|
|
1814
1833
|
*
|
|
1815
1834
|
* The Node runtime dir stays second so the shim's shebang (`#!/usr/bin/env node`)
|
|
1816
1835
|
* still resolves the exact Node that installed the service — never an ancient
|
|
1817
|
-
* system node or a pruned nvm version.
|
|
1836
|
+
* system node or a pruned nvm version. User-bin dirs sit after that pair so a
|
|
1837
|
+
* `~/.local/bin/agents` cannot shadow the daemon binary, but `rush` (and other
|
|
1838
|
+
* login-shell CLIs) still resolve. Deduped across the whole list, so a
|
|
1818
1839
|
* Node/shim dir that already appears among the system dirs (e.g. a
|
|
1819
1840
|
* `/usr/local/bin` install) never doubles.
|
|
1820
1841
|
*/
|
|
1821
1842
|
function daemonPathValue(agentsBin, systemDirs) {
|
|
1822
|
-
return [...new Set([
|
|
1843
|
+
return [...new Set([
|
|
1844
|
+
path.dirname(agentsBin),
|
|
1845
|
+
daemonNodeBinDir(),
|
|
1846
|
+
...daemonUserBinDirs(),
|
|
1847
|
+
...systemDirs,
|
|
1848
|
+
])].join(':');
|
|
1823
1849
|
}
|
|
1824
1850
|
/**
|
|
1825
1851
|
* Build the argv to relaunch the `agents` CLI with the given subcommand args.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* enabled without pulling in the whole daemon lifecycle.
|
|
8
8
|
*/
|
|
9
9
|
/** Every service the daemon can host. IDs are kebab-case and stable. */
|
|
10
|
-
export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'keychain-reap' | 'account-state' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index';
|
|
10
|
+
export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'keychain-reap' | 'account-state' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync';
|
|
11
11
|
/** Human-readable metadata for each service. */
|
|
12
12
|
export interface DaemonServiceDef {
|
|
13
13
|
id: DaemonServiceId;
|
|
@@ -72,6 +72,11 @@ export const DAEMON_SERVICES = [
|
|
|
72
72
|
title: 'Session-index warm',
|
|
73
73
|
description: 'Keeps this host\'s transcript index current so a locally-started session is discoverable within seconds.',
|
|
74
74
|
},
|
|
75
|
+
{
|
|
76
|
+
id: 'auth-sync',
|
|
77
|
+
title: 'Auth bundle sync',
|
|
78
|
+
description: 'Pushes the reserved file-backed auth bundle (setup-tokens) to pinned fleet devices that do not yet have it.',
|
|
79
|
+
},
|
|
75
80
|
];
|
|
76
81
|
/** Stable order of service IDs. */
|
|
77
82
|
export const DAEMON_SERVICE_IDS = DAEMON_SERVICES.map((s) => s.id);
|
|
@@ -223,9 +223,9 @@ export declare function isDaemonEnabled(): boolean;
|
|
|
223
223
|
/**
|
|
224
224
|
* Throw when the daemon is disabled on this machine, naming the setting and
|
|
225
225
|
* the fix. Every AUTO-start surface (routines add/start/catchup/webhook,
|
|
226
|
-
* `ensureDaemonStarted`) refuses with this before calling
|
|
227
|
-
* `agents daemon start` is the deliberate override and does
|
|
228
|
-
* disable only blocks auto-start, mirroring `systemctl disable`.
|
|
226
|
+
* monitors add, `ensureDaemonStarted`) refuses with this before calling
|
|
227
|
+
* `startDaemon()`. `agents daemon start` is the deliberate override and does
|
|
228
|
+
* NOT call this — disable only blocks auto-start, mirroring `systemctl disable`.
|
|
229
229
|
*/
|
|
230
230
|
export declare function assertDaemonEnabled(): void;
|
|
231
231
|
/**
|
|
@@ -145,7 +145,7 @@ export const CONFIG_KEYS = [
|
|
|
145
145
|
defaultValue: true,
|
|
146
146
|
description: 'Whether the daemon may run on this device at all (secrets broker, browser IPC, watchdog, and the ' +
|
|
147
147
|
'routines scheduler). Disabling is the top-level kill switch: nothing auto-starts the daemon while it ' +
|
|
148
|
-
'is set, including `routines add`/`routines start`/`routines catchup`/webhook triggers. ' +
|
|
148
|
+
'is set, including `routines add`/`routines start`/`routines catchup`/`monitors add`/webhook triggers. ' +
|
|
149
149
|
'`agents daemon start` still starts it explicitly.',
|
|
150
150
|
},
|
|
151
151
|
{
|
|
@@ -176,7 +176,7 @@ export const CONFIG_KEYS = [
|
|
|
176
176
|
visibility: 'machine',
|
|
177
177
|
type: 'bool',
|
|
178
178
|
defaultValue: false,
|
|
179
|
-
description: "Whether other fleet machines may drive THIS device's browser over `browser --device <this-device
|
|
179
|
+
description: "Whether other fleet machines may drive THIS device's browser over `browser --device <this-device>` or `agents ssh <this-device> …` (`agents browser` / `ag browser` / standalone `browser`). " +
|
|
180
180
|
'Default off — a fleet-remote drive is refused until the owner runs `agents browser remote-control on`.',
|
|
181
181
|
},
|
|
182
182
|
{
|
|
@@ -813,9 +813,9 @@ export function isDaemonEnabled() {
|
|
|
813
813
|
/**
|
|
814
814
|
* Throw when the daemon is disabled on this machine, naming the setting and
|
|
815
815
|
* the fix. Every AUTO-start surface (routines add/start/catchup/webhook,
|
|
816
|
-
* `ensureDaemonStarted`) refuses with this before calling
|
|
817
|
-
* `agents daemon start` is the deliberate override and does
|
|
818
|
-
* disable only blocks auto-start, mirroring `systemctl disable`.
|
|
816
|
+
* monitors add, `ensureDaemonStarted`) refuses with this before calling
|
|
817
|
+
* `startDaemon()`. `agents daemon start` is the deliberate override and does
|
|
818
|
+
* NOT call this — disable only blocks auto-start, mirroring `systemctl disable`.
|
|
819
819
|
*/
|
|
820
820
|
export function assertDaemonEnabled() {
|
|
821
821
|
if (isDaemonEnabled())
|
|
@@ -37,6 +37,26 @@ export declare function fleetDialTarget(device: DeviceProfile): string;
|
|
|
37
37
|
* login).
|
|
38
38
|
*/
|
|
39
39
|
export declare function wrapRemoteCommand(device: DeviceProfile, cmd: string[]): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* True when `cmd` is a browser drive: `agents browser …`, `ag browser …`, or
|
|
42
|
+
* the standalone `browser` binary (`cli/package.json` `bin.browser` →
|
|
43
|
+
* `src/browser.ts`), including the quoted single-string form
|
|
44
|
+
* `agents ssh box 'browser …'`. The `--device` passthrough stamps
|
|
45
|
+
* {@link markFleetRemote} on this shape so the far-side consent gate can fire;
|
|
46
|
+
* `agents ssh` must too (PHNX-3065).
|
|
47
|
+
*/
|
|
48
|
+
export declare function isAgentsBrowserDrive(cmd: string[]): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Prefix a remote command so the far side sees `AGENTS_FLEET_REMOTE=1` — the
|
|
51
|
+
* marker the browser consent gate reads. `wrapRemoteCommand` joins the argv
|
|
52
|
+
* with spaces (POSIX) or base64-encodes it for PowerShell, so a
|
|
53
|
+
* shell-appropriate leading token rides through both: `env VAR=1 …` on POSIX,
|
|
54
|
+
* `$env:VAR='1'; …` on PowerShell.
|
|
55
|
+
*
|
|
56
|
+
* Already-marked argv (the `--device` fan-out stamps this before
|
|
57
|
+
* {@link buildSshInvocation}) is left unchanged so the prefix is not doubled.
|
|
58
|
+
*/
|
|
59
|
+
export declare function markFleetRemote(cmd: string[], device: Pick<DeviceProfile, 'shell'>): string[];
|
|
40
60
|
/**
|
|
41
61
|
* Build the remote command that starts an INTERACTIVE LOGIN shell inside a
|
|
42
62
|
* mirrored project directory, falling back to the remote home when that
|
|
@@ -97,6 +117,12 @@ export declare function deviceIdentityArgs(device: DeviceProfile): string[];
|
|
|
97
117
|
* to the remote home), matching `agents run --device`. It is ignored when a `cmd`
|
|
98
118
|
* is given: an explicit command keeps its current cwd (the remote home) and its
|
|
99
119
|
* behavior unchanged (RUSH-2412).
|
|
120
|
+
*
|
|
121
|
+
* An `agents browser …` / `ag browser …` / standalone `browser …` command is
|
|
122
|
+
* prefixed with {@link markFleetRemote} so the far-side consent gate sees
|
|
123
|
+
* `AGENTS_FLEET_REMOTE=1` the same way `browser --device` does. The local `env`
|
|
124
|
+
* overlay here is for the ssh *client* (askpass); it cannot carry the marker —
|
|
125
|
+
* OpenSSH does not forward arbitrary env (PHNX-3065).
|
|
100
126
|
*/
|
|
101
127
|
export declare function buildSshInvocation(device: DeviceProfile, cmd: string[], askpassShimPath: string, hostKey?: SshHostKeyOptions, opts?: {
|
|
102
128
|
agentOnly?: boolean;
|
|
@@ -86,6 +86,43 @@ export function wrapRemoteCommand(device, cmd) {
|
|
|
86
86
|
}
|
|
87
87
|
return joined;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* True when `cmd` is a browser drive: `agents browser …`, `ag browser …`, or
|
|
91
|
+
* the standalone `browser` binary (`cli/package.json` `bin.browser` →
|
|
92
|
+
* `src/browser.ts`), including the quoted single-string form
|
|
93
|
+
* `agents ssh box 'browser …'`. The `--device` passthrough stamps
|
|
94
|
+
* {@link markFleetRemote} on this shape so the far-side consent gate can fire;
|
|
95
|
+
* `agents ssh` must too (PHNX-3065).
|
|
96
|
+
*/
|
|
97
|
+
export function isAgentsBrowserDrive(cmd) {
|
|
98
|
+
if (cmd.length === 0)
|
|
99
|
+
return false;
|
|
100
|
+
const tokens = cmd.length === 1 && /\s/.test(cmd[0]) ? cmd[0].trim().split(/\s+/) : cmd;
|
|
101
|
+
const bin = tokens[0];
|
|
102
|
+
if (bin === 'browser')
|
|
103
|
+
return true;
|
|
104
|
+
return (bin === 'agents' || bin === 'ag') && tokens[1] === 'browser';
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Prefix a remote command so the far side sees `AGENTS_FLEET_REMOTE=1` — the
|
|
108
|
+
* marker the browser consent gate reads. `wrapRemoteCommand` joins the argv
|
|
109
|
+
* with spaces (POSIX) or base64-encodes it for PowerShell, so a
|
|
110
|
+
* shell-appropriate leading token rides through both: `env VAR=1 …` on POSIX,
|
|
111
|
+
* `$env:VAR='1'; …` on PowerShell.
|
|
112
|
+
*
|
|
113
|
+
* Already-marked argv (the `--device` fan-out stamps this before
|
|
114
|
+
* {@link buildSshInvocation}) is left unchanged so the prefix is not doubled.
|
|
115
|
+
*/
|
|
116
|
+
export function markFleetRemote(cmd, device) {
|
|
117
|
+
if (device.shell === 'powershell') {
|
|
118
|
+
return cmd[0] === `$env:AGENTS_FLEET_REMOTE='1';`
|
|
119
|
+
? cmd
|
|
120
|
+
: [`$env:AGENTS_FLEET_REMOTE='1';`, ...cmd];
|
|
121
|
+
}
|
|
122
|
+
return cmd[0] === 'env' && cmd[1] === 'AGENTS_FLEET_REMOTE=1'
|
|
123
|
+
? cmd
|
|
124
|
+
: ['env', 'AGENTS_FLEET_REMOTE=1', ...cmd];
|
|
125
|
+
}
|
|
89
126
|
/**
|
|
90
127
|
* Build the remote command that starts an INTERACTIVE LOGIN shell inside a
|
|
91
128
|
* mirrored project directory, falling back to the remote home when that
|
|
@@ -156,6 +193,12 @@ export function deviceIdentityArgs(device) {
|
|
|
156
193
|
* to the remote home), matching `agents run --device`. It is ignored when a `cmd`
|
|
157
194
|
* is given: an explicit command keeps its current cwd (the remote home) and its
|
|
158
195
|
* behavior unchanged (RUSH-2412).
|
|
196
|
+
*
|
|
197
|
+
* An `agents browser …` / `ag browser …` / standalone `browser …` command is
|
|
198
|
+
* prefixed with {@link markFleetRemote} so the far-side consent gate sees
|
|
199
|
+
* `AGENTS_FLEET_REMOTE=1` the same way `browser --device` does. The local `env`
|
|
200
|
+
* overlay here is for the ssh *client* (askpass); it cannot carry the marker —
|
|
201
|
+
* OpenSSH does not forward arbitrary env (PHNX-3065).
|
|
159
202
|
*/
|
|
160
203
|
export function buildSshInvocation(device, cmd, askpassShimPath, hostKey = {}, opts = {}) {
|
|
161
204
|
// The effective profile: central config (ssh.*/platform/user) overlaid on
|
|
@@ -166,9 +209,13 @@ export function buildSshInvocation(device, cmd, askpassShimPath, hostKey = {}, o
|
|
|
166
209
|
// wrapper (interactiveCwd), which is an interactive login too and still needs
|
|
167
210
|
// a real tty below.
|
|
168
211
|
const interactive = cmd.length === 0;
|
|
212
|
+
// Stamp the consent marker on the REMOTE command, not the local ssh env:
|
|
213
|
+
// SSH_ASKPASS lives on this side; AGENTS_FLEET_REMOTE must be visible to the
|
|
214
|
+
// process that runs on the peer.
|
|
215
|
+
const remoteCmd = !interactive && isAgentsBrowserDrive(cmd) ? markFleetRemote(cmd, device) : cmd;
|
|
169
216
|
const remote = interactive
|
|
170
217
|
? buildInteractiveShellCommand(device, opts.interactiveCwd)
|
|
171
|
-
: wrapRemoteCommand(device,
|
|
218
|
+
: wrapRemoteCommand(device, remoteCmd);
|
|
172
219
|
const env = {};
|
|
173
220
|
const args = [
|
|
174
221
|
...hostKeyCheckingOpts(hostKey.pinned ?? false, hostKey.knownHostsFile),
|
|
@@ -13,7 +13,7 @@ export type FindingSeverity = 'critical' | 'warning';
|
|
|
13
13
|
* the JSON consumer group by kind. */
|
|
14
14
|
/** Every finding class. Severity is NOT annotated here — {@link FINDING_SEVERITY}
|
|
15
15
|
* below owns it, and a second copy in these comments is a fourth place to drift. */
|
|
16
|
-
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "hook-runtime-broken", "hook-runtime-visibility-unavailable", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "env-secret-export", "exec-policy", "ssh-key-enrollment", "stale-cli", "binary-shadow", "owner-sink-unreachable"];
|
|
16
|
+
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "hook-runtime-broken", "hook-runtime-visibility-unavailable", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "env-secret-export", "auth-bundle-wrong-backend", "exec-policy", "ssh-key-enrollment", "stale-cli", "binary-shadow", "owner-sink-unreachable"];
|
|
17
17
|
/**
|
|
18
18
|
* The severity each kind is emitted with - the SINGLE source of truth, read by
|
|
19
19
|
* the builders below and asserted against both prose rubrics by
|
|
@@ -100,6 +100,10 @@ export interface LocalFindingInputs {
|
|
|
100
100
|
* scan reports clean while the leak is still in flight (RUSH-1968). Never the
|
|
101
101
|
* value — only whether it is set. */
|
|
102
102
|
masterPassphraseInEnv?: boolean;
|
|
103
|
+
/** True when the reserved `auth` bundle exists on a non-file backend
|
|
104
|
+
* (SEC-GAP-3): usage/probe ignores the setup-tokens and falls through to
|
|
105
|
+
* Touch ID. Collected by `inspectReservedAuthBundle`. */
|
|
106
|
+
authBundleWrongBackend?: boolean;
|
|
103
107
|
/** The effective PowerShell execution policy and the platform it was read on.
|
|
104
108
|
* Only `win32` yields a finding — the `agents.ps1` launcher is Windows-only. */
|
|
105
109
|
execPolicy?: {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* never-synced · stale · repo-behind · repo-drift · version-skew ·
|
|
27
27
|
* fleet-resource-gap · hook-runtime-visibility-unavailable · orphan · duplicate-hook ·
|
|
28
28
|
* duplicate-hook-drift · host-cli-missing · host-cli-invalid ·
|
|
29
|
-
* rc-secret-export · env-secret-export · exec-policy · stale-cli ·
|
|
29
|
+
* rc-secret-export · env-secret-export · auth-bundle-wrong-backend · exec-policy · stale-cli ·
|
|
30
30
|
* binary-shadow.
|
|
31
31
|
* (RUSH-2162 moved never-synced and duplicate-hook-drift to WARNING: both are
|
|
32
32
|
* stale-sync states one `agents sync` resolves, not "needs you now".)
|
|
@@ -102,6 +102,7 @@ export const ALL_FINDING_KINDS = [
|
|
|
102
102
|
'duplicate-hook-drift', // …with differing content, so a stale copy can disagree
|
|
103
103
|
'rc-secret-export', // credential-shaped export in a shell rc file
|
|
104
104
|
'env-secret-export', // the file-store master key live in THIS process's env
|
|
105
|
+
'auth-bundle-wrong-backend', // reserved `auth` bundle exists but is not file-backed
|
|
105
106
|
'exec-policy', // Windows execution policy blocks agents.ps1
|
|
106
107
|
'ssh-key-enrollment', // Windows OpenSSH public-key path/content/ACL is invalid
|
|
107
108
|
'stale-cli',
|
|
@@ -150,6 +151,7 @@ export const FINDING_SEVERITY = {
|
|
|
150
151
|
'host-cli-invalid': 'warning',
|
|
151
152
|
'rc-secret-export': 'warning',
|
|
152
153
|
'env-secret-export': 'warning',
|
|
154
|
+
'auth-bundle-wrong-backend': 'warning',
|
|
153
155
|
'exec-policy': 'warning',
|
|
154
156
|
'ssh-key-enrollment': 'critical',
|
|
155
157
|
'stale-cli': 'warning',
|
|
@@ -269,6 +271,8 @@ export function remediationFor(finding) {
|
|
|
269
271
|
// that inherited it — an editor, a tmux server, the agents daemon — and
|
|
270
272
|
// each keeps handing it to new children until IT restarts.
|
|
271
273
|
return 'unset at the source, then restart every process that inherited it (shells, editor, tmux, agents daemon)';
|
|
274
|
+
case 'auth-bundle-wrong-backend':
|
|
275
|
+
return 'agents secrets delete auth --yes && agents secrets create auth --backend file';
|
|
272
276
|
case 'exec-policy':
|
|
273
277
|
return 'Set-ExecutionPolicy -Scope CurrentUser RemoteSigned';
|
|
274
278
|
case 'ssh-key-enrollment':
|
|
@@ -519,6 +523,9 @@ export function buildLocalFindings(input) {
|
|
|
519
523
|
const envFinding = envSecretFinding(device, input.masterPassphraseInEnv ?? false);
|
|
520
524
|
if (envFinding)
|
|
521
525
|
out.push(envFinding);
|
|
526
|
+
const authFinding = authBundleWrongBackendFinding(device, input.authBundleWrongBackend ?? false);
|
|
527
|
+
if (authFinding)
|
|
528
|
+
out.push(authFinding);
|
|
522
529
|
// Windows execution policy blocking the generated agents.ps1 launcher.
|
|
523
530
|
const policyFinding = execPolicyFinding(device, input.execPolicy);
|
|
524
531
|
if (policyFinding)
|
|
@@ -688,6 +695,15 @@ function envSecretFinding(device, present) {
|
|
|
688
695
|
+ '(Expected inside a release sign context, which sets it deliberately.)',
|
|
689
696
|
});
|
|
690
697
|
}
|
|
698
|
+
function authBundleWrongBackendFinding(device, present) {
|
|
699
|
+
if (!present)
|
|
700
|
+
return null;
|
|
701
|
+
return finding({
|
|
702
|
+
severity: FINDING_SEVERITY['auth-bundle-wrong-backend'], kind: 'auth-bundle-wrong-backend', device,
|
|
703
|
+
message: "reserved secrets bundle 'auth' exists but is not file-backed — "
|
|
704
|
+
+ 'usage/probe ignores the setup-tokens and falls through to the interactive login',
|
|
705
|
+
});
|
|
706
|
+
}
|
|
691
707
|
function execPolicyFinding(device, execPolicy) {
|
|
692
708
|
if (!execPolicy || execPolicy.platform !== 'win32')
|
|
693
709
|
return null;
|
|
@@ -1038,6 +1054,8 @@ function warningSubject(f) {
|
|
|
1038
1054
|
return 'shell rc';
|
|
1039
1055
|
if (f.kind === 'env-secret-export')
|
|
1040
1056
|
return 'environment';
|
|
1057
|
+
if (f.kind === 'auth-bundle-wrong-backend')
|
|
1058
|
+
return 'auth bundle';
|
|
1041
1059
|
if (f.kind === 'exec-policy')
|
|
1042
1060
|
return 'PowerShell';
|
|
1043
1061
|
if (f.kind === 'fleet-resource-gap')
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentId, Mode } from './types.js';
|
|
2
|
+
import { type UsernsStatus } from './linux-userns.js';
|
|
2
3
|
/**
|
|
3
4
|
* Agent execution modes. Canonical name `skip` (dangerously skip permissions);
|
|
4
5
|
* `full` is accepted as a permanent silent alias via normalizeMode().
|
|
@@ -88,6 +89,33 @@ export interface ModeWarningState {
|
|
|
88
89
|
export declare function defaultModeFor(agent: AgentId): Mode;
|
|
89
90
|
/** Safe mode used when the user did not provide --mode or a configured default. */
|
|
90
91
|
export declare function implicitModeFor(agent: AgentId): ExecMode;
|
|
92
|
+
/**
|
|
93
|
+
* Preflight for Codex's Linux sandbox. Codex ≥0.146 sandboxes `read-only` and
|
|
94
|
+
* `workspace-write` runs with a bundled bubblewrap that needs an unprivileged
|
|
95
|
+
* user namespace; on a box that restricts it (Ubuntu 24.04
|
|
96
|
+
* `apparmor_restrict_unprivileged_userns=1`) bwrap dies with "setting up uid map:
|
|
97
|
+
* Permission denied" and a HEADLESS codex run lands zero tools — no file writes,
|
|
98
|
+
* no shell — while still reporting a completed turn. That silent under-delivery
|
|
99
|
+
* is what breaks `agents teams` codex teammates (always headless + workspace-write)
|
|
100
|
+
* and headless `agents run codex` alike on the fleet. Returns a loud, actionable
|
|
101
|
+
* message to fail the launch with instead of spawning that doomed run; returns
|
|
102
|
+
* null when the run is fine to proceed.
|
|
103
|
+
*
|
|
104
|
+
* Deliberately scoped: only `codex`, only Linux, only a HEADLESS run (an
|
|
105
|
+
* interactive TUI surfaces the bwrap error to the operator itself), and only a
|
|
106
|
+
* SANDBOXED mode — `skip` is codex `--dangerously-bypass-approvals-and-sandbox`,
|
|
107
|
+
* which uses no bwrap and is unaffected. The intended auto=workspace-write config
|
|
108
|
+
* is never weakened here; the run fails loud rather than silently downgrading.
|
|
109
|
+
* PHNX-3285.
|
|
110
|
+
*/
|
|
111
|
+
export declare function codexSandboxPreflight(args: {
|
|
112
|
+
agent: AgentId;
|
|
113
|
+
platform: NodeJS.Platform;
|
|
114
|
+
interactive: boolean;
|
|
115
|
+
mode: Mode;
|
|
116
|
+
userns: UsernsStatus;
|
|
117
|
+
machine: string;
|
|
118
|
+
}): string | null;
|
|
91
119
|
/** Reasoning effort levels passed to agents that support them. 'auto' defers to the agent's default. */
|
|
92
120
|
export type ExecEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
|
|
93
121
|
/** Options for spawning an agent process. Omitting `prompt` launches the CLI interactively. */
|
package/dist/lib/exec.js
CHANGED
|
@@ -32,6 +32,7 @@ import { isTmuxEnabled, selfConfiguredDeviceRole } from './device-config.js';
|
|
|
32
32
|
import { machineId } from './machine-id.js';
|
|
33
33
|
import { shellQuote } from './ssh-exec.js';
|
|
34
34
|
import { codexEditWritableRoots, codexPolicyArgs } from './codex-policy.js';
|
|
35
|
+
import { probeUnprivilegedUserns } from './linux-userns.js';
|
|
35
36
|
import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
36
37
|
import { applyAddDirs } from './add-dir.js';
|
|
37
38
|
import { applyActiveRulesPresetAtRun } from './rules/run-sync.js';
|
|
@@ -181,6 +182,49 @@ export function defaultModeFor(agent) {
|
|
|
181
182
|
export function implicitModeFor(agent) {
|
|
182
183
|
return agent === 'codex' ? 'edit' : 'plan';
|
|
183
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Preflight for Codex's Linux sandbox. Codex ≥0.146 sandboxes `read-only` and
|
|
187
|
+
* `workspace-write` runs with a bundled bubblewrap that needs an unprivileged
|
|
188
|
+
* user namespace; on a box that restricts it (Ubuntu 24.04
|
|
189
|
+
* `apparmor_restrict_unprivileged_userns=1`) bwrap dies with "setting up uid map:
|
|
190
|
+
* Permission denied" and a HEADLESS codex run lands zero tools — no file writes,
|
|
191
|
+
* no shell — while still reporting a completed turn. That silent under-delivery
|
|
192
|
+
* is what breaks `agents teams` codex teammates (always headless + workspace-write)
|
|
193
|
+
* and headless `agents run codex` alike on the fleet. Returns a loud, actionable
|
|
194
|
+
* message to fail the launch with instead of spawning that doomed run; returns
|
|
195
|
+
* null when the run is fine to proceed.
|
|
196
|
+
*
|
|
197
|
+
* Deliberately scoped: only `codex`, only Linux, only a HEADLESS run (an
|
|
198
|
+
* interactive TUI surfaces the bwrap error to the operator itself), and only a
|
|
199
|
+
* SANDBOXED mode — `skip` is codex `--dangerously-bypass-approvals-and-sandbox`,
|
|
200
|
+
* which uses no bwrap and is unaffected. The intended auto=workspace-write config
|
|
201
|
+
* is never weakened here; the run fails loud rather than silently downgrading.
|
|
202
|
+
* PHNX-3285.
|
|
203
|
+
*/
|
|
204
|
+
export function codexSandboxPreflight(args) {
|
|
205
|
+
if (args.agent !== 'codex')
|
|
206
|
+
return null;
|
|
207
|
+
if (args.platform !== 'linux')
|
|
208
|
+
return null;
|
|
209
|
+
if (args.interactive)
|
|
210
|
+
return null;
|
|
211
|
+
if (args.mode === 'skip')
|
|
212
|
+
return null;
|
|
213
|
+
if (args.userns.state !== 'blocked')
|
|
214
|
+
return null;
|
|
215
|
+
const reason = args.userns.reason ?? 'unprivileged user namespaces are restricted';
|
|
216
|
+
return (`codex's Linux sandbox can't start on ${args.machine}: ${reason}, so codex's ` +
|
|
217
|
+
`bundled bubblewrap fails with "bwrap: setting up uid map: Permission denied" ` +
|
|
218
|
+
`and a headless run lands zero tools. (PHNX-3285)\n` +
|
|
219
|
+
`\n` +
|
|
220
|
+
`Enable it once on this box — keeps codex's workspace-write sandbox intact:\n` +
|
|
221
|
+
` echo 'kernel.apparmor_restrict_unprivileged_userns=0' | sudo tee /etc/sysctl.d/60-codex-userns.conf\n` +
|
|
222
|
+
` sudo sysctl --system\n` +
|
|
223
|
+
` # verify: unshare --user --map-root-user true (exit 0 = fixed)\n` +
|
|
224
|
+
`(agents-cli ships cli/scripts/enable-codex-sandbox.sh to apply + verify this.)\n` +
|
|
225
|
+
`\n` +
|
|
226
|
+
`Or run codex WITHOUT a filesystem sandbox instead: add --mode skip.`);
|
|
227
|
+
}
|
|
184
228
|
/**
|
|
185
229
|
* Identity a custom-harness run stamps on env / pid-registry / sidecars.
|
|
186
230
|
* `agent` is the host CLI; `harnessName` is the profile the user launched.
|
|
@@ -797,8 +841,13 @@ export function buildExecCommand(options) {
|
|
|
797
841
|
// that maps to reasoning effort on a single-model harness (e.g. Grok, where the
|
|
798
842
|
// tier IS the effort dial). `modelVersion` is null when no version resolves;
|
|
799
843
|
// `tierModel` is the concrete model a tier resolved to (null => drop the flag).
|
|
844
|
+
// Prefer the user's explicit --model. Codex falls back to ~/.codex/config.toml
|
|
845
|
+
// (per-version CODEX_HOME may not carry that setting). OpenCode has no model
|
|
846
|
+
// env var — a custom-harness pin lives in OPENCODE_MODEL and must become
|
|
847
|
+
// `--model` here, or the host silently uses its configured default (PHNX-2577).
|
|
800
848
|
const effectiveModel = options.model
|
|
801
|
-
?? (options.agent === 'codex' ? readCodexConfiguredModel() : undefined)
|
|
849
|
+
?? (options.agent === 'codex' ? readCodexConfiguredModel() : undefined)
|
|
850
|
+
?? (options.agent === 'opencode' ? options.env?.OPENCODE_MODEL : undefined);
|
|
802
851
|
const modelVersion = effectiveModel && template.modelFlag
|
|
803
852
|
? (options.version || resolveVersion(options.agent, options.cwd || process.cwd()))
|
|
804
853
|
: null;
|
|
@@ -906,12 +955,8 @@ export function buildExecCommand(options) {
|
|
|
906
955
|
else if (options.sessionId && options.agent === 'claude') {
|
|
907
956
|
cmd.push('--session-id', options.sessionId);
|
|
908
957
|
}
|
|
909
|
-
// Add model.
|
|
910
|
-
//
|
|
911
|
-
// Codex runs under a per-version CODEX_HOME (see buildExecEnv) that may not
|
|
912
|
-
// carry that setting, so without this it silently defaults to gpt-5.3-codex,
|
|
913
|
-
// which a ChatGPT-tier account can't use (HTTP 400). Forwarding keeps the
|
|
914
|
-
// user's default model setup for both `agents run` and `agents teams`.
|
|
958
|
+
// Add model. `effectiveModel` already preferred --model, then Codex's
|
|
959
|
+
// configured default, then an OpenCode custom-harness pin (OPENCODE_MODEL).
|
|
915
960
|
if (effectiveModel && template.modelFlag) {
|
|
916
961
|
if (tierResolved) {
|
|
917
962
|
// Cost tier (cheap|default|best|ultra) -> a concrete model this harness+
|
|
@@ -1653,6 +1698,27 @@ async function spawnAgent(options) {
|
|
|
1653
1698
|
const timeoutMs = options.timeout ? parseTimeout(options.timeout) : undefined;
|
|
1654
1699
|
const piped = !process.stdout.isTTY;
|
|
1655
1700
|
const interactive = resolveInteractive(options);
|
|
1701
|
+
// Fail loud before spawning a headless codex whose Linux sandbox can't start —
|
|
1702
|
+
// otherwise it burns a turn and lands zero tools (PHNX-3285). Cheap gate first
|
|
1703
|
+
// so the userns probe (one `unshare`) runs only for the exact case it applies to
|
|
1704
|
+
// (codex + Linux + headless + a sandboxed mode), never for every spawn.
|
|
1705
|
+
if (options.agent === 'codex' && process.platform === 'linux' && !interactive) {
|
|
1706
|
+
const codexMode = options.mode ? normalizeMode(options.mode) : defaultModeFor(options.agent);
|
|
1707
|
+
if (codexMode !== 'skip') {
|
|
1708
|
+
const message = codexSandboxPreflight({
|
|
1709
|
+
agent: options.agent,
|
|
1710
|
+
platform: process.platform,
|
|
1711
|
+
interactive,
|
|
1712
|
+
mode: codexMode,
|
|
1713
|
+
userns: probeUnprivilegedUserns(),
|
|
1714
|
+
machine: machineId(),
|
|
1715
|
+
});
|
|
1716
|
+
if (message) {
|
|
1717
|
+
process.stderr.write(`\x1b[31m${message}\x1b[0m\n`);
|
|
1718
|
+
return { exitCode: 1, stdout: '', stderr: message };
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1656
1722
|
// Budget live kill-switch (issue #346). For headless runs we incrementally
|
|
1657
1723
|
// parse stream-json usage off stdout, accumulate cost, and kill the child the
|
|
1658
1724
|
// moment a configured cap is crossed — exactly like the --timeout path, but
|