oh-my-codex 0.16.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +5 -5
- package/Cargo.toml +1 -1
- package/crates/omx-explore/src/main.rs +434 -28
- package/dist/agents/__tests__/native-config.test.js +50 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +3 -2
- package/dist/agents/native-config.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +1 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +118 -1
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/imagegen-continuation.test.d.ts +2 -0
- package/dist/cli/__tests__/imagegen-continuation.test.d.ts.map +1 -0
- package/dist/cli/__tests__/imagegen-continuation.test.js +135 -0
- package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -0
- package/dist/cli/__tests__/index.test.js +182 -18
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +88 -2
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +62 -0
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +45 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +465 -12
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +40 -0
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +208 -8
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +11 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +124 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +37 -3
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +93 -4
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/team.d.ts +1 -0
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +42 -7
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +6 -3
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +5 -0
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +18 -1
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +4 -1
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/models.d.ts +6 -0
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +37 -0
- package/dist/config/models.js.map +1 -1
- package/dist/exec/followup.d.ts +1 -0
- package/dist/exec/followup.d.ts.map +1 -1
- package/dist/exec/followup.js +9 -3
- package/dist/exec/followup.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js +19 -0
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js +19 -2
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +40 -0
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/foreground-isolation-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/foreground-isolation-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/foreground-isolation-contract.test.js +28 -0
- package/dist/hooks/__tests__/foreground-isolation-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/session.test.js +32 -0
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/codebase-map.d.ts.map +1 -1
- package/dist/hooks/codebase-map.js +3 -2
- package/dist/hooks/codebase-map.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +6 -4
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/logging.d.ts.map +1 -1
- package/dist/hooks/extensibility/logging.js +3 -2
- package/dist/hooks/extensibility/logging.js.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.js +4 -3
- package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +22 -12
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +8 -7
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +1 -1
- package/dist/hud/__tests__/state.test.js +24 -0
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/index.js +1 -1
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +22 -8
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.js +1 -1
- package/dist/hud/tmux.js.map +1 -1
- package/dist/imagegen/continuation.d.ts +44 -0
- package/dist/imagegen/continuation.d.ts.map +1 -0
- package/dist/imagegen/continuation.js +220 -0
- package/dist/imagegen/continuation.js.map +1 -0
- package/dist/mcp/__tests__/bootstrap.test.js +47 -2
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +49 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +2 -0
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +95 -15
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/lifecycle-telemetry.d.ts +16 -0
- package/dist/mcp/lifecycle-telemetry.d.ts.map +1 -0
- package/dist/mcp/lifecycle-telemetry.js +95 -0
- package/dist/mcp/lifecycle-telemetry.js.map +1 -0
- package/dist/pipeline/__tests__/stages.test.js +274 -5
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/team-exec.d.ts +2 -0
- package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
- package/dist/pipeline/stages/team-exec.js +51 -26
- package/dist/pipeline/stages/team-exec.js.map +1 -1
- package/dist/planning/__tests__/artifacts.test.js +138 -3
- package/dist/planning/__tests__/artifacts.test.js.map +1 -1
- package/dist/planning/__tests__/context-pack-status.test.d.ts +2 -0
- package/dist/planning/__tests__/context-pack-status.test.d.ts.map +1 -0
- package/dist/planning/__tests__/context-pack-status.test.js +271 -0
- package/dist/planning/__tests__/context-pack-status.test.js.map +1 -0
- package/dist/planning/artifacts.d.ts +12 -1
- package/dist/planning/artifacts.d.ts.map +1 -1
- package/dist/planning/artifacts.js +32 -9
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/planning/context-pack-status.d.ts +42 -0
- package/dist/planning/context-pack-status.d.ts.map +1 -0
- package/dist/planning/context-pack-status.js +479 -0
- package/dist/planning/context-pack-status.js.map +1 -0
- package/dist/runtime/__tests__/process-tree.test.d.ts +2 -0
- package/dist/runtime/__tests__/process-tree.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/process-tree.test.js +107 -0
- package/dist/runtime/__tests__/process-tree.test.js.map +1 -0
- package/dist/runtime/process-tree.d.ts +28 -0
- package/dist/runtime/process-tree.d.ts.map +1 -0
- package/dist/runtime/process-tree.js +230 -0
- package/dist/runtime/process-tree.js.map +1 -0
- package/dist/scripts/__tests__/codex-native-hook.test.js +205 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-state-io.test.d.ts +2 -0
- package/dist/scripts/__tests__/notify-state-io.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +40 -0
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +111 -7
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.js +6 -9
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
- package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
- package/dist/scripts/notify-hook/process-runner.js +4 -1
- package/dist/scripts/notify-hook/process-runner.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +4 -7
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook.js +25 -3
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/verify-native-agents.d.ts.map +1 -1
- package/dist/scripts/verify-native-agents.js +3 -1
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/sidecar/__tests__/tmux.test.js +1 -1
- package/dist/sidecar/__tests__/tmux.test.js.map +1 -1
- package/dist/sidecar/tmux.js +1 -1
- package/dist/sidecar/tmux.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +45 -1
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +2 -2
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.js +2 -2
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/team/__tests__/approved-execution.test.js +96 -0
- package/dist/team/__tests__/approved-execution.test.js.map +1 -1
- package/dist/team/__tests__/followup-planner.test.js +16 -0
- package/dist/team/__tests__/followup-planner.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +16 -0
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/repo-aware-decomposition.test.js +20 -0
- package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +16 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +209 -11
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +110 -0
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +9 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +6 -0
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/approved-execution.d.ts +13 -0
- package/dist/team/approved-execution.d.ts.map +1 -1
- package/dist/team/approved-execution.js +40 -22
- package/dist/team/approved-execution.js.map +1 -1
- package/dist/team/followup-planner.d.ts +1 -0
- package/dist/team/followup-planner.d.ts.map +1 -1
- package/dist/team/followup-planner.js +9 -9
- package/dist/team/followup-planner.js.map +1 -1
- package/dist/team/model-contract.d.ts +1 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +4 -3
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/repo-aware-decomposition.d.ts +1 -0
- package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
- package/dist/team/repo-aware-decomposition.js +5 -1
- package/dist/team/repo-aware-decomposition.js.map +1 -1
- package/dist/team/runtime-cli.d.ts +4 -0
- package/dist/team/runtime-cli.d.ts.map +1 -1
- package/dist/team/runtime-cli.js +14 -1
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts +1 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +46 -16
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +13 -6
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +7 -0
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +51 -0
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.d.ts +2 -0
- package/dist/ultragoal/__tests__/docs-contract.test.d.ts.map +1 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js +23 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -0
- package/dist/ultragoal/artifacts.d.ts +2 -2
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +37 -1
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +44 -14
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +9 -0
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +25 -2
- package/plugins/oh-my-codex/skills/plan/SKILL.md +7 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +13 -3
- package/plugins/oh-my-codex/skills/team/SKILL.md +2 -2
- package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +8 -0
- package/prompts/planner.md +1 -1
- package/skills/ai-slop-cleaner/SKILL.md +9 -0
- package/skills/deep-interview/SKILL.md +25 -2
- package/skills/plan/SKILL.md +7 -4
- package/skills/ralplan/SKILL.md +13 -3
- package/skills/team/SKILL.md +2 -2
- package/skills/visual-ralph/SKILL.md +8 -0
- package/src/scripts/__tests__/codex-native-hook.test.ts +231 -1
- package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
- package/src/scripts/codex-native-hook.ts +129 -14
- package/src/scripts/notify-hook/managed-tmux.ts +6 -7
- package/src/scripts/notify-hook/process-runner.ts +4 -1
- package/src/scripts/notify-hook/state-io.ts +5 -7
- package/src/scripts/notify-hook.ts +26 -3
- package/src/scripts/verify-native-agents.ts +3 -1
package/Cargo.lock
CHANGED
|
@@ -32,14 +32,14 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
|
32
32
|
|
|
33
33
|
[[package]]
|
|
34
34
|
name = "omx-explore-harness"
|
|
35
|
-
version = "0.16.
|
|
35
|
+
version = "0.16.1"
|
|
36
36
|
dependencies = [
|
|
37
37
|
"libc",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
40
|
[[package]]
|
|
41
41
|
name = "omx-mux"
|
|
42
|
-
version = "0.16.
|
|
42
|
+
version = "0.16.1"
|
|
43
43
|
dependencies = [
|
|
44
44
|
"serde",
|
|
45
45
|
"serde_json",
|
|
@@ -47,7 +47,7 @@ dependencies = [
|
|
|
47
47
|
|
|
48
48
|
[[package]]
|
|
49
49
|
name = "omx-runtime"
|
|
50
|
-
version = "0.16.
|
|
50
|
+
version = "0.16.1"
|
|
51
51
|
dependencies = [
|
|
52
52
|
"omx-mux",
|
|
53
53
|
"omx-runtime-core",
|
|
@@ -56,7 +56,7 @@ dependencies = [
|
|
|
56
56
|
|
|
57
57
|
[[package]]
|
|
58
58
|
name = "omx-runtime-core"
|
|
59
|
-
version = "0.16.
|
|
59
|
+
version = "0.16.1"
|
|
60
60
|
dependencies = [
|
|
61
61
|
"fs2",
|
|
62
62
|
"serde",
|
|
@@ -65,7 +65,7 @@ dependencies = [
|
|
|
65
65
|
|
|
66
66
|
[[package]]
|
|
67
67
|
name = "omx-sparkshell"
|
|
68
|
-
version = "0.16.
|
|
68
|
+
version = "0.16.1"
|
|
69
69
|
dependencies = [
|
|
70
70
|
"omx-mux",
|
|
71
71
|
]
|
package/Cargo.toml
CHANGED
|
@@ -6,22 +6,35 @@ use std::fs::{
|
|
|
6
6
|
use std::io::{self, BufRead, BufReader, Read};
|
|
7
7
|
use std::path::{Path, PathBuf};
|
|
8
8
|
use std::process::{Child, Command, Output, Stdio};
|
|
9
|
-
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
|
|
9
|
+
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, TryRecvError};
|
|
10
10
|
use std::thread;
|
|
11
11
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|
12
12
|
|
|
13
13
|
const CODEX_BIN_ENV: &str = "OMX_EXPLORE_CODEX_BIN";
|
|
14
14
|
const HARNESS_ROOT_ENV: &str = "OMX_EXPLORE_ROOT";
|
|
15
15
|
const CODEX_TIMEOUT_MS_ENV: &str = "OMX_EXPLORE_CODEX_TIMEOUT_MS";
|
|
16
|
+
const PROCESS_LIMIT_ENV: &str = "OMX_EXPLORE_PROCESS_LIMIT";
|
|
17
|
+
const CODEX_OUTPUT_LIMIT_BYTES_ENV: &str = "OMX_EXPLORE_CODEX_OUTPUT_LIMIT_BYTES";
|
|
16
18
|
const INTERNAL_DIRECT_WRAPPER_FLAG: &str = "--internal-allowlist-direct";
|
|
17
19
|
const INTERNAL_SHELL_WRAPPER_FLAG: &str = "--internal-allowlist-shell";
|
|
18
20
|
const TEMP_ALLOWLIST_DIR_PREFIX: &str = "omx-explore-allowlist-";
|
|
19
21
|
const DEFAULT_CODEX_TIMEOUT_MS: u64 = 180_000;
|
|
22
|
+
const DEFAULT_PROCESS_LIMIT: usize = 96;
|
|
23
|
+
const DEFAULT_CODEX_OUTPUT_LIMIT_BYTES: usize = 8 * 1024 * 1024;
|
|
24
|
+
const PROCESS_LIMIT_POLL_MS: u64 = 100;
|
|
20
25
|
const PROCESS_TERMINATION_GRACE_MS: u64 = 500;
|
|
21
26
|
const PIPE_READER_READY_GRACE_MS: u64 = 25;
|
|
22
27
|
const PIPE_READER_JOIN_GRACE_MS: u64 = 500;
|
|
23
|
-
const EXPLORE_SUBPROCESS_ENV_VARS_TO_SCRUB: &[&str] =
|
|
24
|
-
|
|
28
|
+
const EXPLORE_SUBPROCESS_ENV_VARS_TO_SCRUB: &[&str] = &[
|
|
29
|
+
"BASH_ENV",
|
|
30
|
+
"ENV",
|
|
31
|
+
"PROMPT_COMMAND",
|
|
32
|
+
"NODE_OPTIONS",
|
|
33
|
+
"SHELLOPTS",
|
|
34
|
+
"BASHOPTS",
|
|
35
|
+
"GREP_OPTIONS",
|
|
36
|
+
"GREP_COLORS",
|
|
37
|
+
];
|
|
25
38
|
const WINDOWS_UNSUPPORTED_ALLOWLIST_MESSAGE: &str =
|
|
26
39
|
"omx explore built-in harness is not ready on Windows because its allowlist runtime relies on POSIX sh/bash wrappers. Set OMX_EXPLORE_BIN to a compatible custom harness, prefer `omx sparkshell` for shell-native read-only lookups, or run `omx doctor` for readiness details.";
|
|
27
40
|
|
|
@@ -334,13 +347,59 @@ fn invoke_codex(args: &Args, model: &str, prompt_contract: &str) -> io::Result<A
|
|
|
334
347
|
),
|
|
335
348
|
output_markdown: None,
|
|
336
349
|
}),
|
|
350
|
+
TimedCommandOutput::ProcessLimitExceeded {
|
|
351
|
+
stderr,
|
|
352
|
+
process_count,
|
|
353
|
+
process_limit,
|
|
354
|
+
} => Ok(AttemptResult {
|
|
355
|
+
status_code: 125,
|
|
356
|
+
stderr: format!(
|
|
357
|
+
"[omx explore] codex exec exceeded per-run process limit ({process_count}>{process_limit}); terminated process tree to avoid runaway shell storms{}{}",
|
|
358
|
+
if stderr.trim().is_empty() {
|
|
359
|
+
""
|
|
360
|
+
} else {
|
|
361
|
+
". stderr before termination: "
|
|
362
|
+
},
|
|
363
|
+
stderr.trim()
|
|
364
|
+
),
|
|
365
|
+
output_markdown: None,
|
|
366
|
+
}),
|
|
367
|
+
TimedCommandOutput::OutputLimitExceeded {
|
|
368
|
+
stderr,
|
|
369
|
+
output_limit,
|
|
370
|
+
stream,
|
|
371
|
+
} => Ok(AttemptResult {
|
|
372
|
+
status_code: 126,
|
|
373
|
+
stderr: format!(
|
|
374
|
+
"[omx explore] codex exec exceeded subprocess {stream} output limit ({output_limit} bytes); terminated process tree to avoid unbounded memory growth{}{}",
|
|
375
|
+
if stderr.trim().is_empty() {
|
|
376
|
+
""
|
|
377
|
+
} else {
|
|
378
|
+
". stderr before termination: "
|
|
379
|
+
},
|
|
380
|
+
stderr.trim()
|
|
381
|
+
),
|
|
382
|
+
output_markdown: None,
|
|
383
|
+
}),
|
|
337
384
|
}
|
|
338
385
|
}
|
|
339
386
|
|
|
340
387
|
#[derive(Debug)]
|
|
341
388
|
enum TimedCommandOutput {
|
|
342
389
|
Completed(Output),
|
|
343
|
-
TimedOut {
|
|
390
|
+
TimedOut {
|
|
391
|
+
stderr: String,
|
|
392
|
+
},
|
|
393
|
+
ProcessLimitExceeded {
|
|
394
|
+
stderr: String,
|
|
395
|
+
process_count: usize,
|
|
396
|
+
process_limit: usize,
|
|
397
|
+
},
|
|
398
|
+
OutputLimitExceeded {
|
|
399
|
+
stderr: String,
|
|
400
|
+
output_limit: usize,
|
|
401
|
+
stream: &'static str,
|
|
402
|
+
},
|
|
344
403
|
}
|
|
345
404
|
|
|
346
405
|
fn codex_timeout() -> Duration {
|
|
@@ -352,6 +411,22 @@ fn codex_timeout() -> Duration {
|
|
|
352
411
|
Duration::from_millis(timeout_ms)
|
|
353
412
|
}
|
|
354
413
|
|
|
414
|
+
fn codex_output_limit_bytes() -> usize {
|
|
415
|
+
env::var(CODEX_OUTPUT_LIMIT_BYTES_ENV)
|
|
416
|
+
.ok()
|
|
417
|
+
.and_then(|value| value.trim().parse::<usize>().ok())
|
|
418
|
+
.filter(|value| *value > 0)
|
|
419
|
+
.unwrap_or(DEFAULT_CODEX_OUTPUT_LIMIT_BYTES)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
fn process_limit() -> usize {
|
|
423
|
+
env::var(PROCESS_LIMIT_ENV)
|
|
424
|
+
.ok()
|
|
425
|
+
.and_then(|value| value.trim().parse::<usize>().ok())
|
|
426
|
+
.filter(|value| *value > 0)
|
|
427
|
+
.unwrap_or(DEFAULT_PROCESS_LIMIT)
|
|
428
|
+
}
|
|
429
|
+
|
|
355
430
|
fn run_command_with_timeout(
|
|
356
431
|
mut command: Command,
|
|
357
432
|
timeout: Duration,
|
|
@@ -360,19 +435,37 @@ fn run_command_with_timeout(
|
|
|
360
435
|
configure_process_group(&mut command);
|
|
361
436
|
let mut child = command.spawn()?;
|
|
362
437
|
|
|
363
|
-
let
|
|
364
|
-
let
|
|
438
|
+
let output_limit = codex_output_limit_bytes();
|
|
439
|
+
let mut stdout_reader = spawn_pipe_reader("stdout", child.stdout.take(), output_limit);
|
|
440
|
+
let mut stderr_reader = spawn_pipe_reader("stderr", child.stderr.take(), output_limit);
|
|
365
441
|
|
|
366
442
|
let deadline = Instant::now() + timeout;
|
|
443
|
+
let process_limit = process_limit();
|
|
444
|
+
let mut next_process_limit_poll = Instant::now() + Duration::from_millis(PROCESS_LIMIT_POLL_MS);
|
|
367
445
|
loop {
|
|
368
446
|
if let Some(status) = child.try_wait()? {
|
|
369
|
-
|
|
447
|
+
// The wrapper may exit while grandchildren keep the process group
|
|
448
|
+
// alive. Sweep it before collecting pipes so completed harness
|
|
449
|
+
// runs cannot leave detached shells behind.
|
|
450
|
+
terminate_child_process_tree(&mut child);
|
|
451
|
+
let output = collect_completed_output(
|
|
370
452
|
&mut child,
|
|
371
|
-
&stdout_reader,
|
|
372
|
-
&stderr_reader,
|
|
453
|
+
&mut stdout_reader,
|
|
454
|
+
&mut stderr_reader,
|
|
373
455
|
Duration::from_millis(PIPE_READER_READY_GRACE_MS),
|
|
374
456
|
Duration::from_millis(PIPE_READER_JOIN_GRACE_MS),
|
|
375
|
-
)
|
|
457
|
+
);
|
|
458
|
+
let (stdout, stderr) = match output {
|
|
459
|
+
Ok(output) => output,
|
|
460
|
+
Err(err) if is_output_limit_error(&err) => {
|
|
461
|
+
return Ok(TimedCommandOutput::OutputLimitExceeded {
|
|
462
|
+
stderr: String::new(),
|
|
463
|
+
output_limit,
|
|
464
|
+
stream: output_limit_stream(&err),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
Err(err) => return Err(err),
|
|
468
|
+
};
|
|
376
469
|
return Ok(TimedCommandOutput::Completed(Output {
|
|
377
470
|
status,
|
|
378
471
|
stdout,
|
|
@@ -384,37 +477,192 @@ fn run_command_with_timeout(
|
|
|
384
477
|
terminate_child_process_tree(&mut child);
|
|
385
478
|
let _ = child.wait();
|
|
386
479
|
let reader_timeout = Duration::from_millis(PIPE_READER_JOIN_GRACE_MS);
|
|
387
|
-
let _ = receive_pipe_reader(&stdout_reader, reader_timeout);
|
|
388
|
-
let stderr =
|
|
480
|
+
let _ = receive_pipe_reader(&mut stdout_reader, reader_timeout);
|
|
481
|
+
let stderr =
|
|
482
|
+
receive_pipe_reader(&mut stderr_reader, reader_timeout).unwrap_or_default();
|
|
389
483
|
return Ok(TimedCommandOutput::TimedOut {
|
|
390
484
|
stderr: String::from_utf8_lossy(&stderr).into_owned(),
|
|
391
485
|
});
|
|
392
486
|
}
|
|
393
487
|
|
|
488
|
+
if Instant::now() >= next_process_limit_poll {
|
|
489
|
+
next_process_limit_poll = Instant::now() + Duration::from_millis(PROCESS_LIMIT_POLL_MS);
|
|
490
|
+
if let Some(process_count) = count_process_tree(child.id()) {
|
|
491
|
+
if process_count > process_limit {
|
|
492
|
+
terminate_child_process_tree(&mut child);
|
|
493
|
+
let _ = child.wait();
|
|
494
|
+
let reader_timeout = Duration::from_millis(PIPE_READER_JOIN_GRACE_MS);
|
|
495
|
+
let _ = receive_pipe_reader(&mut stdout_reader, reader_timeout);
|
|
496
|
+
let stderr =
|
|
497
|
+
receive_pipe_reader(&mut stderr_reader, reader_timeout).unwrap_or_default();
|
|
498
|
+
return Ok(TimedCommandOutput::ProcessLimitExceeded {
|
|
499
|
+
stderr: String::from_utf8_lossy(&stderr).into_owned(),
|
|
500
|
+
process_count,
|
|
501
|
+
process_limit,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if let Some(stream) = poll_output_limit(&mut stdout_reader, &mut stderr_reader)? {
|
|
508
|
+
terminate_child_process_tree(&mut child);
|
|
509
|
+
let _ = child.wait();
|
|
510
|
+
let reader_timeout = Duration::from_millis(PIPE_READER_JOIN_GRACE_MS);
|
|
511
|
+
let stderr = if stream == "stderr" {
|
|
512
|
+
Vec::new()
|
|
513
|
+
} else {
|
|
514
|
+
receive_pipe_reader(&mut stderr_reader, reader_timeout).unwrap_or_default()
|
|
515
|
+
};
|
|
516
|
+
return Ok(TimedCommandOutput::OutputLimitExceeded {
|
|
517
|
+
stderr: String::from_utf8_lossy(&stderr).into_owned(),
|
|
518
|
+
output_limit,
|
|
519
|
+
stream,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
394
523
|
thread::sleep(Duration::from_millis(25));
|
|
395
524
|
}
|
|
396
525
|
}
|
|
397
526
|
|
|
398
|
-
|
|
527
|
+
#[cfg(target_os = "linux")]
|
|
528
|
+
fn count_process_tree(root_pid: u32) -> Option<usize> {
|
|
529
|
+
use std::collections::HashMap;
|
|
530
|
+
let entries = std::fs::read_dir("/proc").ok()?;
|
|
531
|
+
let mut children: HashMap<u32, Vec<u32>> = HashMap::new();
|
|
532
|
+
for entry in entries.flatten() {
|
|
533
|
+
let name = entry.file_name();
|
|
534
|
+
let Some(name) = name.to_str() else {
|
|
535
|
+
continue;
|
|
536
|
+
};
|
|
537
|
+
let Ok(pid) = name.parse::<u32>() else {
|
|
538
|
+
continue;
|
|
539
|
+
};
|
|
540
|
+
let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
|
|
541
|
+
continue;
|
|
542
|
+
};
|
|
543
|
+
let Some(close_paren) = stat.rfind(')') else {
|
|
544
|
+
continue;
|
|
545
|
+
};
|
|
546
|
+
let fields: Vec<&str> = stat[close_paren + 2..].split(' ').collect();
|
|
547
|
+
let Some(ppid) = fields.get(1).and_then(|field| field.parse::<u32>().ok()) else {
|
|
548
|
+
continue;
|
|
549
|
+
};
|
|
550
|
+
children.entry(ppid).or_default().push(pid);
|
|
551
|
+
}
|
|
552
|
+
let mut count = 1;
|
|
553
|
+
let mut stack = children.remove(&root_pid).unwrap_or_default();
|
|
554
|
+
while let Some(pid) = stack.pop() {
|
|
555
|
+
count += 1;
|
|
556
|
+
if let Some(mut nested) = children.remove(&pid) {
|
|
557
|
+
stack.append(&mut nested);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
Some(count)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
#[cfg(not(target_os = "linux"))]
|
|
564
|
+
fn count_process_tree(_root_pid: u32) -> Option<usize> {
|
|
565
|
+
None
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
struct PipeReader {
|
|
569
|
+
receiver: Receiver<io::Result<Vec<u8>>>,
|
|
570
|
+
cached: Option<io::Result<Vec<u8>>>,
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
fn spawn_pipe_reader<R: Read + Send + 'static>(
|
|
574
|
+
stream: &'static str,
|
|
575
|
+
pipe: Option<R>,
|
|
576
|
+
output_limit: usize,
|
|
577
|
+
) -> PipeReader {
|
|
399
578
|
let (sender, receiver) = mpsc::channel();
|
|
400
579
|
thread::spawn(move || {
|
|
401
|
-
let _ = sender.send(
|
|
580
|
+
let _ = sender.send(read_pipe_bounded(pipe, stream, output_limit));
|
|
402
581
|
});
|
|
403
|
-
|
|
582
|
+
PipeReader {
|
|
583
|
+
receiver,
|
|
584
|
+
cached: None,
|
|
585
|
+
}
|
|
404
586
|
}
|
|
405
587
|
|
|
406
|
-
fn
|
|
588
|
+
fn read_pipe_bounded<R: Read + Send + 'static>(
|
|
589
|
+
pipe: Option<R>,
|
|
590
|
+
stream: &'static str,
|
|
591
|
+
output_limit: usize,
|
|
592
|
+
) -> io::Result<Vec<u8>> {
|
|
407
593
|
let mut bytes = Vec::new();
|
|
408
|
-
|
|
409
|
-
|
|
594
|
+
let Some(pipe) = pipe else {
|
|
595
|
+
return Ok(bytes);
|
|
596
|
+
};
|
|
597
|
+
let mut reader = BufReader::new(pipe);
|
|
598
|
+
let mut chunk = [0_u8; 8192];
|
|
599
|
+
loop {
|
|
600
|
+
let read = reader.read(&mut chunk)?;
|
|
601
|
+
if read == 0 {
|
|
602
|
+
return Ok(bytes);
|
|
603
|
+
}
|
|
604
|
+
if bytes.len().saturating_add(read) > output_limit {
|
|
605
|
+
return Err(output_limit_error(stream, output_limit));
|
|
606
|
+
}
|
|
607
|
+
bytes.extend_from_slice(&chunk[..read]);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
fn output_limit_error(stream: &'static str, output_limit: usize) -> io::Error {
|
|
612
|
+
io::Error::new(
|
|
613
|
+
io::ErrorKind::Other,
|
|
614
|
+
format!("subprocess {stream} exceeded output limit of {output_limit} bytes"),
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
fn is_output_limit_error(err: &io::Error) -> bool {
|
|
619
|
+
err.to_string().contains("exceeded output limit")
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
fn output_limit_stream(err: &io::Error) -> &'static str {
|
|
623
|
+
if err.to_string().contains("stderr") {
|
|
624
|
+
"stderr"
|
|
625
|
+
} else {
|
|
626
|
+
"stdout"
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
fn poll_output_limit(
|
|
631
|
+
stdout_reader: &mut PipeReader,
|
|
632
|
+
stderr_reader: &mut PipeReader,
|
|
633
|
+
) -> io::Result<Option<&'static str>> {
|
|
634
|
+
if let Some(stream) = poll_one_output_limit("stdout", stdout_reader)? {
|
|
635
|
+
return Ok(Some(stream));
|
|
636
|
+
}
|
|
637
|
+
poll_one_output_limit("stderr", stderr_reader)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
fn poll_one_output_limit(
|
|
641
|
+
stream: &'static str,
|
|
642
|
+
reader: &mut PipeReader,
|
|
643
|
+
) -> io::Result<Option<&'static str>> {
|
|
644
|
+
if reader.cached.is_some() {
|
|
645
|
+
return Ok(None);
|
|
646
|
+
}
|
|
647
|
+
match reader.receiver.try_recv() {
|
|
648
|
+
Ok(Ok(bytes)) => {
|
|
649
|
+
reader.cached = Some(Ok(bytes));
|
|
650
|
+
Ok(None)
|
|
651
|
+
}
|
|
652
|
+
Ok(Err(err)) if is_output_limit_error(&err) => Ok(Some(stream)),
|
|
653
|
+
Ok(Err(err)) => Err(err),
|
|
654
|
+
Err(TryRecvError::Empty) => Ok(None),
|
|
655
|
+
Err(TryRecvError::Disconnected) => Err(io::Error::new(
|
|
656
|
+
io::ErrorKind::Other,
|
|
657
|
+
"subprocess output reader disconnected",
|
|
658
|
+
)),
|
|
410
659
|
}
|
|
411
|
-
Ok(bytes)
|
|
412
660
|
}
|
|
413
661
|
|
|
414
662
|
fn collect_completed_output(
|
|
415
663
|
child: &mut Child,
|
|
416
|
-
stdout_reader: &
|
|
417
|
-
stderr_reader: &
|
|
664
|
+
stdout_reader: &mut PipeReader,
|
|
665
|
+
stderr_reader: &mut PipeReader,
|
|
418
666
|
ready_timeout: Duration,
|
|
419
667
|
cleanup_timeout: Duration,
|
|
420
668
|
) -> io::Result<(Vec<u8>, Vec<u8>)> {
|
|
@@ -438,21 +686,21 @@ fn collect_completed_output(
|
|
|
438
686
|
}
|
|
439
687
|
|
|
440
688
|
fn receive_pipe_reader_if_ready(
|
|
441
|
-
|
|
689
|
+
reader: &mut PipeReader,
|
|
442
690
|
timeout: Duration,
|
|
443
691
|
) -> io::Result<Option<Vec<u8>>> {
|
|
444
|
-
match receive_pipe_reader(
|
|
692
|
+
match receive_pipe_reader(reader, timeout) {
|
|
445
693
|
Ok(bytes) => Ok(Some(bytes)),
|
|
446
694
|
Err(err) if err.kind() == io::ErrorKind::TimedOut => Ok(None),
|
|
447
695
|
Err(err) => Err(err),
|
|
448
696
|
}
|
|
449
697
|
}
|
|
450
698
|
|
|
451
|
-
fn receive_pipe_reader(
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
match receiver.recv_timeout(timeout) {
|
|
699
|
+
fn receive_pipe_reader(reader: &mut PipeReader, timeout: Duration) -> io::Result<Vec<u8>> {
|
|
700
|
+
if let Some(result) = reader.cached.take() {
|
|
701
|
+
return result;
|
|
702
|
+
}
|
|
703
|
+
match reader.receiver.recv_timeout(timeout) {
|
|
456
704
|
Ok(result) => result,
|
|
457
705
|
Err(RecvTimeoutError::Timeout) => Err(io::Error::new(
|
|
458
706
|
io::ErrorKind::TimedOut,
|
|
@@ -2476,6 +2724,126 @@ sleep 30
|
|
|
2476
2724
|
assert_eq!(read_to_string(&term_file).unwrap_or_default(), "term");
|
|
2477
2725
|
}
|
|
2478
2726
|
|
|
2727
|
+
#[cfg(target_os = "linux")]
|
|
2728
|
+
#[test]
|
|
2729
|
+
fn run_command_with_timeout_aborts_suspicious_process_storm() {
|
|
2730
|
+
let _env_guard = env_lock();
|
|
2731
|
+
let _process_guard = process_tree_lock();
|
|
2732
|
+
let root = temp_allowlist_dir().expect("temp root");
|
|
2733
|
+
let script = root.path.join("storm.sh");
|
|
2734
|
+
write_executable(
|
|
2735
|
+
&script,
|
|
2736
|
+
r#"#!/bin/sh
|
|
2737
|
+
while :; do
|
|
2738
|
+
sleep 30 &
|
|
2739
|
+
sleep 0.01
|
|
2740
|
+
done
|
|
2741
|
+
"#,
|
|
2742
|
+
)
|
|
2743
|
+
.expect("write script");
|
|
2744
|
+
|
|
2745
|
+
unsafe {
|
|
2746
|
+
env::set_var(PROCESS_LIMIT_ENV, "12");
|
|
2747
|
+
}
|
|
2748
|
+
let started = Instant::now();
|
|
2749
|
+
let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
|
|
2750
|
+
.expect("run with process storm");
|
|
2751
|
+
unsafe {
|
|
2752
|
+
env::remove_var(PROCESS_LIMIT_ENV);
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
let TimedCommandOutput::ProcessLimitExceeded {
|
|
2756
|
+
process_count,
|
|
2757
|
+
process_limit,
|
|
2758
|
+
..
|
|
2759
|
+
} = result
|
|
2760
|
+
else {
|
|
2761
|
+
panic!("expected process limit failure");
|
|
2762
|
+
};
|
|
2763
|
+
assert!(process_count > process_limit);
|
|
2764
|
+
assert!(started.elapsed() < Duration::from_secs(5));
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
#[cfg(unix)]
|
|
2768
|
+
#[test]
|
|
2769
|
+
fn run_command_with_timeout_fails_closed_on_large_stdout() {
|
|
2770
|
+
let _env_guard = env_lock();
|
|
2771
|
+
let _process_guard = process_tree_lock();
|
|
2772
|
+
let root = temp_allowlist_dir().expect("temp root");
|
|
2773
|
+
let script = root.path.join("large-stdout.sh");
|
|
2774
|
+
write_executable(
|
|
2775
|
+
&script,
|
|
2776
|
+
r#"#!/bin/sh
|
|
2777
|
+
while :; do
|
|
2778
|
+
printf 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
|
2779
|
+
done
|
|
2780
|
+
"#,
|
|
2781
|
+
)
|
|
2782
|
+
.expect("write script");
|
|
2783
|
+
|
|
2784
|
+
unsafe {
|
|
2785
|
+
env::set_var(CODEX_OUTPUT_LIMIT_BYTES_ENV, "4096");
|
|
2786
|
+
}
|
|
2787
|
+
let started = Instant::now();
|
|
2788
|
+
let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
|
|
2789
|
+
.expect("run with large stdout");
|
|
2790
|
+
unsafe {
|
|
2791
|
+
env::remove_var(CODEX_OUTPUT_LIMIT_BYTES_ENV);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
let TimedCommandOutput::OutputLimitExceeded {
|
|
2795
|
+
stream,
|
|
2796
|
+
output_limit,
|
|
2797
|
+
..
|
|
2798
|
+
} = result
|
|
2799
|
+
else {
|
|
2800
|
+
panic!("expected stdout output limit failure");
|
|
2801
|
+
};
|
|
2802
|
+
assert_eq!(stream, "stdout");
|
|
2803
|
+
assert_eq!(output_limit, 4096);
|
|
2804
|
+
assert!(started.elapsed() < Duration::from_secs(3));
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
#[cfg(unix)]
|
|
2808
|
+
#[test]
|
|
2809
|
+
fn run_command_with_timeout_fails_closed_on_large_stderr() {
|
|
2810
|
+
let _env_guard = env_lock();
|
|
2811
|
+
let _process_guard = process_tree_lock();
|
|
2812
|
+
let root = temp_allowlist_dir().expect("temp root");
|
|
2813
|
+
let script = root.path.join("large-stderr.sh");
|
|
2814
|
+
write_executable(
|
|
2815
|
+
&script,
|
|
2816
|
+
r#"#!/bin/sh
|
|
2817
|
+
while :; do
|
|
2818
|
+
printf 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' >&2
|
|
2819
|
+
done
|
|
2820
|
+
"#,
|
|
2821
|
+
)
|
|
2822
|
+
.expect("write script");
|
|
2823
|
+
|
|
2824
|
+
unsafe {
|
|
2825
|
+
env::set_var(CODEX_OUTPUT_LIMIT_BYTES_ENV, "4096");
|
|
2826
|
+
}
|
|
2827
|
+
let started = Instant::now();
|
|
2828
|
+
let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
|
|
2829
|
+
.expect("run with large stderr");
|
|
2830
|
+
unsafe {
|
|
2831
|
+
env::remove_var(CODEX_OUTPUT_LIMIT_BYTES_ENV);
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
let TimedCommandOutput::OutputLimitExceeded {
|
|
2835
|
+
stream,
|
|
2836
|
+
output_limit,
|
|
2837
|
+
..
|
|
2838
|
+
} = result
|
|
2839
|
+
else {
|
|
2840
|
+
panic!("expected stderr output limit failure");
|
|
2841
|
+
};
|
|
2842
|
+
assert_eq!(stream, "stderr");
|
|
2843
|
+
assert_eq!(output_limit, 4096);
|
|
2844
|
+
assert!(started.elapsed() < Duration::from_secs(3));
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2479
2847
|
#[cfg(unix)]
|
|
2480
2848
|
#[test]
|
|
2481
2849
|
fn run_command_with_timeout_closes_inherited_stdio_after_parent_exit() {
|
|
@@ -2510,6 +2878,44 @@ exit 0
|
|
|
2510
2878
|
assert_eq!(String::from_utf8_lossy(&output.stderr), "parent stderr\n");
|
|
2511
2879
|
}
|
|
2512
2880
|
|
|
2881
|
+
#[cfg(unix)]
|
|
2882
|
+
#[test]
|
|
2883
|
+
fn run_command_with_timeout_sweeps_detached_grandchildren_after_parent_exit() {
|
|
2884
|
+
let _env_guard = env_lock();
|
|
2885
|
+
let _process_guard = process_tree_lock();
|
|
2886
|
+
let root = temp_allowlist_dir().expect("temp root");
|
|
2887
|
+
let term_file = root.path.join("orphan.term");
|
|
2888
|
+
let ready_file = root.path.join("orphan.ready");
|
|
2889
|
+
let script = root.path.join("spawn-detached-grandchild.sh");
|
|
2890
|
+
write_executable(
|
|
2891
|
+
&script,
|
|
2892
|
+
&format!(
|
|
2893
|
+
r#"#!/bin/sh
|
|
2894
|
+
(trap 'printf term > {}; exit 0' TERM; printf ready > {}; sleep 30) >/dev/null 2>&1 &
|
|
2895
|
+
while [ ! -f {} ]; do
|
|
2896
|
+
sleep 0.01
|
|
2897
|
+
done
|
|
2898
|
+
printf 'parent done\n'
|
|
2899
|
+
exit 0
|
|
2900
|
+
"#,
|
|
2901
|
+
shell_quote(&term_file.display().to_string()),
|
|
2902
|
+
shell_quote(&ready_file.display().to_string()),
|
|
2903
|
+
shell_quote(&ready_file.display().to_string()),
|
|
2904
|
+
),
|
|
2905
|
+
)
|
|
2906
|
+
.expect("write script");
|
|
2907
|
+
|
|
2908
|
+
let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
|
|
2909
|
+
.expect("run with detached grandchild");
|
|
2910
|
+
|
|
2911
|
+
let TimedCommandOutput::Completed(output) = result else {
|
|
2912
|
+
panic!("expected parent completion");
|
|
2913
|
+
};
|
|
2914
|
+
assert!(output.status.success());
|
|
2915
|
+
assert_eq!(String::from_utf8_lossy(&output.stdout), "parent done\n");
|
|
2916
|
+
assert_eq!(read_to_string(&term_file).unwrap_or_default(), "term");
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2513
2919
|
fn fallback_test_event() -> FallbackEvent {
|
|
2514
2920
|
FallbackEvent {
|
|
2515
2921
|
from_model: "spark-model".to_string(),
|
|
@@ -88,6 +88,31 @@ describe("agents/native-config", () => {
|
|
|
88
88
|
const tripleQuoteBlocks = toml.match(/"""/g) || [];
|
|
89
89
|
assert.equal(tripleQuoteBlocks.length, 2, "only TOML delimiters should remain as raw triple quotes");
|
|
90
90
|
});
|
|
91
|
+
it("applies per-agent reasoning overrides when generating native TOML", async () => {
|
|
92
|
+
const codexHome = await mkdtemp(join(tmpdir(), "omx-native-config-reasoning-"));
|
|
93
|
+
try {
|
|
94
|
+
await writeFile(join(codexHome, ".omx-config.json"), JSON.stringify({
|
|
95
|
+
agentReasoning: {
|
|
96
|
+
architect: "xhigh",
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
const agent = {
|
|
100
|
+
name: "architect",
|
|
101
|
+
description: "System design",
|
|
102
|
+
reasoningEffort: "high",
|
|
103
|
+
posture: "frontier-orchestrator",
|
|
104
|
+
modelClass: "frontier",
|
|
105
|
+
routingRole: "leader",
|
|
106
|
+
tools: "read-only",
|
|
107
|
+
category: "build",
|
|
108
|
+
};
|
|
109
|
+
const toml = generateAgentToml(agent, "Architect prompt", { codexHomeOverride: codexHome });
|
|
110
|
+
assert.match(toml, /model_reasoning_effort = "xhigh"/);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await rm(codexHome, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
91
116
|
it("applies exact-model mini guidance only for resolved gpt-5.4-mini standard roles", () => {
|
|
92
117
|
const agent = {
|
|
93
118
|
name: "debugger",
|
|
@@ -145,6 +170,31 @@ describe("agents/native-config", () => {
|
|
|
145
170
|
await rm(root, { recursive: true, force: true });
|
|
146
171
|
}
|
|
147
172
|
});
|
|
173
|
+
it("installs native agent TOML with configured per-agent reasoning overrides", async () => {
|
|
174
|
+
const root = await mkdtemp(join(tmpdir(), "omx-native-config-install-reasoning-"));
|
|
175
|
+
const codexHome = join(root, ".codex");
|
|
176
|
+
const promptsDir = join(root, "prompts");
|
|
177
|
+
const outDir = join(codexHome, "agents");
|
|
178
|
+
try {
|
|
179
|
+
await mkdir(promptsDir, { recursive: true });
|
|
180
|
+
await mkdir(codexHome, { recursive: true });
|
|
181
|
+
await writeFile(join(codexHome, ".omx-config.json"), JSON.stringify({
|
|
182
|
+
agentReasoning: {
|
|
183
|
+
architect: "xhigh",
|
|
184
|
+
},
|
|
185
|
+
}));
|
|
186
|
+
await writeFile(join(promptsDir, "architect.md"), "architect prompt");
|
|
187
|
+
await installNativeAgentConfigs(root, {
|
|
188
|
+
agentsDir: outDir,
|
|
189
|
+
catalogManifest: manifestWithAgents(["architect"]),
|
|
190
|
+
});
|
|
191
|
+
const architectToml = await readFile(join(outDir, "architect.toml"), "utf8");
|
|
192
|
+
assert.match(architectToml, /model_reasoning_effort = "xhigh"/);
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
await rm(root, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
148
198
|
it("preserves active provider on native agents so websocket-capable Responses providers are inherited", async () => {
|
|
149
199
|
const root = await mkdtemp(join(tmpdir(), "omx-native-config-provider-"));
|
|
150
200
|
const codexHome = join(root, ".codex");
|