@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.
- package/README.md +100 -0
- package/dist/build-info.json +4 -4
- package/dist/config.d.ts +4 -0
- package/dist/config.js +9 -1
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +39 -0
- package/dist/doctor.js +32 -0
- package/dist/exec.d.ts +1 -0
- package/dist/exec.js +1 -1
- package/dist/harness/claude-code-session.js +3 -0
- package/dist/harness/codex-runtime.d.ts +14 -0
- package/dist/harness/codex-runtime.js +70 -0
- package/dist/harness/codex-session.js +3 -0
- package/dist/harness/codex.js +21 -5
- package/dist/runner.js +13 -3
- package/dist/session/acp.d.ts +23 -0
- package/dist/session/acp.js +278 -4
- package/dist/session/stall-watchdog.d.ts +60 -0
- package/dist/session/stall-watchdog.js +210 -0
- package/dist/session/types.d.ts +5 -2
- package/dist/temp-lifecycle.js +3 -0
- package/package.json +2 -2
|
@@ -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
|
}
|
package/dist/harness/codex.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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;
|
package/dist/session/acp.d.ts
CHANGED
|
@@ -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
|