@ours.network/fleet 1.1.0 → 1.1.2

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.
@@ -0,0 +1,70 @@
1
+ import { createRequire } from 'node:module';
2
+ import { accessSync, constants, existsSync, realpathSync } from 'node:fs';
3
+ import { dirname, join, isAbsolute, resolve, delimiter } from 'node:path';
4
+ import { realExec } from '../exec.js';
5
+ export const VERIFIED_CODEX_ACP_VERSIONS = new Set(['1.1.7', '1.10.0']);
6
+ export function executableOnPath(command, env, cwd = process.cwd()) {
7
+ if (isAbsolute(command) || command.includes('/') || command.includes('\\'))
8
+ return resolve(cwd, command);
9
+ for (const dir of (env.PATH ?? '').split(delimiter)) {
10
+ for (const suffix of process.platform === 'win32' ? ['', '.exe', '.cmd'] : ['']) {
11
+ const path = resolve(cwd, dir, command + suffix);
12
+ try {
13
+ accessSync(path, constants.X_OK);
14
+ return path;
15
+ }
16
+ catch { /* next PATH entry */ }
17
+ }
18
+ }
19
+ throw new Error(`${command} not found on PATH`);
20
+ }
21
+ /** Match the npm Codex wrapper's platform-package/vendor selection. */
22
+ export function codexExecutable(entry) {
23
+ const canonical = realpathSync(entry);
24
+ if (!canonical.endsWith('/bin/codex.js') && !canonical.endsWith('\\bin\\codex.js'))
25
+ return canonical;
26
+ const platform = process.platform === 'android' ? 'linux' : process.platform;
27
+ const cpu = process.arch === 'x64' ? 'x86_64' : process.arch === 'arm64' ? 'aarch64' : undefined;
28
+ const target = platform === 'linux' ? `${cpu}-unknown-linux-musl`
29
+ : platform === 'darwin' ? `${cpu}-apple-darwin`
30
+ : platform === 'win32' ? `${cpu}-pc-windows-msvc` : undefined;
31
+ if (!cpu || !target)
32
+ throw new Error(`Unsupported Codex platform ${platform}/${process.arch}`);
33
+ let vendor;
34
+ try {
35
+ vendor = join(dirname(createRequire(canonical).resolve(`@openai/codex-${platform}-${process.arch}/package.json`)), 'vendor');
36
+ }
37
+ catch {
38
+ vendor = join(dirname(canonical), '..', 'vendor');
39
+ }
40
+ const name = platform === 'win32' ? 'codex.exe' : 'codex';
41
+ // 0.153 uses bin/, while the supported legacy 0.145 wrapper uses codex/.
42
+ for (const dir of ['bin', 'codex']) {
43
+ const binary = join(vendor, target, dir, name);
44
+ if (existsSync(binary))
45
+ return realpathSync(binary);
46
+ }
47
+ throw new Error(`Codex platform binary missing under ${vendor}; reinstall Fleet with optional dependencies`);
48
+ }
49
+ export async function probeCodexRuntime(adapter, env, exec = realExec, cwd = process.cwd()) {
50
+ const configured = env.CODEX_PATH;
51
+ if (!configured && !adapter.manifestPath)
52
+ throw new Error('ACP runtime is unknown for a PATH/custom adapter; set CODEX_PATH to an absolute executable or use the bundled adapter');
53
+ const entry = configured ? executableOnPath(configured, env, cwd)
54
+ : createRequire(adapter.manifestPath).resolve('@openai/codex/bin/codex.js');
55
+ const executable = codexExecutable(entry);
56
+ const result = await exec(!configured ? process.execPath : entry, !configured ? [entry, '--version'] : ['--version'], { env, timeout: 5_000 });
57
+ const version = result.stdout.match(/\b(\d+\.\d+\.\d+)\b/)?.[1];
58
+ if (result.code !== 0 || !version)
59
+ throw new Error(`Cannot read Codex version from ${entry}; check CODEX_PATH and executable permissions`);
60
+ return { source: configured ? 'CODEX_PATH' : 'bundled', entry, executable, version };
61
+ }
62
+ export function codexVersionAtLeast(version, minimum) {
63
+ const actual = version.split('.').map(Number);
64
+ const required = minimum.split('.').map(Number);
65
+ for (let i = 0; i < 3; i++) {
66
+ if (actual[i] !== required[i])
67
+ return actual[i] > required[i];
68
+ }
69
+ return true;
70
+ }
@@ -46,6 +46,9 @@ export class CodexAgentSessionAdapter {
46
46
  permissionMode: options.permissionMode,
47
47
  permissionMetadataSource: acpAdapterState(launch.adapterState).permissionMetadataSource,
48
48
  scrubObsoleteOursAutostart: true,
49
+ ...(role.monitor?.mode === 'fleet' && role.monitor.stall_recovery ? {
50
+ stallRecovery: { timeoutMs: role.monitor.stall_timeout_ms },
51
+ } : {}),
49
52
  log: options.log,
50
53
  });
51
54
  }
@@ -6,6 +6,7 @@ import { realExec } from '../exec.js';
6
6
  import { registerAdapter } from './registry.js';
7
7
  import { harnessRuntimeDir } from '../isolation/policy.js';
8
8
  import { resolveBundledAcpAgent, } from './acp-agent.js';
9
+ import { VERIFIED_CODEX_ACP_VERSIONS, probeCodexRuntime, codexVersionAtLeast } from './codex-runtime.js';
9
10
  import { CodexAgentSessionAdapter } from './codex-session.js';
10
11
  const OPTION_KEYS = [
11
12
  'launcher', 'sandbox', 'approval', 'permission_mode', 'search', 'profile', 'config', 'add_dirs',
@@ -17,7 +18,6 @@ const SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access'];
17
18
  /** Codex CLI's accepted `--ask-for-approval` values. */
18
19
  const APPROVAL_POLICIES = ['untrusted', 'on-request', 'never'];
19
20
  const NATIVE_CONFIG_ALLOWLIST = new Set(['model_reasoning_effort']);
20
- const BUNDLED_CODEX_ACP_VERSION = '1.1.7';
21
21
  const CODEX_ACP_PACKAGE = '@agentclientprotocol/codex-acp';
22
22
  const CODEX_PROXY_APPROVAL_ENV = 'OURS_FLEET_CODEX_APPROVAL';
23
23
  const CODEX_PROXY_SANDBOX_ENV = 'OURS_FLEET_CODEX_SANDBOX';
@@ -205,7 +205,7 @@ function codexAgentLaunch(role, prep) {
205
205
  /** Bind launch argv and metadata provenance to one already-completed resolution. */
206
206
  export function codexAcpLaunchForResolution(resolution) {
207
207
  const permissionMetadataSource = resolution.bundled
208
- && resolution.version === BUNDLED_CODEX_ACP_VERSION
208
+ && VERIFIED_CODEX_ACP_VERSIONS.has(resolution.version ?? '')
209
209
  && resolution.manifestPath !== undefined
210
210
  ? 'codex-acp'
211
211
  : undefined;
@@ -216,7 +216,7 @@ export function codexAcpLaunchForResolution(resolution) {
216
216
  }
217
217
  function canOverrideBundledAcpApproval() {
218
218
  const resolution = bundledCodexAcp();
219
- return resolution.bundled && resolution.version === BUNDLED_CODEX_ACP_VERSION
219
+ return resolution.bundled && VERIFIED_CODEX_ACP_VERSIONS.has(resolution.version ?? '')
220
220
  && resolution.manifestPath !== undefined && compiledProxyModule() !== undefined;
221
221
  }
222
222
  function compiledProxyModule() {
@@ -239,7 +239,7 @@ function codexAcpEnvironment(role, dirs) {
239
239
  if (role.session !== 'acp' || role.session_options?.acp?.command != null)
240
240
  return {};
241
241
  const resolution = bundledCodexAcp();
242
- if (!resolution.bundled || resolution.version !== BUNDLED_CODEX_ACP_VERSION
242
+ if (!resolution.bundled || !VERIFIED_CODEX_ACP_VERSIONS.has(resolution.version ?? '')
243
243
  || !resolution.manifestPath)
244
244
  return {};
245
245
  const runtimeDir = harnessRuntimeDir(dirs.stateDir, 'codex');
@@ -261,7 +261,7 @@ function codexAcpEnvironment(role, dirs) {
261
261
  [CODEX_PROXY_APPROVAL_ENV]: approvalPolicy(role) ?? 'on-request',
262
262
  [CODEX_PROXY_SANDBOX_ENV]: acpRuntimeSandbox(role),
263
263
  [CODEX_PROXY_MANIFEST_ENV]: resolution.manifestPath,
264
- ...(process.env.CODEX_PATH ? { [CODEX_PROXY_REAL_PATH_ENV]: process.env.CODEX_PATH } : {}),
264
+ [CODEX_PROXY_REAL_PATH_ENV]: role.env?.CODEX_PATH ?? process.env.CODEX_PATH ?? '',
265
265
  };
266
266
  }
267
267
  function encodeTomlValue(value) {
@@ -459,6 +459,22 @@ export function makeCodexAdapter(exec = realExec, transport, nativeTransport) {
459
459
  // sandbox needs this directory to exist before entry.
460
460
  if (role.isolation)
461
461
  mkdirSync(harnessRuntimeDir(dirs.stateDir, 'codex'), { recursive: true });
462
+ if (role.session === 'acp' && role.session_options?.acp?.command == null) {
463
+ const resolution = bundledCodexAcp();
464
+ if (resolution.bundled && VERIFIED_CODEX_ACP_VERSIONS.has(resolution.version ?? '')) {
465
+ let runtime;
466
+ try {
467
+ runtime = await probeCodexRuntime(resolution, { ...process.env, ...role.env }, realExec, dirs.runCwd);
468
+ }
469
+ catch (error) {
470
+ throw new Error(`Codex ACP runtime check failed: ${error.message}; check CODEX_PATH or reinstall Fleet with optional dependencies`);
471
+ }
472
+ const minimum = role.model === 'gpt-6-astra' || resolution.version === '1.10.0'
473
+ ? '0.153.3' : '0.145.0';
474
+ if (!codexVersionAtLeast(runtime.version, minimum))
475
+ throw new Error(`${role.model ?? 'Codex ACP'} requires a newer Codex (>=${minimum}): ACP selects ${runtime.executable} (${runtime.version}); upgrade Fleet or set CODEX_PATH to a supported executable in the agent/service environment`);
476
+ }
477
+ }
462
478
  return {
463
479
  // OURS_BIND_IDENTITY is the connector's startup bind seed — see the note in
464
480
  // claude-code.ts's prepareSession. It belongs on EVERY harness that runs a
package/dist/runner.js CHANGED
@@ -119,6 +119,13 @@ export function managedFleetProxyEnv(role, stateDir) {
119
119
  */
120
120
  export function harnessChildEnv(role, launchEnv, stateDir) {
121
121
  const env = { ...(launchEnv ?? {}), ...managedFleetProxyEnv(role, stateDir) };
122
+ // CODEX_PATH selects the underlying runtime, not a bypass around Fleet's proxy.
123
+ if (role.harness === 'codex' && role.session === 'acp'
124
+ && launchEnv?.OURS_FLEET_CODEX_ACP_MANIFEST && launchEnv.CODEX_PATH) {
125
+ env.CODEX_PATH = launchEnv.CODEX_PATH;
126
+ env.OURS_FLEET_REAL_CODEX_PATH = role.env?.CODEX_PATH
127
+ ?? launchEnv.OURS_FLEET_REAL_CODEX_PATH ?? '';
128
+ }
122
129
  assertModelPinReachesChild(role, env);
123
130
  return env;
124
131
  }
@@ -612,7 +619,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
612
619
  arbiter = new RoleTurnArbiter(agentSession);
613
620
  sessionHandle = arbiter;
614
621
  unsubscribeRecovery = agentSession.subscribe(event => {
615
- if (event.kind !== 'error' || !event.text)
622
+ if (event.kind !== 'error' || !event.text || event.origin?.kind === 'stall-watchdog')
616
623
  return;
617
624
  const evidence = classifyFailureText(event.text, sessionBackend, new Date(deps.now()).toISOString());
618
625
  if (evidence)
@@ -747,7 +754,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
747
754
  // valid. Keep every unproven cancellation, refusal, shutdown, and genuine
748
755
  // failure terminal so a role that never accepted its briefing is not
749
756
  // silently reported as healthy.
750
- const interruptedForWake = isRecoverableTempStartupCancellation(temp, started);
757
+ const interruptedForWake = isRecoverableTempStartupCancellation(temp, started)
758
+ || (started.outcome === 'cancelled' && started.cancellationSource === 'stall-watchdog');
751
759
  if (!started.succeeded && !interruptedForWake) {
752
760
  monitor?.stop();
753
761
  await control.close();
@@ -772,7 +780,9 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
772
780
  throw new Error(`[${name}] ${sessionLabel} startup prompt ${started.outcome}` +
773
781
  `${started.detail ? `: ${started.detail}` : ''}`);
774
782
  }
775
- if (interruptedForWake)
783
+ if (started.cancellationSource === 'stall-watchdog')
784
+ deps.log(`[${name}] ${sessionLabel} startup diagnostic recovery requires operator attention; keeping supervisor alive`);
785
+ else if (interruptedForWake)
776
786
  deps.log(`[${name}] ${sessionLabel} startup prompt cancelled by ${started.cancellationSource}; `
777
787
  + 'keeping temporary supervisor alive');
778
788
  sessionStartupComplete = true;
@@ -27,6 +27,12 @@ export declare const CODEX_DISABLE_INHERITED_MCP_ENV = "OURS_FLEET_CODEX_DISABLE
27
27
  export declare function promptContentBlocks(text: string, origin?: PromptOrigin): acp.ContentBlock[];
28
28
  export declare function runtimeSelector(options: acp.SessionConfigOption[] | null | undefined, category: string): RuntimeSelectorMetadata | undefined;
29
29
  export interface AcpSessionOptions {
30
+ /** Opt-in Fleet watchdog, owned by this ACP session, never a process restart. */
31
+ stallRecovery?: {
32
+ timeoutMs?: number;
33
+ tickMs?: number;
34
+ cancelWaitMs?: number;
35
+ };
30
36
  name: string;
31
37
  /** Harness identity used only for honest optional capability reporting. */
32
38
  harness?: string;
@@ -144,6 +150,15 @@ export declare class AcpSession implements AgentSession {
144
150
  private terminate;
145
151
  /** ACP-authenticated in-flight calls, including independently reserved permissions. */
146
152
  private readonly activeToolCalls;
153
+ private stallWatchdog?;
154
+ private stallToolHistory?;
155
+ private stallRecoveryClaimed;
156
+ private managedTurnCount;
157
+ private steeringWasUsed;
158
+ private steeringRequests;
159
+ private retryNativeTurnId?;
160
+ private stallTimer?;
161
+ private stallAttempt?;
147
162
  private readonly toolBoundaryWaiters;
148
163
  private activeTurn?;
149
164
  private constructor();
@@ -275,7 +290,13 @@ export declare class AcpSession implements AgentSession {
275
290
  private declaredMcpServers;
276
291
  private initialize;
277
292
  private captureRuntimeMetadata;
293
+ private startStallWatchdog;
294
+ private checkStallWatchdog;
295
+ private stallObservation;
296
+ private recoverStall;
297
+ /** Keep the original queue slot (including startup) until recovery finishes. */
278
298
  private runPrompt;
299
+ private runSinglePrompt;
279
300
  private steerPrompt;
280
301
  private requestPermission;
281
302
  /**
@@ -294,6 +315,8 @@ export declare class AcpSession implements AgentSession {
294
315
  * malformed requests on the ordinary fail-closed path.
295
316
  */
296
317
  private isEffectiveCodexProtectedMcpApproval;
318
+ /** Pinned codex-acp 1.1.7 structured metadata, never stderr or assistant text. */
319
+ private recordStallMetadata;
297
320
  private recordUpdate;
298
321
  /**
299
322
  * Codex ACP's phase extension is the only currently supported visibility