mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
merge main into compaction death loop
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
<p align="center"><code>npm i -g @openai/codex</code><br />or <code>brew install --cask codex</code></p>
|
||||
<p align="center"><strong>Codex CLI</strong> is a coding agent from OpenAI that runs locally on your computer.
|
||||
<p align="center">
|
||||
<img src="https://github.com/openai/codex/blob/main/.github/codex-cli-splash.png" alt="Codex CLI splash" width="80%" />
|
||||
@@ -14,7 +13,19 @@ If you want Codex in your code editor (VS Code, Cursor, Windsurf), <a href="http
|
||||
|
||||
### Installing and running Codex CLI
|
||||
|
||||
Install globally with your preferred package manager:
|
||||
Run the following on Mac or Linux to install Codex CLI:
|
||||
|
||||
```shell
|
||||
curl -fsSL https://chatgpt.com/codex/install.sh | sh
|
||||
```
|
||||
|
||||
Run the following on Windows to install Codex CLI:
|
||||
|
||||
```
|
||||
powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"
|
||||
```
|
||||
|
||||
Codex CLI can also be installed via the following package managers:
|
||||
|
||||
```shell
|
||||
# Install using npm
|
||||
|
||||
Generated
+1
@@ -4146,6 +4146,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing-appender",
|
||||
"windows 0.58.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -66,6 +66,10 @@ codex sandbox [COMMAND]...
|
||||
codex sandbox --log-denials [COMMAND]...
|
||||
```
|
||||
|
||||
`codex sandbox` also accepts `--profile NAME` (`-p NAME`) to layer
|
||||
`$CODEX_HOME/NAME.config.toml` onto the base user config for the sandboxed
|
||||
command.
|
||||
|
||||
### Selecting a sandbox policy via `--sandbox`
|
||||
|
||||
The Rust CLI exposes a dedicated `--sandbox` (`-s`) flag that lets you pick the sandbox policy **without** having to reach for the generic `-c/--config` option:
|
||||
|
||||
@@ -1262,6 +1262,14 @@ fn compaction_event_serializes_expected_shape() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compaction_implementation_serializes_remote_v2() {
|
||||
let payload = serde_json::to_value(CompactionImplementation::ResponsesCompactionV2)
|
||||
.expect("serialize compaction implementation");
|
||||
|
||||
assert_eq!(payload, json!("responses_compaction_v2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_used_dedupe_is_keyed_by_turn_and_connector() {
|
||||
let (sender, _receiver) = mpsc::channel(1);
|
||||
|
||||
@@ -229,6 +229,7 @@ pub enum CompactionReason {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompactionImplementation {
|
||||
Responses,
|
||||
ResponsesCompactionV2,
|
||||
ResponsesCompact,
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -7715,6 +7715,12 @@
|
||||
},
|
||||
"ConfigRequirements": {
|
||||
"properties": {
|
||||
"allowAppshots": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowManagedHooksOnly": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
+6
@@ -4084,6 +4084,12 @@
|
||||
},
|
||||
"ConfigRequirements": {
|
||||
"properties": {
|
||||
"allowAppshots": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowManagedHooksOnly": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
+6
@@ -73,6 +73,12 @@
|
||||
},
|
||||
"ConfigRequirements": {
|
||||
"properties": {
|
||||
"allowAppshots": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowManagedHooksOnly": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
@@ -7,4 +7,4 @@ import type { ComputerUseRequirements } from "./ComputerUseRequirements";
|
||||
import type { ResidencyRequirement } from "./ResidencyRequirement";
|
||||
import type { SandboxMode } from "./SandboxMode";
|
||||
|
||||
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedPermissions: Array<string> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
|
||||
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedPermissions: Array<string> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
|
||||
|
||||
@@ -1393,6 +1393,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -1471,6 +1472,7 @@ mod tests {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-image".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -1806,6 +1808,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -1866,6 +1869,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -1978,6 +1982,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2043,6 +2048,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2109,6 +2115,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2199,6 +2206,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2283,6 +2291,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2347,6 +2356,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2367,6 +2377,7 @@ mod tests {
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2441,6 +2452,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2461,6 +2473,7 @@ mod tests {
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2530,6 +2543,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2595,6 +2609,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2660,6 +2675,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2680,6 +2696,7 @@ mod tests {
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2728,6 +2745,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2748,6 +2766,7 @@ mod tests {
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -2789,6 +2808,7 @@ mod tests {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-compact".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -3042,6 +3062,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -3098,6 +3119,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -3156,6 +3178,7 @@ mod tests {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -3204,6 +3227,7 @@ mod tests {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
|
||||
@@ -361,6 +361,7 @@ pub struct ConfigRequirements {
|
||||
pub allowed_permissions: Option<Vec<String>>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
|
||||
pub allow_managed_hooks_only: Option<bool>,
|
||||
pub allow_appshots: Option<bool>,
|
||||
pub computer_use: Option<ComputerUseRequirements>,
|
||||
pub feature_requirements: Option<BTreeMap<String, bool>>,
|
||||
#[experimental("configRequirements/read.hooks")]
|
||||
|
||||
@@ -1593,6 +1593,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
|
||||
allowed_permissions: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
|
||||
@@ -3219,6 +3219,7 @@ mod tests {
|
||||
"turn-1",
|
||||
&EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
trace_id: None,
|
||||
started_at: Some(42),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -3252,6 +3253,7 @@ mod tests {
|
||||
id: "turn-1".to_string(),
|
||||
msg: EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
trace_id: None,
|
||||
started_at: Some(42),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -3301,6 +3303,7 @@ mod tests {
|
||||
&event_turn_id,
|
||||
&EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: event_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: Some(42),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
|
||||
@@ -431,6 +431,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
|
||||
normalized
|
||||
}),
|
||||
allow_managed_hooks_only: requirements.allow_managed_hooks_only,
|
||||
allow_appshots: requirements.allow_appshots,
|
||||
computer_use: requirements
|
||||
.computer_use
|
||||
.map(map_computer_use_requirements_to_api),
|
||||
@@ -658,6 +659,17 @@ mod tests {
|
||||
assert_eq!(mapped.hooks, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_allow_appshots() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
allow_appshots: Some(false),
|
||||
..ConfigRequirementsToml::default()
|
||||
});
|
||||
|
||||
assert_eq!(mapped.allow_appshots, Some(false));
|
||||
assert_eq!(mapped.hooks, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_computer_use_requirements() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
|
||||
@@ -1155,6 +1155,7 @@ mod thread_processor_behavior_tests {
|
||||
"turn-1",
|
||||
&EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
|
||||
@@ -1431,6 +1431,7 @@ async fn thread_resume_token_usage_replay_ignores_stale_interrupted_tail_turn()
|
||||
"type": "event_msg",
|
||||
"payload": serde_json::to_value(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: stale_turn_id.to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -1518,6 +1519,7 @@ async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Re
|
||||
"type": "event_msg",
|
||||
"payload": serde_json::to_value(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: interrupted_turn_id.to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -1835,6 +1837,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is
|
||||
"type": "event_msg",
|
||||
"payload": serde_json::to_value(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
|
||||
@@ -41,9 +41,11 @@ use seatbelt::DenialLogger;
|
||||
pub async fn run_command_under_seatbelt(
|
||||
command: SeatbeltCommand,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
) -> anyhow::Result<()> {
|
||||
let SeatbeltCommand {
|
||||
permissions_profile,
|
||||
config_profile: _,
|
||||
cwd,
|
||||
include_managed_config,
|
||||
allow_unix_sockets,
|
||||
@@ -60,6 +62,7 @@ pub async fn run_command_under_seatbelt(
|
||||
permissions_profile,
|
||||
cwd,
|
||||
managed_requirements_mode,
|
||||
loader_overrides,
|
||||
},
|
||||
command,
|
||||
config_overrides,
|
||||
@@ -75,6 +78,7 @@ pub async fn run_command_under_seatbelt(
|
||||
pub async fn run_command_under_seatbelt(
|
||||
_command: SeatbeltCommand,
|
||||
_codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
_loader_overrides: LoaderOverrides,
|
||||
) -> anyhow::Result<()> {
|
||||
anyhow::bail!("Seatbelt sandbox is only available on macOS");
|
||||
}
|
||||
@@ -82,9 +86,11 @@ pub async fn run_command_under_seatbelt(
|
||||
pub async fn run_command_under_landlock(
|
||||
command: LandlockCommand,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
) -> anyhow::Result<()> {
|
||||
let LandlockCommand {
|
||||
permissions_profile,
|
||||
config_profile: _,
|
||||
cwd,
|
||||
include_managed_config,
|
||||
config_overrides,
|
||||
@@ -99,6 +105,7 @@ pub async fn run_command_under_landlock(
|
||||
permissions_profile,
|
||||
cwd,
|
||||
managed_requirements_mode,
|
||||
loader_overrides,
|
||||
},
|
||||
command,
|
||||
config_overrides,
|
||||
@@ -113,9 +120,11 @@ pub async fn run_command_under_landlock(
|
||||
pub async fn run_command_under_windows_sandbox(
|
||||
command: WindowsCommand,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
) -> anyhow::Result<()> {
|
||||
let WindowsCommand {
|
||||
permissions_profile,
|
||||
config_profile: _,
|
||||
cwd,
|
||||
include_managed_config,
|
||||
config_overrides,
|
||||
@@ -130,6 +139,7 @@ pub async fn run_command_under_windows_sandbox(
|
||||
permissions_profile,
|
||||
cwd,
|
||||
managed_requirements_mode,
|
||||
loader_overrides,
|
||||
},
|
||||
command,
|
||||
config_overrides,
|
||||
@@ -153,6 +163,7 @@ struct DebugSandboxConfigOptions {
|
||||
permissions_profile: Option<String>,
|
||||
cwd: Option<PathBuf>,
|
||||
managed_requirements_mode: ManagedRequirementsMode,
|
||||
loader_overrides: LoaderOverrides,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -650,7 +661,7 @@ async fn load_debug_sandbox_config(
|
||||
}
|
||||
|
||||
async fn load_debug_sandbox_config_with_codex_home(
|
||||
mut cli_overrides: Vec<(String, TomlValue)>,
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
options: DebugSandboxConfigOptions,
|
||||
codex_home: Option<PathBuf>,
|
||||
@@ -660,7 +671,9 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
permissions_profile,
|
||||
cwd,
|
||||
managed_requirements_mode,
|
||||
loader_overrides,
|
||||
} = options;
|
||||
let mut cli_overrides = cli_overrides;
|
||||
|
||||
if let Some(permissions_profile) = permissions_profile {
|
||||
cli_overrides.push((
|
||||
@@ -674,7 +687,7 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
// config. Keep that behavior unless this invocation explicitly passes a
|
||||
// legacy `sandbox_mode` CLI override for compatibility with older callers.
|
||||
let uses_legacy_sandbox_mode_override = cli_overrides_use_legacy_sandbox_mode(&cli_overrides);
|
||||
let config = build_debug_sandbox_config(
|
||||
let config = build_debug_sandbox_config_with_loader_overrides(
|
||||
cli_overrides.clone(),
|
||||
ConfigOverrides {
|
||||
cwd: cwd.clone(),
|
||||
@@ -683,6 +696,7 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
},
|
||||
codex_home.clone(),
|
||||
managed_requirements_mode,
|
||||
loader_overrides.clone(),
|
||||
strict_config,
|
||||
)
|
||||
.await?;
|
||||
@@ -691,7 +705,7 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
build_debug_sandbox_config(
|
||||
build_debug_sandbox_config_with_loader_overrides(
|
||||
cli_overrides,
|
||||
ConfigOverrides {
|
||||
sandbox_mode: Some(SandboxMode::ReadOnly),
|
||||
@@ -701,17 +715,19 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
},
|
||||
codex_home,
|
||||
managed_requirements_mode,
|
||||
loader_overrides,
|
||||
strict_config,
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn build_debug_sandbox_config(
|
||||
async fn build_debug_sandbox_config_with_loader_overrides(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
harness_overrides: ConfigOverrides,
|
||||
codex_home: Option<PathBuf>,
|
||||
managed_requirements_mode: ManagedRequirementsMode,
|
||||
mut loader_overrides: LoaderOverrides,
|
||||
strict_config: bool,
|
||||
) -> std::io::Result<Config> {
|
||||
let mut builder = ConfigBuilder::default()
|
||||
@@ -719,11 +735,9 @@ async fn build_debug_sandbox_config(
|
||||
.harness_overrides(harness_overrides)
|
||||
.strict_config(strict_config);
|
||||
if matches!(managed_requirements_mode, ManagedRequirementsMode::Ignore) {
|
||||
builder = builder.loader_overrides(LoaderOverrides {
|
||||
ignore_managed_requirements: true,
|
||||
..LoaderOverrides::default()
|
||||
});
|
||||
loader_overrides.ignore_managed_requirements = true;
|
||||
}
|
||||
builder = builder.loader_overrides(loader_overrides);
|
||||
if let Some(codex_home) = codex_home {
|
||||
builder = builder
|
||||
.codex_home(codex_home.clone())
|
||||
@@ -750,6 +764,24 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn build_debug_sandbox_config(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
harness_overrides: ConfigOverrides,
|
||||
codex_home: Option<PathBuf>,
|
||||
managed_requirements_mode: ManagedRequirementsMode,
|
||||
strict_config: bool,
|
||||
) -> std::io::Result<Config> {
|
||||
build_debug_sandbox_config_with_loader_overrides(
|
||||
cli_overrides,
|
||||
harness_overrides,
|
||||
codex_home,
|
||||
managed_requirements_mode,
|
||||
LoaderOverrides::default(),
|
||||
strict_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn escape_toml_path(path: &std::path::Path) -> String {
|
||||
path.display().to_string().replace('\\', "\\\\")
|
||||
}
|
||||
@@ -758,6 +790,18 @@ mod tests {
|
||||
codex_home: &TempDir,
|
||||
docs: &std::path::Path,
|
||||
private: &std::path::Path,
|
||||
) -> std::io::Result<()> {
|
||||
write_permissions_profile_config_to_path(
|
||||
&codex_home.path().join("config.toml"),
|
||||
docs,
|
||||
private,
|
||||
)
|
||||
}
|
||||
|
||||
fn write_permissions_profile_config_to_path(
|
||||
config_path: &std::path::Path,
|
||||
docs: &std::path::Path,
|
||||
private: &std::path::Path,
|
||||
) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(private)?;
|
||||
let config = format!(
|
||||
@@ -772,7 +816,7 @@ mod tests {
|
||||
escape_toml_path(docs),
|
||||
escape_toml_path(private),
|
||||
);
|
||||
std::fs::write(codex_home.path().join("config.toml"), config)?;
|
||||
std::fs::write(config_path, config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -812,6 +856,7 @@ mod tests {
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Include,
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home_path),
|
||||
/*strict_config*/ false,
|
||||
@@ -836,6 +881,70 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_honors_config_profile_loader_overrides() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let sandbox_paths = TempDir::new()?;
|
||||
let docs = sandbox_paths.path().join("docs");
|
||||
let private = docs.join("private");
|
||||
let profile_path = codex_home.path().join("work.config.toml");
|
||||
write_permissions_profile_config_to_path(&profile_path, &docs, &private)?;
|
||||
let codex_home_path = codex_home.path().to_path_buf();
|
||||
let loader_overrides = LoaderOverrides {
|
||||
user_config_path: Some(AbsolutePathBuf::from_absolute_path(&profile_path)?),
|
||||
user_config_profile: Some("work".parse().expect("profile name should parse")),
|
||||
..LoaderOverrides::default()
|
||||
};
|
||||
|
||||
let profile_config = build_debug_sandbox_config_with_loader_overrides(
|
||||
Vec::new(),
|
||||
ConfigOverrides::default(),
|
||||
Some(codex_home_path.clone()),
|
||||
ManagedRequirementsMode::Include,
|
||||
loader_overrides.clone(),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
let read_only_config = build_debug_sandbox_config(
|
||||
Vec::new(),
|
||||
ConfigOverrides {
|
||||
sandbox_mode: Some(SandboxMode::ReadOnly),
|
||||
..Default::default()
|
||||
},
|
||||
Some(codex_home_path.clone()),
|
||||
ManagedRequirementsMode::Include,
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let config = load_debug_sandbox_config_with_codex_home(
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Include,
|
||||
loader_overrides,
|
||||
},
|
||||
Some(codex_home_path),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(config_uses_permission_profiles(&config));
|
||||
assert_ne!(
|
||||
profile_config.permissions.file_system_sandbox_policy(),
|
||||
read_only_config.permissions.file_system_sandbox_policy(),
|
||||
"test fixture should distinguish the profile config from read-only"
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
profile_config.permissions.file_system_sandbox_policy(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_honors_explicit_legacy_sandbox_mode() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -872,6 +981,7 @@ mod tests {
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Include,
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home_path),
|
||||
/*strict_config*/ false,
|
||||
@@ -929,6 +1039,7 @@ mod tests {
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Include,
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home_path),
|
||||
/*strict_config*/ false,
|
||||
@@ -955,6 +1066,7 @@ mod tests {
|
||||
permissions_profile: Some(":workspace".to_string()),
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Ignore,
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
/*strict_config*/ false,
|
||||
@@ -993,6 +1105,7 @@ mod tests {
|
||||
permissions_profile: Some("limited-read-test".to_string()),
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Ignore,
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
/*strict_config*/ false,
|
||||
@@ -1031,6 +1144,7 @@ mod tests {
|
||||
permissions_profile: Some(":workspace".to_string()),
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
managed_requirements_mode: ManagedRequirementsMode::Ignore,
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
/*strict_config*/ false,
|
||||
|
||||
@@ -5,6 +5,7 @@ pub(crate) mod login;
|
||||
use clap::Parser;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use codex_utils_cli::ProfileV2Name;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub use debug_sandbox::run_command_under_landlock;
|
||||
@@ -28,6 +29,10 @@ pub struct SeatbeltCommand {
|
||||
#[arg(long = "permissions-profile", value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
|
||||
/// Layer $CODEX_HOME/<name>.config.toml on top of the base user config.
|
||||
#[arg(long = "profile", short = 'p')]
|
||||
pub config_profile: Option<ProfileV2Name>,
|
||||
|
||||
/// Working directory used for profile resolution and command execution.
|
||||
#[arg(
|
||||
short = 'C',
|
||||
@@ -72,6 +77,10 @@ pub struct LandlockCommand {
|
||||
#[arg(long = "permissions-profile", value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
|
||||
/// Layer $CODEX_HOME/<name>.config.toml on top of the base user config.
|
||||
#[arg(long = "profile", short = 'p')]
|
||||
pub config_profile: Option<ProfileV2Name>,
|
||||
|
||||
/// Working directory used for profile resolution and command execution.
|
||||
#[arg(
|
||||
short = 'C',
|
||||
@@ -103,6 +112,10 @@ pub struct WindowsCommand {
|
||||
#[arg(long = "permissions-profile", value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
|
||||
/// Layer $CODEX_HOME/<name>.config.toml on top of the base user config.
|
||||
#[arg(long = "profile", short = 'p')]
|
||||
pub config_profile: Option<ProfileV2Name>,
|
||||
|
||||
/// Working directory used for profile resolution and command execution.
|
||||
#[arg(
|
||||
short = 'C',
|
||||
|
||||
@@ -353,6 +353,10 @@ type HostSandboxArgs = UnsupportedSandboxArgs;
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
#[derive(Debug, Parser)]
|
||||
struct UnsupportedSandboxArgs {
|
||||
/// Layer $CODEX_HOME/<name>.config.toml on top of the base user config.
|
||||
#[arg(long = "profile", short = 'p')]
|
||||
pub config_profile: Option<ProfileV2Name>,
|
||||
|
||||
#[clap(skip)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
|
||||
@@ -1242,6 +1246,11 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
root_remote_auth_token_env.as_deref(),
|
||||
"sandbox",
|
||||
)?;
|
||||
let config_profile = sandbox_cli
|
||||
.config_profile
|
||||
.as_ref()
|
||||
.or(interactive.config_profile_v2.as_ref());
|
||||
let loader_overrides = loader_overrides_for_profile(config_profile)?;
|
||||
prepend_config_flags(
|
||||
&mut sandbox_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
@@ -1250,22 +1259,28 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
codex_cli::run_command_under_seatbelt(
|
||||
sandbox_cli,
|
||||
arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
loader_overrides,
|
||||
)
|
||||
.await?;
|
||||
#[cfg(target_os = "linux")]
|
||||
codex_cli::run_command_under_landlock(
|
||||
sandbox_cli,
|
||||
arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
loader_overrides,
|
||||
)
|
||||
.await?;
|
||||
#[cfg(target_os = "windows")]
|
||||
codex_cli::run_command_under_windows_sandbox(
|
||||
sandbox_cli,
|
||||
arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
loader_overrides,
|
||||
)
|
||||
.await?;
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
anyhow::bail!("`codex sandbox` is not supported on this operating system");
|
||||
{
|
||||
let _ = loader_overrides;
|
||||
anyhow::bail!("`codex sandbox` is not supported on this operating system");
|
||||
}
|
||||
}
|
||||
Some(Subcommand::Debug(DebugCommand { subcommand })) => match subcommand {
|
||||
DebugSubcommand::Models(cmd) => {
|
||||
@@ -1441,11 +1456,12 @@ fn profile_v2_for_subcommand<'a>(
|
||||
| Subcommand::Resume(_)
|
||||
| Subcommand::Fork(_)
|
||||
| Subcommand::Mcp(_)
|
||||
| Subcommand::Sandbox(_)
|
||||
| Subcommand::Debug(DebugCommand {
|
||||
subcommand: DebugSubcommand::PromptInput(_),
|
||||
}) => Ok(Some(profile_v2)),
|
||||
_ => anyhow::bail!(
|
||||
"--profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, `codex review`, `codex resume`, `codex fork`, `codex mcp`, and `codex debug prompt-input`."
|
||||
"--profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, `codex review`, `codex resume`, `codex fork`, `codex mcp`, `codex sandbox`, and `codex debug prompt-input`."
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -2252,6 +2268,12 @@ mod tests {
|
||||
.as_deref(),
|
||||
Some("work")
|
||||
);
|
||||
assert_eq!(
|
||||
profile_v2_for_args(&["codex", "--profile", "work", "sandbox"])
|
||||
.expect("sandbox supports config profile")
|
||||
.as_deref(),
|
||||
Some("work")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2500,6 +2522,21 @@ mod tests {
|
||||
assert_eq!(command.command, vec!["echo"]);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
|
||||
#[test]
|
||||
fn sandbox_parses_config_profile() {
|
||||
let cli =
|
||||
MultitoolCli::try_parse_from(["codex", "sandbox", "--profile", "work", "--", "echo"])
|
||||
.expect("parse");
|
||||
|
||||
let Some(Subcommand::Sandbox(command)) = cli.subcommand else {
|
||||
panic!("expected sandbox command");
|
||||
};
|
||||
|
||||
assert_eq!(command.config_profile.as_deref(), Some("work"));
|
||||
assert_eq!(command.command, vec!["echo"]);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
|
||||
#[test]
|
||||
fn sandbox_rejects_explicit_profile_controls_without_profile() {
|
||||
|
||||
+41
-18
@@ -8,6 +8,7 @@ use anyhow::bail;
|
||||
use clap::ArgGroup;
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::McpServerOAuthConfig;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_core::McpManager;
|
||||
use codex_core::config::Config;
|
||||
@@ -134,6 +135,14 @@ pub struct AddMcpStreamableHttpArgs {
|
||||
requires = "url"
|
||||
)]
|
||||
pub bearer_token_env_var: Option<String>,
|
||||
|
||||
/// Optional OAuth client identifier to use for this MCP server.
|
||||
#[arg(long = "oauth-client-id", value_name = "CLIENT_ID", requires = "url")]
|
||||
pub oauth_client_id: Option<String>,
|
||||
|
||||
/// Optional OAuth resource parameter to include during MCP login.
|
||||
#[arg(long = "oauth-resource", value_name = "RESOURCE", requires = "url")]
|
||||
pub oauth_resource: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
@@ -282,7 +291,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
.await
|
||||
.with_context(|| format!("failed to load MCP servers from {}", codex_home.display()))?;
|
||||
|
||||
let transport = match transport_args {
|
||||
let (transport, oauth_client_id, oauth_resource) = match transport_args {
|
||||
AddMcpTransportArgs {
|
||||
stdio: Some(stdio), ..
|
||||
} => {
|
||||
@@ -297,27 +306,37 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
} else {
|
||||
Some(stdio.env.into_iter().collect::<HashMap<_, _>>())
|
||||
};
|
||||
McpServerTransportConfig::Stdio {
|
||||
command: command_bin,
|
||||
args: command_args,
|
||||
env: env_map,
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
}
|
||||
(
|
||||
McpServerTransportConfig::Stdio {
|
||||
command: command_bin,
|
||||
args: command_args,
|
||||
env: env_map,
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
AddMcpTransportArgs {
|
||||
streamable_http:
|
||||
Some(AddMcpStreamableHttpArgs {
|
||||
url,
|
||||
bearer_token_env_var,
|
||||
oauth_client_id,
|
||||
oauth_resource,
|
||||
}),
|
||||
..
|
||||
} => McpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
bearer_token_env_var,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
} => (
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
bearer_token_env_var,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
oauth_client_id,
|
||||
oauth_resource,
|
||||
),
|
||||
AddMcpTransportArgs { .. } => bail!("exactly one of --command or --url must be provided"),
|
||||
};
|
||||
|
||||
@@ -334,8 +353,12 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth: None,
|
||||
oauth_resource: None,
|
||||
oauth: oauth_client_id
|
||||
.clone()
|
||||
.map(|client_id| McpServerOAuthConfig {
|
||||
client_id: Some(client_id),
|
||||
}),
|
||||
oauth_resource: oauth_resource.clone(),
|
||||
tools: HashMap::new(),
|
||||
};
|
||||
|
||||
@@ -364,8 +387,8 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
oauth_config.http_headers,
|
||||
oauth_config.env_http_headers,
|
||||
&resolved_scopes,
|
||||
/*oauth_client_id*/ None,
|
||||
/*oauth_resource*/ None,
|
||||
oauth_client_id.as_deref(),
|
||||
oauth_resource.as_deref(),
|
||||
config.mcp_oauth_callback_port,
|
||||
config.mcp_oauth_callback_url.as_deref(),
|
||||
)
|
||||
|
||||
@@ -198,6 +198,42 @@ async fn add_streamable_http_with_custom_env_var() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_streamable_http_with_oauth_options() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let mut add_cmd = codex_command(codex_home.path())?;
|
||||
add_cmd
|
||||
.args([
|
||||
"mcp",
|
||||
"add",
|
||||
"oauth-server",
|
||||
"--url",
|
||||
"https://example.com/mcp",
|
||||
"--oauth-client-id",
|
||||
"eci-prd-pub-codex-123",
|
||||
"--oauth-resource",
|
||||
"https://resource.example.com",
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let servers = load_global_mcp_servers(codex_home.path()).await?;
|
||||
let oauth_server = servers
|
||||
.get("oauth-server")
|
||||
.expect("oauth server should exist");
|
||||
assert_eq!(
|
||||
oauth_server.oauth_client_id(),
|
||||
Some("eci-prd-pub-codex-123")
|
||||
);
|
||||
assert_eq!(
|
||||
oauth_server.oauth_resource.as_deref(),
|
||||
Some("https://resource.example.com")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_streamable_http_rejects_removed_flag() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -1220,6 +1220,7 @@ mod tests {
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -1305,6 +1306,7 @@ mod tests {
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -1341,6 +1343,7 @@ mod tests {
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -1394,6 +1397,7 @@ mod tests {
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -1576,6 +1580,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -1659,6 +1664,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -1740,6 +1746,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -1949,6 +1956,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -1992,6 +2000,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -2055,6 +2064,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -2114,6 +2124,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -2175,6 +2186,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -2237,6 +2249,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -2303,6 +2316,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -2395,6 +2409,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
@@ -2433,6 +2448,7 @@ command = "sample-mcp"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
use crate::auth::SharedAuthProvider;
|
||||
use crate::endpoint::session::EndpointSession;
|
||||
use crate::error::ApiError;
|
||||
use crate::images::ImageEditRequest;
|
||||
use crate::images::ImageGenerationRequest;
|
||||
use crate::images::ImageResponse;
|
||||
use crate::provider::Provider;
|
||||
use codex_client::HttpTransport;
|
||||
use codex_client::RequestTelemetry;
|
||||
use http::HeaderMap;
|
||||
use http::Method;
|
||||
use serde::Serialize;
|
||||
use serde_json::to_value;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ImagesClient<T: HttpTransport> {
|
||||
session: EndpointSession<T>,
|
||||
}
|
||||
|
||||
impl<T: HttpTransport> ImagesClient<T> {
|
||||
pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self {
|
||||
Self {
|
||||
session: EndpointSession::new(transport, provider, auth),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_telemetry(self, request: Option<Arc<dyn RequestTelemetry>>) -> Self {
|
||||
Self {
|
||||
session: self.session.with_request_telemetry(request),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate(
|
||||
&self,
|
||||
request: &ImageGenerationRequest,
|
||||
extra_headers: HeaderMap,
|
||||
) -> Result<ImageResponse, ApiError> {
|
||||
self.post_image_request(
|
||||
"images/generations",
|
||||
request,
|
||||
extra_headers,
|
||||
"image generation",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn edit(
|
||||
&self,
|
||||
request: &ImageEditRequest,
|
||||
extra_headers: HeaderMap,
|
||||
) -> Result<ImageResponse, ApiError> {
|
||||
self.post_image_request("images/edits", request, extra_headers, "image edit")
|
||||
.await
|
||||
}
|
||||
|
||||
async fn post_image_request<R: Serialize>(
|
||||
&self,
|
||||
path: &str,
|
||||
request: &R,
|
||||
extra_headers: HeaderMap,
|
||||
operation: &str,
|
||||
) -> Result<ImageResponse, ApiError> {
|
||||
let body = to_value(request)
|
||||
.map_err(|e| ApiError::Stream(format!("failed to encode {operation} request: {e}")))?;
|
||||
let resp = self
|
||||
.session
|
||||
.execute(Method::POST, path, extra_headers, Some(body))
|
||||
.await?;
|
||||
serde_json::from_slice(&resp.body)
|
||||
.map_err(|e| ApiError::Stream(format!("failed to decode {operation} response: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::auth::AuthProvider;
|
||||
use crate::images::ImageBackground;
|
||||
use crate::images::ImageData;
|
||||
use crate::images::ImageQuality;
|
||||
use crate::images::ImageUrl;
|
||||
use crate::provider::RetryConfig;
|
||||
use async_trait::async_trait;
|
||||
use codex_client::Request;
|
||||
use codex_client::RequestBody;
|
||||
use codex_client::Response;
|
||||
use codex_client::StreamResponse;
|
||||
use codex_client::TransportError;
|
||||
use http::StatusCode;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct DummyAuth;
|
||||
|
||||
impl AuthProvider for DummyAuth {
|
||||
fn add_auth_headers(&self, _headers: &mut HeaderMap) {}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CapturingTransport {
|
||||
last_request: Arc<Mutex<Option<Request>>>,
|
||||
response_body: Arc<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl CapturingTransport {
|
||||
fn new(response_body: Vec<u8>) -> Self {
|
||||
Self {
|
||||
last_request: Arc::new(Mutex::new(None)),
|
||||
response_body: Arc::new(response_body),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HttpTransport for CapturingTransport {
|
||||
async fn execute(&self, req: Request) -> Result<Response, TransportError> {
|
||||
*self.last_request.lock().expect("lock request store") = Some(req);
|
||||
Ok(Response {
|
||||
status: StatusCode::OK,
|
||||
headers: HeaderMap::new(),
|
||||
body: self.response_body.as_ref().clone().into(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn stream(&self, _req: Request) -> Result<StreamResponse, TransportError> {
|
||||
Err(TransportError::Build("stream should not run".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn provider() -> Provider {
|
||||
Provider {
|
||||
name: "test".to_string(),
|
||||
base_url: "https://example.com/api/codex".to_string(),
|
||||
query_params: None,
|
||||
headers: HeaderMap::new(),
|
||||
retry: RetryConfig {
|
||||
max_attempts: 1,
|
||||
base_delay: Duration::from_millis(1),
|
||||
retry_429: false,
|
||||
retry_5xx: true,
|
||||
retry_transport: true,
|
||||
},
|
||||
stream_idle_timeout: Duration::from_secs(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn response_body() -> Vec<u8> {
|
||||
serde_json::to_vec(&json!({
|
||||
"created": 1778832973u64,
|
||||
"background": "opaque",
|
||||
"data": [{"b64_json": "REDACT"}],
|
||||
"output_format": "png",
|
||||
"quality": "medium",
|
||||
"size": "1024x1536",
|
||||
"usage": {
|
||||
"input_tokens": 1474,
|
||||
"input_tokens_details": {
|
||||
"image_tokens": 1457,
|
||||
"text_tokens": 17,
|
||||
},
|
||||
"output_tokens": 1372,
|
||||
"output_tokens_details": {
|
||||
"image_tokens": 1372,
|
||||
"text_tokens": 0,
|
||||
},
|
||||
"total_tokens": 2846,
|
||||
}
|
||||
}))
|
||||
.expect("serialize response")
|
||||
}
|
||||
|
||||
fn expected_response() -> ImageResponse {
|
||||
ImageResponse {
|
||||
created: 1778832973,
|
||||
background: Some(ImageBackground::Opaque),
|
||||
data: vec![ImageData {
|
||||
b64_json: "REDACT".to_string(),
|
||||
}],
|
||||
quality: Some(ImageQuality::Medium),
|
||||
size: Some("1024x1536".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn captured_request(transport: &CapturingTransport) -> Request {
|
||||
transport
|
||||
.last_request
|
||||
.lock()
|
||||
.expect("lock request store")
|
||||
.clone()
|
||||
.expect("request should be captured")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generate_posts_typed_request_and_parses_image_response() {
|
||||
let transport = CapturingTransport::new(response_body());
|
||||
let client = ImagesClient::new(transport.clone(), provider(), Arc::new(DummyAuth));
|
||||
|
||||
let response = client
|
||||
.generate(
|
||||
&ImageGenerationRequest {
|
||||
prompt: "a red fox in a field".to_string(),
|
||||
background: Some(ImageBackground::Opaque),
|
||||
model: "gpt-image-1.5".to_string(),
|
||||
n: None,
|
||||
quality: Some(ImageQuality::Medium),
|
||||
size: Some("1024x1536".to_string()),
|
||||
},
|
||||
HeaderMap::new(),
|
||||
)
|
||||
.await
|
||||
.expect("image generation request should succeed");
|
||||
|
||||
assert_eq!(response, expected_response());
|
||||
|
||||
let request = captured_request(&transport);
|
||||
assert_eq!(
|
||||
request.url,
|
||||
"https://example.com/api/codex/images/generations"
|
||||
);
|
||||
assert_eq!(
|
||||
request.body.as_ref().and_then(RequestBody::json),
|
||||
Some(&json!({
|
||||
"prompt": "a red fox in a field",
|
||||
"background": "opaque",
|
||||
"model": "gpt-image-1.5",
|
||||
"quality": "medium",
|
||||
"size": "1024x1536",
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn edit_posts_typed_request_and_parses_image_response() {
|
||||
let transport = CapturingTransport::new(response_body());
|
||||
let client = ImagesClient::new(transport.clone(), provider(), Arc::new(DummyAuth));
|
||||
|
||||
let response = client
|
||||
.edit(
|
||||
&ImageEditRequest {
|
||||
images: vec![ImageUrl {
|
||||
image_url: "data:image/png;base64,Zm9v".to_string(),
|
||||
}],
|
||||
prompt: "add a red hat".to_string(),
|
||||
background: None,
|
||||
model: "gpt-image-1.5".to_string(),
|
||||
n: None,
|
||||
quality: None,
|
||||
size: None,
|
||||
},
|
||||
HeaderMap::new(),
|
||||
)
|
||||
.await
|
||||
.expect("image edit request should succeed");
|
||||
|
||||
assert_eq!(response, expected_response());
|
||||
|
||||
let request = captured_request(&transport);
|
||||
assert_eq!(request.url, "https://example.com/api/codex/images/edits");
|
||||
assert_eq!(
|
||||
request.body.as_ref().and_then(RequestBody::json),
|
||||
Some(&json!({
|
||||
"images": [{"image_url": "data:image/png;base64,Zm9v"}],
|
||||
"prompt": "add a red hat",
|
||||
"model": "gpt-image-1.5",
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn image_response_requires_image_data() {
|
||||
let transport = CapturingTransport::new(
|
||||
serde_json::to_vec(&json!({"created": 1778832973u64})).expect("serialize response"),
|
||||
);
|
||||
let client = ImagesClient::new(transport, provider(), Arc::new(DummyAuth));
|
||||
|
||||
let error = client
|
||||
.generate(
|
||||
&ImageGenerationRequest {
|
||||
prompt: "a red fox in a field".to_string(),
|
||||
background: None,
|
||||
model: "gpt-image-1.5".to_string(),
|
||||
n: None,
|
||||
quality: None,
|
||||
size: None,
|
||||
},
|
||||
HeaderMap::new(),
|
||||
)
|
||||
.await
|
||||
.expect_err("image response without data should fail");
|
||||
|
||||
let ApiError::Stream(message) = error else {
|
||||
panic!("expected image response decode error");
|
||||
};
|
||||
assert!(
|
||||
message.starts_with("failed to decode image generation response: missing field `data`"),
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub(crate) mod compact;
|
||||
pub(crate) mod images;
|
||||
pub(crate) mod memories;
|
||||
pub(crate) mod models;
|
||||
pub(crate) mod realtime_call;
|
||||
@@ -9,6 +10,7 @@ pub(crate) mod search;
|
||||
mod session;
|
||||
|
||||
pub use compact::CompactClient;
|
||||
pub use images::ImagesClient;
|
||||
pub use memories::MemoriesClient;
|
||||
pub use models::ModelsClient;
|
||||
pub use realtime_call::RealtimeCallClient;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
pub struct ImageGenerationRequest {
|
||||
pub prompt: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<ImageBackground>,
|
||||
pub model: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub n: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality: Option<ImageQuality>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
pub struct ImageEditRequest {
|
||||
pub images: Vec<ImageUrl>,
|
||||
pub prompt: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<ImageBackground>,
|
||||
pub model: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub n: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality: Option<ImageQuality>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ImageUrl {
|
||||
pub image_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ImageBackground {
|
||||
Transparent,
|
||||
Opaque,
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ImageQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct ImageResponse {
|
||||
pub created: u64,
|
||||
pub data: Vec<ImageData>,
|
||||
#[serde(default)]
|
||||
pub background: Option<ImageBackground>,
|
||||
#[serde(default)]
|
||||
pub quality: Option<ImageQuality>,
|
||||
#[serde(default)]
|
||||
pub size: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct ImageData {
|
||||
pub b64_json: String,
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub(crate) mod common;
|
||||
pub(crate) mod endpoint;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod files;
|
||||
pub(crate) mod images;
|
||||
pub(crate) mod provider;
|
||||
pub(crate) mod rate_limits;
|
||||
pub(crate) mod requests;
|
||||
@@ -41,6 +42,7 @@ pub use crate::common::WS_REQUEST_HEADER_TRACESTATE_CLIENT_METADATA_KEY;
|
||||
pub use crate::common::create_text_param_for_request;
|
||||
pub use crate::common::response_create_client_metadata;
|
||||
pub use crate::endpoint::CompactClient;
|
||||
pub use crate::endpoint::ImagesClient;
|
||||
pub use crate::endpoint::MemoriesClient;
|
||||
pub use crate::endpoint::ModelsClient;
|
||||
pub use crate::endpoint::RealtimeCallClient;
|
||||
@@ -63,6 +65,13 @@ pub use crate::endpoint::SearchClient;
|
||||
pub use crate::endpoint::session_update_session_json;
|
||||
pub use crate::error::ApiError;
|
||||
pub use crate::files::upload_local_file;
|
||||
pub use crate::images::ImageBackground;
|
||||
pub use crate::images::ImageData;
|
||||
pub use crate::images::ImageEditRequest;
|
||||
pub use crate::images::ImageGenerationRequest;
|
||||
pub use crate::images::ImageQuality;
|
||||
pub use crate::images::ImageResponse;
|
||||
pub use crate::images::ImageUrl;
|
||||
pub use crate::provider::Provider;
|
||||
pub use crate::provider::RetryConfig;
|
||||
pub use crate::provider::is_azure_responses_provider;
|
||||
|
||||
@@ -89,6 +89,7 @@ pub struct ConfigRequirements {
|
||||
pub permission_profile: ConstrainedWithSource<PermissionProfile>,
|
||||
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
|
||||
pub allow_managed_hooks_only: Option<Sourced<bool>>,
|
||||
pub allow_appshots: Option<Sourced<bool>>,
|
||||
pub computer_use: Option<Sourced<ComputerUseRequirementsToml>>,
|
||||
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
|
||||
pub managed_hooks: Option<ConstrainedWithSource<ManagedHooksRequirementsToml>>,
|
||||
@@ -124,6 +125,7 @@ impl Default for ConfigRequirements {
|
||||
/*source*/ None,
|
||||
),
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
feature_requirements: None,
|
||||
managed_hooks: None,
|
||||
@@ -750,6 +752,7 @@ pub struct ConfigRequirementsToml {
|
||||
pub remote_sandbox_config: Option<Vec<RemoteSandboxConfigToml>>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
|
||||
pub allow_managed_hooks_only: Option<bool>,
|
||||
pub allow_appshots: Option<bool>,
|
||||
pub computer_use: Option<ComputerUseRequirementsToml>,
|
||||
#[serde(rename = "features", alias = "feature_requirements")]
|
||||
pub feature_requirements: Option<FeatureRequirementsToml>,
|
||||
@@ -801,6 +804,7 @@ pub struct ConfigRequirementsWithSources {
|
||||
pub allowed_permissions: Option<Sourced<Vec<String>>>,
|
||||
pub allowed_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
|
||||
pub allow_managed_hooks_only: Option<Sourced<bool>>,
|
||||
pub allow_appshots: Option<Sourced<bool>>,
|
||||
pub computer_use: Option<Sourced<ComputerUseRequirementsToml>>,
|
||||
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
|
||||
pub hooks: Option<Sourced<ManagedHooksRequirementsToml>>,
|
||||
@@ -840,6 +844,7 @@ impl ConfigRequirementsWithSources {
|
||||
remote_sandbox_config: _,
|
||||
allowed_web_search_modes: _,
|
||||
allow_managed_hooks_only: _,
|
||||
allow_appshots: _,
|
||||
computer_use: _,
|
||||
feature_requirements: _,
|
||||
hooks: _,
|
||||
@@ -872,6 +877,7 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_permissions,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
allow_appshots,
|
||||
computer_use,
|
||||
feature_requirements,
|
||||
hooks,
|
||||
@@ -902,6 +908,7 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_permissions,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
allow_appshots,
|
||||
computer_use,
|
||||
feature_requirements,
|
||||
hooks,
|
||||
@@ -922,6 +929,7 @@ impl ConfigRequirementsWithSources {
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value),
|
||||
allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value),
|
||||
allow_appshots: allow_appshots.map(|sourced| sourced.value),
|
||||
computer_use: computer_use.map(|sourced| sourced.value),
|
||||
feature_requirements: feature_requirements.map(|sourced| sourced.value),
|
||||
hooks: hooks.map(|sourced| sourced.value),
|
||||
@@ -1008,6 +1016,7 @@ impl ConfigRequirementsToml {
|
||||
&& self.remote_sandbox_config.is_none()
|
||||
&& self.allowed_web_search_modes.is_none()
|
||||
&& self.allow_managed_hooks_only.is_none()
|
||||
&& self.allow_appshots.is_none()
|
||||
&& self
|
||||
.computer_use
|
||||
.as_ref()
|
||||
@@ -1054,6 +1063,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
allowed_permissions: _,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
allow_appshots,
|
||||
computer_use,
|
||||
feature_requirements,
|
||||
hooks,
|
||||
@@ -1291,6 +1301,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
permission_profile,
|
||||
web_search_mode,
|
||||
allow_managed_hooks_only,
|
||||
allow_appshots,
|
||||
computer_use,
|
||||
feature_requirements,
|
||||
managed_hooks,
|
||||
@@ -1361,6 +1372,7 @@ mod tests {
|
||||
remote_sandbox_config: _,
|
||||
allowed_web_search_modes,
|
||||
allow_managed_hooks_only,
|
||||
allow_appshots,
|
||||
computer_use,
|
||||
feature_requirements,
|
||||
hooks,
|
||||
@@ -1386,6 +1398,8 @@ mod tests {
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
allow_managed_hooks_only: allow_managed_hooks_only
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
allow_appshots: allow_appshots
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
computer_use: computer_use.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
feature_requirements: feature_requirements
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
@@ -1466,6 +1480,19 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_allow_appshots() -> Result<()> {
|
||||
let requirements: ConfigRequirementsToml = from_str(
|
||||
r#"
|
||||
allow_appshots = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_eq!(requirements.allow_appshots, Some(true));
|
||||
assert!(!requirements.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filesystem_requirements_table_cannot_define_a_permission_profile() {
|
||||
let err = from_str::<ConfigRequirementsToml>(
|
||||
@@ -1484,6 +1511,19 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_appshots_false_is_still_configured() -> Result<()> {
|
||||
let requirements: ConfigRequirementsToml = from_str(
|
||||
r#"
|
||||
allow_appshots = false
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_eq!(requirements.allow_appshots, Some(false));
|
||||
assert!(!requirements.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_computer_use_requirements() -> Result<()> {
|
||||
let requirements: ConfigRequirementsToml = from_str(
|
||||
@@ -1539,6 +1579,7 @@ mod tests {
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
|
||||
allow_managed_hooks_only: Some(true),
|
||||
allow_appshots: Some(false),
|
||||
computer_use: Some(computer_use.clone()),
|
||||
feature_requirements: Some(feature_requirements.clone()),
|
||||
hooks: None,
|
||||
@@ -1578,6 +1619,7 @@ mod tests {
|
||||
/*value*/ true,
|
||||
enforce_source.clone(),
|
||||
)),
|
||||
allow_appshots: Some(Sourced::new(/*value*/ false, enforce_source.clone(),)),
|
||||
computer_use: Some(Sourced::new(computer_use, enforce_source.clone())),
|
||||
feature_requirements: Some(Sourced::new(
|
||||
feature_requirements,
|
||||
@@ -1623,6 +1665,7 @@ mod tests {
|
||||
allowed_permissions: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
@@ -1673,6 +1716,7 @@ mod tests {
|
||||
allowed_permissions: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
|
||||
@@ -271,6 +271,7 @@ async fn get_status_returns_not_found_without_manager() {
|
||||
async fn on_event_updates_status_from_task_started() {
|
||||
let status = agent_status_from_event(&EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
|
||||
@@ -100,6 +100,7 @@ pub(crate) async fn run_compact_task(
|
||||
) -> CodexResult<()> {
|
||||
let start_event = EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
|
||||
model_context_window: turn_context.model_context_window(),
|
||||
collaboration_mode_kind: turn_context.collaboration_mode.mode,
|
||||
|
||||
@@ -62,6 +62,7 @@ pub(crate) async fn run_remote_compact_task(
|
||||
) -> CodexResult<()> {
|
||||
let start_event = EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
|
||||
model_context_window: turn_context.model_context_window(),
|
||||
collaboration_mode_kind: turn_context.collaboration_mode.mode,
|
||||
|
||||
@@ -78,6 +78,7 @@ pub(crate) async fn run_remote_compact_task(
|
||||
) -> CodexResult<()> {
|
||||
let start_event = EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
|
||||
model_context_window: turn_context.model_context_window(),
|
||||
collaboration_mode_kind: turn_context.collaboration_mode.mode,
|
||||
@@ -110,7 +111,7 @@ async fn run_remote_compact_task_inner(
|
||||
turn_context.as_ref(),
|
||||
trigger,
|
||||
reason,
|
||||
CompactionImplementation::Responses,
|
||||
CompactionImplementation::ResponsesCompactionV2,
|
||||
phase,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1101,6 +1101,7 @@ allowed_approval_policies = ["on-request"]
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
@@ -1161,6 +1162,7 @@ allowed_approval_policies = ["on-request"]
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
@@ -1370,6 +1372,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
|
||||
@@ -8098,6 +8098,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
@@ -8820,6 +8821,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
|
||||
@@ -2440,6 +2440,7 @@ impl Config {
|
||||
permission_profile: mut constrained_permission_profile,
|
||||
web_search_mode: mut constrained_web_search_mode,
|
||||
allow_managed_hooks_only: _,
|
||||
allow_appshots: _,
|
||||
computer_use: _,
|
||||
feature_requirements,
|
||||
managed_hooks: _,
|
||||
|
||||
@@ -468,16 +468,8 @@ pub async fn reload_user_config(sess: &Arc<Session>) {
|
||||
pub async fn compact(sess: &Arc<Session>, sub_id: String) {
|
||||
let turn_context = sess.new_default_turn_with_sub_id(sub_id).await;
|
||||
|
||||
sess.spawn_task(
|
||||
Arc::clone(&turn_context),
|
||||
vec![UserInput::Text {
|
||||
text: turn_context.compact_prompt().to_string(),
|
||||
// Compaction prompt is synthesized; no UI element ranges to preserve.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
CompactTask,
|
||||
)
|
||||
.await;
|
||||
sess.spawn_task(Arc::clone(&turn_context), Vec::new(), CompactTask)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn thread_rollback(sess: &Arc<Session>, sub_id: String, num_turns: u32) {
|
||||
|
||||
@@ -122,6 +122,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -189,6 +190,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: first_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -218,6 +220,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: rolled_back_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -289,6 +292,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: first_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -318,6 +322,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: incomplete_turn_id,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -381,6 +386,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: first_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -410,6 +416,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: second_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -438,6 +445,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: standalone_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -501,6 +509,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() {
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: first_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -530,6 +539,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() {
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: assistant_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -590,6 +600,7 @@ async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: only_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -643,6 +654,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: user_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -671,6 +683,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: standalone_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -716,6 +729,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: previous_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -743,6 +757,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: incomplete_turn_id,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -875,6 +890,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: current_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -939,6 +955,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: previous_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -1043,6 +1060,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: previous_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -1070,6 +1088,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: aborted_turn_id,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -1151,6 +1170,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: previous_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -1178,6 +1198,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: current_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -1268,6 +1289,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: previous_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -1295,6 +1317,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: incomplete_turn_id,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -1346,6 +1369,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_preserves_turn_
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: current_turn_id,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -1420,6 +1444,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: previous_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -1447,6 +1472,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: compacted_incomplete_turn_id,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -1470,6 +1496,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: replacing_turn_id,
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
|
||||
@@ -267,8 +267,24 @@ fn skill_message(text: &str) -> ResponseItem {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regular_turn_emits_turn_started_without_waiting_for_startup_prewarm() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
async fn regular_turn_emits_turn_started_with_trace_id_without_waiting_for_startup_prewarm() {
|
||||
let _trace_test_context = install_test_tracing("codex-core-tests");
|
||||
let request_parent = W3cTraceContext {
|
||||
traceparent: Some("00-00000000000000000000000000000011-0000000000000022-01".into()),
|
||||
tracestate: Some("vendor=value".into()),
|
||||
};
|
||||
let request_span = info_span!("app_server.request");
|
||||
assert!(set_parent_from_w3c_trace_context(
|
||||
&request_span,
|
||||
&request_parent
|
||||
));
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx()
|
||||
.instrument(request_span)
|
||||
.await;
|
||||
assert_eq!(
|
||||
tc.trace_id.as_deref(),
|
||||
Some("00000000000000000000000000000011")
|
||||
);
|
||||
let (_tx, startup_prewarm_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = startup_prewarm_rx.await;
|
||||
@@ -294,10 +310,11 @@ async fn regular_turn_emits_turn_started_without_waiting_for_startup_prewarm() {
|
||||
.await
|
||||
.expect("expected turn started event without waiting for startup prewarm")
|
||||
.expect("channel open");
|
||||
assert!(matches!(
|
||||
first.msg,
|
||||
EventMsg::TurnStarted(TurnStartedEvent { turn_id, .. }) if turn_id == tc.sub_id
|
||||
));
|
||||
let EventMsg::TurnStarted(turn_started) = first.msg else {
|
||||
panic!("expected turn started event");
|
||||
};
|
||||
assert_eq!(turn_started.turn_id, tc.sub_id);
|
||||
assert_eq!(turn_started.trace_id, tc.trace_id);
|
||||
|
||||
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
|
||||
}
|
||||
@@ -2378,6 +2395,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -2573,6 +2591,7 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: first_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -2600,6 +2619,7 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: rolled_back_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -2684,6 +2704,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: first_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -2709,6 +2730,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: compact_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -2728,6 +2750,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: rolled_back_turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -2783,6 +2806,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -2808,6 +2832,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-2".to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -2833,6 +2858,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-3".to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: Some(128_000),
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
@@ -5712,7 +5738,7 @@ async fn spawn_task_turn_span_inherits_dispatch_trace_context() {
|
||||
self: Arc<Self>,
|
||||
_session: Arc<SessionTaskContext>,
|
||||
_ctx: Arc<TurnContext>,
|
||||
_input: Vec<UserInput>,
|
||||
_input: Vec<TurnInput>,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Option<String> {
|
||||
let mut trace = self
|
||||
@@ -7788,7 +7814,7 @@ impl SessionTask for NeverEndingTask {
|
||||
self: Arc<Self>,
|
||||
_session: Arc<SessionTaskContext>,
|
||||
_ctx: Arc<TurnContext>,
|
||||
_input: Vec<UserInput>,
|
||||
_input: Vec<TurnInput>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Option<String> {
|
||||
if self.listen_to_cancellation_token {
|
||||
@@ -7817,7 +7843,7 @@ impl SessionTask for GuardianDeniedApprovalTask {
|
||||
self: Arc<Self>,
|
||||
session: Arc<SessionTaskContext>,
|
||||
ctx: Arc<TurnContext>,
|
||||
_input: Vec<UserInput>,
|
||||
_input: Vec<TurnInput>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Option<String> {
|
||||
let session = session.clone_session();
|
||||
|
||||
@@ -121,8 +121,8 @@ pub(crate) enum RunTurnResult {
|
||||
Terminal,
|
||||
}
|
||||
|
||||
/// Takes a user message as input and runs a loop where, at each sampling request, the model
|
||||
/// replies with either:
|
||||
/// Takes initial turn input and runs a loop where, at each sampling request,
|
||||
/// the model replies with either:
|
||||
///
|
||||
/// - requested function calls
|
||||
/// - an assistant message
|
||||
@@ -140,7 +140,7 @@ pub(crate) async fn run_turn(
|
||||
turn_context: Arc<TurnContext>,
|
||||
turn_extension_data: Arc<codex_extension_api::ExtensionData>,
|
||||
auto_compact_limiter: &mut AutoCompactTurnLimiter,
|
||||
input: Vec<UserInput>,
|
||||
input: Vec<TurnInput>,
|
||||
prewarmed_client_session: Option<ModelClientSession>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> RunTurnResult {
|
||||
@@ -190,26 +190,13 @@ pub(crate) async fn run_turn(
|
||||
if run_pending_session_start_hooks(&sess, &turn_context).await {
|
||||
return RunTurnResult::Continue(None);
|
||||
}
|
||||
if !input.is_empty() {
|
||||
let initial_turn_input = TurnInput::UserInput(input.clone());
|
||||
let user_prompt_submit_outcome =
|
||||
inspect_pending_input(&sess, &turn_context, &initial_turn_input).await;
|
||||
if user_prompt_submit_outcome.should_stop {
|
||||
record_additional_contexts(
|
||||
&sess,
|
||||
&turn_context,
|
||||
user_prompt_submit_outcome.additional_contexts,
|
||||
)
|
||||
.await;
|
||||
return RunTurnResult::Continue(None);
|
||||
}
|
||||
record_pending_input(
|
||||
&sess,
|
||||
&turn_context,
|
||||
initial_turn_input,
|
||||
user_prompt_submit_outcome.additional_contexts,
|
||||
)
|
||||
.await;
|
||||
let mut can_drain_pending_input = input.is_empty();
|
||||
let input_record_outcome = run_hooks_and_record_inputs(&sess, &turn_context, &input).await;
|
||||
if input_record_outcome.accepted_user_input {
|
||||
auto_compact_limiter.reset_after_user_input();
|
||||
}
|
||||
if input_record_outcome.should_stop {
|
||||
return RunTurnResult::Continue(None);
|
||||
}
|
||||
|
||||
sess.merge_connector_selection(explicitly_enabled_connectors.clone())
|
||||
@@ -250,9 +237,8 @@ pub(crate) async fn run_turn(
|
||||
// one instance across retries within this turn.
|
||||
// Pending input is drained into history before building the next model request.
|
||||
// However, we defer that drain until after sampling in two cases:
|
||||
// 1. At the start of a turn, so the fresh user prompt in `input` gets sampled first.
|
||||
// 1. At the start of a turn, so the fresh turn input in `input` gets sampled first.
|
||||
// 2. After auto-compact, when model/tool continuation needs to resume before any steer.
|
||||
let mut can_drain_pending_input = input.is_empty();
|
||||
|
||||
loop {
|
||||
// Note that pending_input would be something like a message the user
|
||||
@@ -264,31 +250,12 @@ pub(crate) async fn run_turn(
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut blocked_pending_input = false;
|
||||
let mut accepted_pending_input = false;
|
||||
for pending_input_item in pending_input {
|
||||
let is_user_input = matches!(&pending_input_item, TurnInput::UserInput(_));
|
||||
let hook_outcome =
|
||||
inspect_pending_input(&sess, &turn_context, &pending_input_item).await;
|
||||
if hook_outcome.should_stop {
|
||||
blocked_pending_input = true;
|
||||
record_additional_contexts(&sess, &turn_context, hook_outcome.additional_contexts)
|
||||
.await;
|
||||
} else {
|
||||
accepted_pending_input = true;
|
||||
record_pending_input(
|
||||
&sess,
|
||||
&turn_context,
|
||||
pending_input_item,
|
||||
hook_outcome.additional_contexts,
|
||||
)
|
||||
.await;
|
||||
if is_user_input {
|
||||
auto_compact_limiter.reset_after_user_input();
|
||||
}
|
||||
}
|
||||
let pending_input_record_outcome =
|
||||
run_hooks_and_record_inputs(&sess, &turn_context, &pending_input).await;
|
||||
if pending_input_record_outcome.accepted_user_input {
|
||||
auto_compact_limiter.reset_after_user_input();
|
||||
}
|
||||
if blocked_pending_input && !accepted_pending_input {
|
||||
if pending_input_record_outcome.should_stop {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -476,6 +443,45 @@ pub(crate) async fn run_turn(
|
||||
RunTurnResult::Continue(last_agent_message)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InputRecordOutcome {
|
||||
should_stop: bool,
|
||||
accepted_user_input: bool,
|
||||
}
|
||||
|
||||
async fn run_hooks_and_record_inputs(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
input: &[TurnInput],
|
||||
) -> InputRecordOutcome {
|
||||
let mut blocked_input = false;
|
||||
let mut accepted_input = false;
|
||||
let mut accepted_user_input = false;
|
||||
for input_item in input {
|
||||
let hook_outcome = inspect_pending_input(sess, turn_context, input_item).await;
|
||||
if hook_outcome.should_stop {
|
||||
blocked_input = true;
|
||||
record_additional_contexts(sess, turn_context, hook_outcome.additional_contexts).await;
|
||||
} else {
|
||||
accepted_input = true;
|
||||
if matches!(input_item, TurnInput::UserInput(_)) {
|
||||
accepted_user_input = true;
|
||||
}
|
||||
record_pending_input(
|
||||
sess,
|
||||
turn_context,
|
||||
input_item.clone(),
|
||||
hook_outcome.additional_contexts,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
InputRecordOutcome {
|
||||
should_stop: blocked_input && !accepted_input,
|
||||
accepted_user_input,
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "MCP tool listing borrows the read guard across cancellation-aware await"
|
||||
@@ -483,9 +489,18 @@ pub(crate) async fn run_turn(
|
||||
async fn build_skills_and_plugins(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &TurnContext,
|
||||
input: &[UserInput],
|
||||
input: &[TurnInput],
|
||||
cancellation_token: &CancellationToken,
|
||||
) -> Option<(Vec<ResponseItem>, HashSet<String>)> {
|
||||
let user_input = input
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
TurnInput::UserInput(content) => Some(content.as_slice()),
|
||||
TurnInput::ResponseInputItem(_) => None,
|
||||
})
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let tracking = build_track_events_context(
|
||||
turn_context.model_info.slug.clone(),
|
||||
sess.conversation_id.to_string(),
|
||||
@@ -499,7 +514,7 @@ async fn build_skills_and_plugins(
|
||||
// Structured plugin:// mentions are resolved from the current session's
|
||||
// enabled plugins, then converted into turn-scoped guidance below.
|
||||
let mentioned_plugins =
|
||||
collect_explicit_plugin_mentions(input, loaded_plugins.capability_summaries());
|
||||
collect_explicit_plugin_mentions(&user_input, loaded_plugins.capability_summaries());
|
||||
let mcp_tools = if turn_context.apps_enabled() || !mentioned_plugins.is_empty() {
|
||||
// Plugin mentions need raw MCP/app inventory even when app tools
|
||||
// are normally hidden so we can describe the plugin's currently
|
||||
@@ -537,7 +552,7 @@ async fn build_skills_and_plugins(
|
||||
let skill_name_counts_lower =
|
||||
build_skill_name_counts(&skills_outcome.skills, &skills_outcome.disabled_paths).1;
|
||||
let mentioned_skills = collect_explicit_skill_mentions(
|
||||
input,
|
||||
&user_input,
|
||||
&skills_outcome.skills,
|
||||
&skills_outcome.disabled_paths,
|
||||
&connector_slug_counts,
|
||||
@@ -579,7 +594,7 @@ async fn build_skills_and_plugins(
|
||||
);
|
||||
let plugin_items =
|
||||
build_plugin_injections(&mentioned_plugins, &mcp_tools, &available_connectors);
|
||||
let mut explicitly_enabled_connectors = collect_explicit_app_ids(input);
|
||||
let mut explicitly_enabled_connectors = collect_explicit_app_ids(&user_input);
|
||||
explicitly_enabled_connectors.extend(skill_connector_ids);
|
||||
let connector_names_by_id = available_connectors
|
||||
.iter()
|
||||
@@ -615,7 +630,7 @@ async fn build_skills_and_plugins(
|
||||
async fn track_turn_resolved_config_analytics(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
input: &[UserInput],
|
||||
input: &[TurnInput],
|
||||
) {
|
||||
let thread_config = {
|
||||
let state = sess.state.lock().await;
|
||||
@@ -632,6 +647,11 @@ async fn track_turn_resolved_config_analytics(
|
||||
thread_id: sess.conversation_id.to_string(),
|
||||
num_input_images: input
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
TurnInput::UserInput(content) => Some(content.as_slice()),
|
||||
TurnInput::ResponseInputItem(_) => None,
|
||||
})
|
||||
.flatten()
|
||||
.filter(|item| {
|
||||
matches!(item, UserInput::Image { .. } | UserInput::LocalImage { .. })
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use super::SessionTask;
|
||||
use super::SessionTaskContext;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::state::TaskKind;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
@@ -23,7 +24,7 @@ impl SessionTask for CompactTask {
|
||||
self: Arc<Self>,
|
||||
session: Arc<SessionTaskContext>,
|
||||
ctx: Arc<TurnContext>,
|
||||
input: Vec<UserInput>,
|
||||
_input: Vec<TurnInput>,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Option<String> {
|
||||
let session = session.clone_session();
|
||||
@@ -47,6 +48,11 @@ impl SessionTask for CompactTask {
|
||||
/*inc*/ 1,
|
||||
&[("type", "local")],
|
||||
);
|
||||
let input = vec![UserInput::Text {
|
||||
text: ctx.compact_prompt().to_string(),
|
||||
// Compaction prompt is synthesized; no UI element ranges to preserve.
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
crate::compact::run_compact_task(session.clone(), ctx, input).await
|
||||
};
|
||||
None
|
||||
|
||||
@@ -214,7 +214,7 @@ pub(crate) trait SessionTask: Send + Sync + 'static {
|
||||
self: Arc<Self>,
|
||||
session: Arc<SessionTaskContext>,
|
||||
ctx: Arc<TurnContext>,
|
||||
input: Vec<UserInput>,
|
||||
input: Vec<TurnInput>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> impl std::future::Future<Output = Option<String>> + Send;
|
||||
|
||||
@@ -245,7 +245,7 @@ pub(crate) trait AnySessionTask: Send + Sync + 'static {
|
||||
self: Arc<Self>,
|
||||
session: Arc<SessionTaskContext>,
|
||||
ctx: Arc<TurnContext>,
|
||||
input: Vec<UserInput>,
|
||||
input: Vec<TurnInput>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> BoxFuture<'static, Option<String>>;
|
||||
|
||||
@@ -276,7 +276,7 @@ where
|
||||
self: Arc<Self>,
|
||||
session: Arc<SessionTaskContext>,
|
||||
ctx: Arc<TurnContext>,
|
||||
input: Vec<UserInput>,
|
||||
input: Vec<TurnInput>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> BoxFuture<'static, Option<String>> {
|
||||
Box::pin(SessionTask::run(
|
||||
@@ -380,6 +380,11 @@ impl Session {
|
||||
));
|
||||
let ctx = Arc::clone(&turn_context);
|
||||
let task_for_run = Arc::clone(&task);
|
||||
let task_input = if input.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![TurnInput::UserInput(input)]
|
||||
};
|
||||
let task_cancellation_token = cancellation_token.child_token();
|
||||
// Task-owned turn spans keep a core-owned span open for the
|
||||
// full task lifecycle after the submission dispatch span ends.
|
||||
@@ -405,7 +410,7 @@ impl Session {
|
||||
.run(
|
||||
Arc::clone(&session_ctx),
|
||||
ctx,
|
||||
input,
|
||||
task_input,
|
||||
task_cancellation_token.child_token(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -13,7 +13,6 @@ use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use tracing::Instrument;
|
||||
use tracing::trace_span;
|
||||
|
||||
@@ -46,7 +45,7 @@ impl SessionTask for RegularTask {
|
||||
self: Arc<Self>,
|
||||
session: Arc<SessionTaskContext>,
|
||||
ctx: Arc<TurnContext>,
|
||||
input: Vec<UserInput>,
|
||||
input: Vec<TurnInput>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Option<String> {
|
||||
let sess = session.clone_session();
|
||||
@@ -56,6 +55,7 @@ impl SessionTask for RegularTask {
|
||||
// not wait on startup prewarm resolution.
|
||||
let event = EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: ctx.sub_id.clone(),
|
||||
trace_id: ctx.trace_id.clone(),
|
||||
started_at: ctx.turn_timing_state.started_at_unix_secs().await,
|
||||
model_context_window: ctx.model_context_window(),
|
||||
collaboration_mode_kind: ctx.collaboration_mode.mode,
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::codex_delegate::run_codex_thread_one_shot;
|
||||
use crate::config::Constrained;
|
||||
use crate::review_format::format_review_findings_block;
|
||||
use crate::review_format::render_review_output_text;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::state::TaskKind;
|
||||
@@ -59,7 +60,7 @@ impl SessionTask for ReviewTask {
|
||||
self: Arc<Self>,
|
||||
session: Arc<SessionTaskContext>,
|
||||
ctx: Arc<TurnContext>,
|
||||
input: Vec<UserInput>,
|
||||
input: Vec<TurnInput>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Option<String> {
|
||||
session.session.services.session_telemetry.counter(
|
||||
@@ -68,11 +69,19 @@ impl SessionTask for ReviewTask {
|
||||
&[],
|
||||
);
|
||||
|
||||
let mut user_input = Vec::new();
|
||||
for item in input {
|
||||
match item {
|
||||
TurnInput::UserInput(mut content) => user_input.append(&mut content),
|
||||
TurnInput::ResponseInputItem(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Start sub-codex conversation and get the receiver for events.
|
||||
let output = match start_review_conversation(
|
||||
session.clone(),
|
||||
ctx.clone(),
|
||||
input,
|
||||
user_input,
|
||||
cancellation_token.clone(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -7,7 +7,6 @@ use codex_network_proxy::PROXY_ACTIVE_ENV_KEY;
|
||||
use codex_network_proxy::PROXY_ENV_KEYS;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
@@ -17,6 +16,7 @@ use crate::exec::StdoutStream;
|
||||
use crate::exec::execute_exec_request;
|
||||
use crate::exec_env::create_env;
|
||||
use crate::sandboxing::ExecRequest;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::state::TaskKind;
|
||||
use crate::tools::format_exec_output_str;
|
||||
@@ -77,7 +77,7 @@ impl SessionTask for UserShellCommandTask {
|
||||
self: Arc<Self>,
|
||||
session: Arc<SessionTaskContext>,
|
||||
turn_context: Arc<TurnContext>,
|
||||
_input: Vec<UserInput>,
|
||||
_input: Vec<TurnInput>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Option<String> {
|
||||
execute_user_shell_command(
|
||||
@@ -114,6 +114,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
// freshly reinjected context before the summary/replacement history is applied.
|
||||
let event = EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
|
||||
model_context_window: turn_context.model_context_window(),
|
||||
collaboration_mode_kind: turn_context.collaboration_mode.mode,
|
||||
|
||||
@@ -187,6 +187,7 @@ fn out_of_range_truncation_drops_pre_user_active_turn_prefix() {
|
||||
RolloutItem::ResponseItem(assistant_msg("a1")),
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-2".to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
@@ -1307,6 +1308,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
|
||||
InitialHistory::Forked(vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-explicit".to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
|
||||
@@ -758,24 +758,34 @@ async fn steered_user_input_resets_auto_compact_limit() {
|
||||
];
|
||||
|
||||
let (server, _completions) = start_streaming_sse_server(vec![
|
||||
gated_commentary_sampling_chunks(1, "first answer", gate_first_completed_rx, 500),
|
||||
compact_summary_chunks(1, 500),
|
||||
gated_commentary_sampling_chunks(
|
||||
2,
|
||||
/*idx*/ 1,
|
||||
"first answer",
|
||||
gate_first_completed_rx,
|
||||
/*total_tokens*/ 500,
|
||||
),
|
||||
compact_summary_chunks(/*idx*/ 1, /*total_tokens*/ 500),
|
||||
gated_commentary_sampling_chunks(
|
||||
/*idx*/ 2,
|
||||
"processed second prompt",
|
||||
gate_second_completed_rx,
|
||||
500,
|
||||
/*total_tokens*/ 500,
|
||||
),
|
||||
compact_summary_chunks(2, 500),
|
||||
gated_commentary_sampling_chunks(3, "processed third prompt", gate_third_completed_rx, 500),
|
||||
compact_summary_chunks(3, 500),
|
||||
compact_summary_chunks(/*idx*/ 2, /*total_tokens*/ 500),
|
||||
gated_commentary_sampling_chunks(
|
||||
4,
|
||||
/*idx*/ 3,
|
||||
"processed third prompt",
|
||||
gate_third_completed_rx,
|
||||
/*total_tokens*/ 500,
|
||||
),
|
||||
compact_summary_chunks(/*idx*/ 3, /*total_tokens*/ 500),
|
||||
gated_commentary_sampling_chunks(
|
||||
/*idx*/ 4,
|
||||
"processed fourth prompt",
|
||||
gate_fourth_completed_rx,
|
||||
500,
|
||||
/*total_tokens*/ 500,
|
||||
),
|
||||
compact_summary_chunks(4, 500),
|
||||
compact_summary_chunks(/*idx*/ 4, /*total_tokens*/ 500),
|
||||
final_chunks,
|
||||
])
|
||||
.await;
|
||||
@@ -849,7 +859,7 @@ async fn steered_user_input_is_rejected_when_auto_compact_guard_fires() {
|
||||
}
|
||||
responses.extend([
|
||||
fourth_sampling_chunks,
|
||||
compact_summary_chunks(4, token_count_used_after_compaction),
|
||||
compact_summary_chunks(/*idx*/ 4, token_count_used_after_compaction),
|
||||
]);
|
||||
let (server, _completions) = start_streaming_sse_server(responses).await;
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ fn resume_history(
|
||||
history: vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
|
||||
@@ -67,6 +67,7 @@ fn rollout_items_from_messages(messages: &[ConversationMessage]) -> Vec<RolloutI
|
||||
items.push(RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
TurnStartedEvent {
|
||||
turn_id: turn_id.clone(),
|
||||
trace_id: None,
|
||||
started_at: message.timestamp,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
|
||||
@@ -1850,6 +1850,10 @@ pub struct TurnCompleteEvent {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct TurnStartedEvent {
|
||||
pub turn_id: String,
|
||||
// Persist for rollout consumers that correlate turns with telemetry traces.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub trace_id: Option<String>,
|
||||
/// Unix timestamp (in seconds) when the turn started.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "number | null", optional)]
|
||||
|
||||
@@ -156,6 +156,17 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(allow_appshots) = requirements_toml.allow_appshots {
|
||||
requirement_lines.push(requirement_line(
|
||||
"allow_appshots",
|
||||
allow_appshots.to_string(),
|
||||
requirements
|
||||
.allow_appshots
|
||||
.as_ref()
|
||||
.map(|sourced| &sourced.source),
|
||||
));
|
||||
}
|
||||
|
||||
if requirements_toml.guardian_policy_config.is_some() {
|
||||
requirement_lines.push(requirement_line(
|
||||
"guardian_policy_config",
|
||||
@@ -662,6 +673,10 @@ mod tests {
|
||||
/*value*/ true,
|
||||
RequirementSource::CloudRequirements,
|
||||
)),
|
||||
allow_appshots: Some(Sourced::new(
|
||||
/*value*/ false,
|
||||
RequirementSource::CloudRequirements,
|
||||
)),
|
||||
feature_requirements: Some(Sourced::new(
|
||||
FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
|
||||
@@ -701,6 +716,7 @@ mod tests {
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
|
||||
allow_managed_hooks_only: Some(true),
|
||||
allow_appshots: Some(false),
|
||||
computer_use: None,
|
||||
guardian_policy_config: Some("Use the managed guardian policy.".to_string()),
|
||||
feature_requirements: Some(FeatureRequirementsToml {
|
||||
@@ -763,6 +779,7 @@ mod tests {
|
||||
)
|
||||
);
|
||||
assert!(rendered.contains("allow_managed_hooks_only: true (source: cloud requirements)"));
|
||||
assert!(rendered.contains("allow_appshots: false (source: cloud requirements)"));
|
||||
assert!(
|
||||
rendered.contains("guardian_policy_config: configured (source: cloud requirements)")
|
||||
);
|
||||
@@ -917,6 +934,7 @@ approval_policy = "never"
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(Vec::new()),
|
||||
allow_managed_hooks_only: None,
|
||||
allow_appshots: None,
|
||||
computer_use: None,
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: None,
|
||||
|
||||
+71
-1
@@ -166,6 +166,8 @@ mod tests {
|
||||
}
|
||||
|
||||
pub fn set_modes() -> Result<()> {
|
||||
ensure_virtual_terminal_processing()?;
|
||||
|
||||
execute!(stdout(), EnableBracketedPaste)?;
|
||||
|
||||
enable_raw_mode()?;
|
||||
@@ -239,12 +241,16 @@ fn restore_common(
|
||||
raw_mode_restore: RawModeRestore,
|
||||
keyboard_restore: KeyboardRestore,
|
||||
) -> Result<()> {
|
||||
let mut first_error = ensure_virtual_terminal_processing().err();
|
||||
|
||||
match keyboard_restore {
|
||||
KeyboardRestore::PopStack => keyboard_modes::restore_keyboard_enhancement_stack(),
|
||||
KeyboardRestore::ResetAfterExit => keyboard_modes::reset_keyboard_reporting_after_exit(),
|
||||
}
|
||||
|
||||
let mut first_error = execute!(stdout(), DisableBracketedPaste).err();
|
||||
if let Err(err) = execute!(stdout(), DisableBracketedPaste) {
|
||||
first_error.get_or_insert(err);
|
||||
}
|
||||
let _ = execute!(stdout(), DisableFocusChange);
|
||||
if matches!(raw_mode_restore, RawModeRestore::Disable)
|
||||
&& let Err(err) = disable_raw_mode()
|
||||
@@ -797,6 +803,8 @@ impl Tui {
|
||||
// the synchronized update, to avoid racing with the event reader.
|
||||
let mut pending_viewport_area = self.pending_viewport_area()?;
|
||||
|
||||
ensure_virtual_terminal_processing()?;
|
||||
|
||||
stdout().sync_update(|_| {
|
||||
#[cfg(unix)]
|
||||
if let Some(prepared) = prepared_resume.take() {
|
||||
@@ -854,6 +862,10 @@ impl Tui {
|
||||
&mut self,
|
||||
request: Option<crate::pets::AmbientPetDraw>,
|
||||
) -> std::result::Result<(), crate::pets::PetImageRenderError> {
|
||||
if let Err(err) = ensure_virtual_terminal_processing() {
|
||||
return Err(crate::pets::PetImageRenderError::Terminal(err));
|
||||
}
|
||||
|
||||
let terminal = &mut self.terminal;
|
||||
let state = &mut self.ambient_pet_image_state;
|
||||
stdout().sync_update(|_| {
|
||||
@@ -869,6 +881,10 @@ impl Tui {
|
||||
&mut self,
|
||||
request: Option<crate::pets::AmbientPetDraw>,
|
||||
) -> std::result::Result<(), crate::pets::PetImageRenderError> {
|
||||
if let Err(err) = ensure_virtual_terminal_processing() {
|
||||
return Err(crate::pets::PetImageRenderError::Terminal(err));
|
||||
}
|
||||
|
||||
let terminal = &mut self.terminal;
|
||||
let state = &mut self.pet_picker_preview_image_state;
|
||||
stdout().sync_update(|_| {
|
||||
@@ -887,6 +903,10 @@ impl Tui {
|
||||
pub fn clear_ambient_pet_image(
|
||||
&mut self,
|
||||
) -> std::result::Result<(), crate::pets::PetImageRenderError> {
|
||||
if let Err(err) = ensure_virtual_terminal_processing() {
|
||||
return Err(crate::pets::PetImageRenderError::Terminal(err));
|
||||
}
|
||||
|
||||
crate::pets::render_ambient_pet_image(
|
||||
self.terminal.backend_mut(),
|
||||
&mut self.ambient_pet_image_state,
|
||||
@@ -911,6 +931,8 @@ impl Tui {
|
||||
.suspend_context
|
||||
.prepare_resume_action(&mut self.terminal, &mut self.alt_saved_viewport);
|
||||
|
||||
ensure_virtual_terminal_processing()?;
|
||||
|
||||
stdout().sync_update(|_| {
|
||||
#[cfg(unix)]
|
||||
if let Some(prepared) = prepared_resume.take() {
|
||||
@@ -968,3 +990,51 @@ impl Tui {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn ensure_virtual_terminal_processing() -> Result<()> {
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows_sys::Win32::System::Console::ENABLE_PROCESSED_OUTPUT;
|
||||
use windows_sys::Win32::System::Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
||||
use windows_sys::Win32::System::Console::GetConsoleMode;
|
||||
use windows_sys::Win32::System::Console::GetStdHandle;
|
||||
use windows_sys::Win32::System::Console::STD_ERROR_HANDLE;
|
||||
use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE;
|
||||
use windows_sys::Win32::System::Console::SetConsoleMode;
|
||||
|
||||
fn enable_for_handle(handle: HANDLE) -> Result<()> {
|
||||
if handle == INVALID_HANDLE_VALUE || handle == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut mode = 0;
|
||||
if unsafe { GetConsoleMode(handle, &mut mode) } == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let requested = ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
||||
if mode & requested == requested {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if unsafe { SetConsoleMode(handle, mode | requested) } == 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let stdout_handle = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) };
|
||||
enable_for_handle(stdout_handle)?;
|
||||
|
||||
let stderr_handle = unsafe { GetStdHandle(STD_ERROR_HANDLE) };
|
||||
enable_for_handle(stderr_handle)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn ensure_virtual_terminal_processing() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -47,7 +47,12 @@ impl UpdateAction {
|
||||
),
|
||||
UpdateAction::StandaloneWindows => (
|
||||
"powershell",
|
||||
&["-c", "irm https://chatgpt.com/codex/install.ps1|iex"],
|
||||
&[
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-c",
|
||||
"irm https://chatgpt.com/codex/install.ps1 | iex",
|
||||
],
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -142,7 +147,12 @@ mod tests {
|
||||
UpdateAction::StandaloneWindows.command_args(),
|
||||
(
|
||||
"powershell",
|
||||
&["-c", "irm https://chatgpt.com/codex/install.ps1|iex"][..],
|
||||
&[
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-c",
|
||||
"irm https://chatgpt.com/codex/install.ps1 | iex"
|
||||
][..],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tempfile = "3"
|
||||
tokio = { workspace = true, features = ["sync", "rt"] }
|
||||
tracing-appender = { workspace = true }
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_NetworkManagement_WindowsFirewall",
|
||||
|
||||
@@ -6,7 +6,6 @@ use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
use codex_otel::StatsigMetricsSettings;
|
||||
use codex_windows_sandbox::LOG_FILE_NAME;
|
||||
use codex_windows_sandbox::SETUP_VERSION;
|
||||
use codex_windows_sandbox::SetupErrorCode;
|
||||
use codex_windows_sandbox::SetupErrorReport;
|
||||
@@ -21,6 +20,7 @@ use codex_windows_sandbox::hide_newly_created_users;
|
||||
use codex_windows_sandbox::install_wfp_filters;
|
||||
use codex_windows_sandbox::is_command_cwd_root;
|
||||
use codex_windows_sandbox::log_note;
|
||||
use codex_windows_sandbox::log_writer;
|
||||
use codex_windows_sandbox::path_mask_allows;
|
||||
use codex_windows_sandbox::sandbox_bin_dir;
|
||||
use codex_windows_sandbox::sandbox_dir;
|
||||
@@ -36,7 +36,6 @@ use serde::Serialize;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::ffi::c_void;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::path::Path;
|
||||
@@ -109,7 +108,7 @@ enum SetupMode {
|
||||
ReadAclsOnly,
|
||||
}
|
||||
|
||||
fn log_line(log: &mut File, msg: &str) -> Result<()> {
|
||||
fn log_line(log: &mut dyn Write, msg: &str) -> Result<()> {
|
||||
let ts = chrono::Utc::now().to_rfc3339();
|
||||
writeln!(log, "[{ts}] {msg}").map_err(|err| {
|
||||
anyhow::Error::new(SetupFailure::new(
|
||||
@@ -156,7 +155,7 @@ fn workspace_write_cap_sids_for_path(
|
||||
Ok(sid_strs)
|
||||
}
|
||||
|
||||
fn spawn_read_acl_helper(payload: &Payload, _log: &mut File) -> Result<()> {
|
||||
fn spawn_read_acl_helper(payload: &Payload, _log: &mut dyn Write) -> Result<()> {
|
||||
let mut read_payload = payload.clone();
|
||||
read_payload.mode = SetupMode::ReadAclsOnly;
|
||||
read_payload.refresh_only = true;
|
||||
@@ -182,7 +181,7 @@ struct ReadAclSubjects<'a> {
|
||||
fn apply_read_acls(
|
||||
read_roots: &[PathBuf],
|
||||
subjects: &ReadAclSubjects<'_>,
|
||||
log: &mut File,
|
||||
log: &mut dyn Write,
|
||||
refresh_errors: &mut Vec<String>,
|
||||
access_mask: u32,
|
||||
access_label: &str,
|
||||
@@ -259,7 +258,7 @@ fn read_mask_allows_or_log(
|
||||
read_mask: u32,
|
||||
access_label: &str,
|
||||
refresh_errors: &mut Vec<String>,
|
||||
log: &mut File,
|
||||
log: &mut dyn Write,
|
||||
) -> Result<bool> {
|
||||
match path_mask_allows(root, psids, read_mask, /*require_all_bits*/ true) {
|
||||
Ok(has) => Ok(has),
|
||||
@@ -294,7 +293,7 @@ fn lock_sandbox_dir(
|
||||
sandbox_group_access_mode: i32,
|
||||
sandbox_group_mask: u32,
|
||||
real_user_mask: u32,
|
||||
_log: &mut File,
|
||||
_log: &mut dyn Write,
|
||||
) -> Result<()> {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
let system_sid = resolve_sid("SYSTEM")?;
|
||||
@@ -391,8 +390,7 @@ pub fn main() -> Result<()> {
|
||||
if let Ok(codex_home) = std::env::var("CODEX_HOME") {
|
||||
let sbx_dir = sandbox_dir(Path::new(&codex_home));
|
||||
let _ = std::fs::create_dir_all(&sbx_dir);
|
||||
let log_path = sbx_dir.join(LOG_FILE_NAME);
|
||||
if let Ok(mut f) = File::options().create(true).append(true).open(&log_path) {
|
||||
if let Some(mut f) = log_writer(&sbx_dir) {
|
||||
let _ = writeln!(
|
||||
f,
|
||||
"[{}] top-level error: {}",
|
||||
@@ -442,17 +440,12 @@ fn real_main() -> Result<()> {
|
||||
format!("failed to create sandbox dir {}: {err}", sbx_dir.display()),
|
||||
))
|
||||
})?;
|
||||
let log_path = sbx_dir.join(LOG_FILE_NAME);
|
||||
let mut log = File::options()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_path)
|
||||
.map_err(|err| {
|
||||
anyhow::Error::new(SetupFailure::new(
|
||||
SetupErrorCode::HelperLogFailed,
|
||||
format!("open log {} failed: {err}", log_path.display()),
|
||||
))
|
||||
})?;
|
||||
let mut log = log_writer(&sbx_dir).ok_or_else(|| {
|
||||
anyhow::Error::new(SetupFailure::new(
|
||||
SetupErrorCode::HelperLogFailed,
|
||||
format!("open log in {} failed", sbx_dir.display()),
|
||||
))
|
||||
})?;
|
||||
let result = run_setup(&payload, &mut log, &sbx_dir);
|
||||
if let Err(err) = &result {
|
||||
let _ = log_line(&mut log, &format!("setup error: {err:?}"));
|
||||
@@ -480,14 +473,14 @@ fn real_main() -> Result<()> {
|
||||
result
|
||||
}
|
||||
|
||||
fn run_setup(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<()> {
|
||||
fn run_setup(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Result<()> {
|
||||
match payload.mode {
|
||||
SetupMode::ReadAclsOnly => run_read_acl_only(payload, log),
|
||||
SetupMode::Full => run_setup_full(payload, log, sbx_dir),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_read_acl_only(payload: &Payload, log: &mut File) -> Result<()> {
|
||||
fn run_read_acl_only(payload: &Payload, log: &mut dyn Write) -> Result<()> {
|
||||
let _read_acl_guard = match acquire_read_acl_mutex()? {
|
||||
Some(guard) => guard,
|
||||
None => {
|
||||
@@ -550,7 +543,7 @@ fn run_read_acl_only(payload: &Payload, log: &mut File) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<()> {
|
||||
fn run_setup_full(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Result<()> {
|
||||
let refresh_only = payload.refresh_only;
|
||||
if !refresh_only {
|
||||
let provision_result = provision_sandbox_users(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
use windows::Win32::Foundation::S_OK;
|
||||
@@ -57,7 +56,7 @@ pub fn ensure_offline_proxy_allowlist(
|
||||
offline_sid: &str,
|
||||
proxy_ports: &[u16],
|
||||
allow_local_binding: bool,
|
||||
log: &mut File,
|
||||
log: &mut dyn Write,
|
||||
) -> Result<()> {
|
||||
let local_user_spec = format!("O:LSD:(A;;CC;;;{offline_sid})");
|
||||
|
||||
@@ -154,7 +153,7 @@ pub fn ensure_offline_proxy_allowlist(
|
||||
result
|
||||
}
|
||||
|
||||
pub fn ensure_offline_outbound_block(offline_sid: &str, log: &mut File) -> Result<()> {
|
||||
pub fn ensure_offline_outbound_block(offline_sid: &str, log: &mut dyn Write) -> Result<()> {
|
||||
let local_user_spec = format!("O:LSD:(A;;CC;;;{offline_sid})");
|
||||
|
||||
let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
|
||||
@@ -206,7 +205,11 @@ pub fn ensure_offline_outbound_block(offline_sid: &str, log: &mut File) -> Resul
|
||||
result
|
||||
}
|
||||
|
||||
fn remove_rule_if_present(rules: &INetFwRules, internal_name: &str, log: &mut File) -> Result<()> {
|
||||
fn remove_rule_if_present(
|
||||
rules: &INetFwRules,
|
||||
internal_name: &str,
|
||||
log: &mut dyn Write,
|
||||
) -> Result<()> {
|
||||
let name = BSTR::from(internal_name);
|
||||
if unsafe { rules.Item(&name) }.is_ok() {
|
||||
unsafe { rules.Remove(&name) }.map_err(|err| {
|
||||
@@ -266,7 +269,11 @@ fn validate_local_policy_modify_result(
|
||||
)))
|
||||
}
|
||||
|
||||
fn ensure_block_rule(rules: &INetFwRules, spec: &BlockRuleSpec<'_>, log: &mut File) -> Result<()> {
|
||||
fn ensure_block_rule(
|
||||
rules: &INetFwRules,
|
||||
spec: &BlockRuleSpec<'_>,
|
||||
log: &mut dyn Write,
|
||||
) -> Result<()> {
|
||||
let name = BSTR::from(spec.internal_name);
|
||||
let rule: INetFwRule3 = match unsafe { rules.Item(&name) } {
|
||||
Ok(existing) => existing.cast().map_err(|err| {
|
||||
@@ -453,7 +460,7 @@ fn port_range_string(start: u32, end: u32) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn log_line(log: &mut File, msg: &str) -> Result<()> {
|
||||
fn log_line(log: &mut dyn Write, msg: &str) -> Result<()> {
|
||||
let ts = chrono::Utc::now().to_rfc3339();
|
||||
writeln!(log, "[{ts}] {msg}")?;
|
||||
Ok(())
|
||||
|
||||
@@ -7,7 +7,7 @@ use rand::rngs::SmallRng;
|
||||
use serde::Serialize;
|
||||
use std::ffi::OsStr;
|
||||
use std::ffi::c_void;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER;
|
||||
@@ -49,7 +49,7 @@ const SID_AUTHENTICATED_USERS: &str = "S-1-5-11";
|
||||
const SID_EVERYONE: &str = "S-1-1-0";
|
||||
const SID_SYSTEM: &str = "S-1-5-18";
|
||||
|
||||
pub fn ensure_sandbox_users_group(log: &mut File) -> Result<()> {
|
||||
pub fn ensure_sandbox_users_group(log: &mut dyn Write) -> Result<()> {
|
||||
ensure_local_group(SANDBOX_USERS_GROUP, SANDBOX_USERS_GROUP_COMMENT, log)
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ pub fn provision_sandbox_users(
|
||||
online_username: &str,
|
||||
proxy_ports: &[u16],
|
||||
allow_local_binding: bool,
|
||||
log: &mut File,
|
||||
log: &mut dyn Write,
|
||||
) -> Result<()> {
|
||||
ensure_sandbox_users_group(log)?;
|
||||
super::log_line(
|
||||
@@ -86,13 +86,13 @@ pub fn provision_sandbox_users(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_sandbox_user(username: &str, password: &str, log: &mut File) -> Result<()> {
|
||||
pub fn ensure_sandbox_user(username: &str, password: &str, log: &mut dyn Write) -> Result<()> {
|
||||
ensure_local_user(username, password, log)?;
|
||||
ensure_local_group_member(SANDBOX_USERS_GROUP, username)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_local_user(name: &str, password: &str, log: &mut File) -> Result<()> {
|
||||
pub fn ensure_local_user(name: &str, password: &str, log: &mut dyn Write) -> Result<()> {
|
||||
let name_w = to_wide(OsStr::new(name));
|
||||
let pwd_w = to_wide(OsStr::new(password));
|
||||
unsafe {
|
||||
@@ -156,7 +156,7 @@ pub fn ensure_local_user(name: &str, password: &str, log: &mut File) -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_local_group(name: &str, comment: &str, log: &mut File) -> Result<()> {
|
||||
pub fn ensure_local_group(name: &str, comment: &str, log: &mut dyn Write) -> Result<()> {
|
||||
const ERROR_ALIAS_EXISTS: u32 = 1379;
|
||||
const NERR_GROUP_EXISTS: u32 = 2223;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::ffi::c_void;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -13,7 +13,7 @@ use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
|
||||
pub(super) fn ensure_codex_app_runtime_bin_readable(
|
||||
sandbox_group_psid: *mut c_void,
|
||||
refresh_errors: &mut Vec<String>,
|
||||
log: &mut File,
|
||||
log: &mut dyn Write,
|
||||
) -> Result<()> {
|
||||
let local_app_data = std::env::var_os("LOCALAPPDATA")
|
||||
.map(PathBuf::from)
|
||||
|
||||
@@ -183,10 +183,14 @@ pub use ipc_framed::read_frame;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use ipc_framed::write_frame;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use logging::LOG_FILE_NAME;
|
||||
pub use logging::current_log_file_path;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use logging::log_file_path_for_utc_date;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use logging::log_note;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use logging::log_writer;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use path_normalization::canonicalize_path;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use policy::SandboxPolicy;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use codex_utils_string::take_bytes_at_char_boundary;
|
||||
use tracing_appender::rolling::RollingFileAppender;
|
||||
use tracing_appender::rolling::Rotation;
|
||||
|
||||
const LOG_COMMAND_PREVIEW_LIMIT: usize = 200;
|
||||
pub const LOG_FILE_NAME: &str = "sandbox.log";
|
||||
pub const LOG_FILE_PREFIX: &str = "sandbox";
|
||||
pub const LOG_FILE_SUFFIX: &str = "log";
|
||||
pub const MAX_LOG_FILES: usize = 90;
|
||||
|
||||
fn exe_label() -> &'static str {
|
||||
static LABEL: OnceLock<String> = OnceLock::new();
|
||||
@@ -28,18 +31,35 @@ fn preview(command: &[String]) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn log_file_path(base_dir: &Path) -> Option<PathBuf> {
|
||||
if base_dir.is_dir() {
|
||||
Some(base_dir.join(LOG_FILE_NAME))
|
||||
} else {
|
||||
None
|
||||
pub fn log_file_path_for_utc_date(base_dir: &Path, date: chrono::NaiveDate) -> PathBuf {
|
||||
base_dir.join(format!(
|
||||
"{LOG_FILE_PREFIX}.{}.{}",
|
||||
date.format("%Y-%m-%d"),
|
||||
LOG_FILE_SUFFIX
|
||||
))
|
||||
}
|
||||
|
||||
pub fn current_log_file_path(base_dir: &Path) -> PathBuf {
|
||||
log_file_path_for_utc_date(base_dir, chrono::Utc::now().date_naive())
|
||||
}
|
||||
|
||||
pub fn log_writer(base_dir: &Path) -> Option<RollingFileAppender> {
|
||||
if !base_dir.is_dir() {
|
||||
return None;
|
||||
}
|
||||
|
||||
RollingFileAppender::builder()
|
||||
.rotation(Rotation::DAILY)
|
||||
.filename_prefix(LOG_FILE_PREFIX)
|
||||
.filename_suffix(LOG_FILE_SUFFIX)
|
||||
.max_log_files(MAX_LOG_FILES)
|
||||
.build(base_dir)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn append_line(line: &str, base_dir: Option<&Path>) {
|
||||
if let Some(dir) = base_dir
|
||||
&& let Some(path) = log_file_path(dir)
|
||||
&& let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path)
|
||||
&& let Some(mut f) = log_writer(dir)
|
||||
{
|
||||
let _ = writeln!(f, "{line}");
|
||||
}
|
||||
@@ -68,7 +88,7 @@ pub fn debug_log(msg: &str, base_dir: Option<&Path>) {
|
||||
}
|
||||
}
|
||||
|
||||
// Unconditional note logging to sandbox.log
|
||||
// Unconditional note logging to the daily sandbox log.
|
||||
pub fn log_note(msg: &str, base_dir: Option<&Path>) {
|
||||
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
||||
append_line(&format!("[{ts} {}] {}", exe_label(), msg), base_dir);
|
||||
@@ -88,4 +108,38 @@ mod tests {
|
||||
let previewed = result.unwrap();
|
||||
assert!(previewed.len() <= LOG_COMMAND_PREVIEW_LIMIT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_note_writes_to_daily_rolling_log() {
|
||||
let tempdir = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
log_note("hello daily log", Some(tempdir.path()));
|
||||
|
||||
let entries = std::fs::read_dir(tempdir.path())
|
||||
.expect("read log dir")
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect("read entries");
|
||||
assert_eq!(entries.len(), 1);
|
||||
|
||||
let log_path = entries[0].path();
|
||||
let filename = log_path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.expect("utf-8 filename");
|
||||
assert!(filename.starts_with("sandbox."));
|
||||
assert!(filename.ends_with(".log"));
|
||||
|
||||
let log = std::fs::read_to_string(log_path).expect("read log");
|
||||
assert!(log.contains("hello daily log"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_file_path_for_utc_date_matches_rolling_appender_name() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 21).expect("valid date");
|
||||
|
||||
assert_eq!(
|
||||
log_file_path_for_utc_date(Path::new("logs"), date),
|
||||
PathBuf::from("logs").join("sandbox.2026-05-21.log")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ fn sandbox_home(name: &str) -> TempDir {
|
||||
}
|
||||
|
||||
fn sandbox_log(codex_home: &Path) -> String {
|
||||
let log_path = codex_home.join(".sandbox").join("sandbox.log");
|
||||
let log_path = crate::current_log_file_path(&codex_home.join(".sandbox"));
|
||||
fs::read_to_string(&log_path)
|
||||
.unwrap_or_else(|err| format!("failed to read {}: {err}", log_path.display()))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
"""Fetch executable artifacts from checked-in DotSlash manifests."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import stat
|
||||
import tarfile
|
||||
import tempfile
|
||||
import zipfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
from .targets import TargetSpec
|
||||
|
||||
|
||||
DOWNLOAD_TIMEOUT_SECS = 60
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DotSlashArtifact:
|
||||
size: int
|
||||
digest: str
|
||||
archive_format: str
|
||||
archive_member: str
|
||||
url: str
|
||||
|
||||
|
||||
def fetch_dotslash_executable(
|
||||
spec: TargetSpec,
|
||||
*,
|
||||
manifest_path: Path,
|
||||
artifact_label: str,
|
||||
cache_key: str,
|
||||
dest_name: str,
|
||||
missing_ok: bool = False,
|
||||
) -> Path | None:
|
||||
artifact = artifact_for_target(
|
||||
spec,
|
||||
manifest_path,
|
||||
artifact_label=artifact_label,
|
||||
missing_ok=missing_ok,
|
||||
)
|
||||
if artifact is None:
|
||||
return None
|
||||
|
||||
cache_dir = default_cache_root() / cache_key
|
||||
archive_path = cache_dir / archive_filename(artifact.url)
|
||||
|
||||
if not archive_is_valid(archive_path, artifact, artifact_label):
|
||||
download_archive(artifact.url, archive_path)
|
||||
try:
|
||||
verify_archive(archive_path, artifact, artifact_label)
|
||||
except RuntimeError:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
dest = cache_dir / dest_name
|
||||
extract_archive_member(archive_path, artifact, dest, artifact_label)
|
||||
if not spec.is_windows:
|
||||
mode = dest.stat().st_mode
|
||||
dest.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
return dest
|
||||
|
||||
|
||||
def artifact_for_target(
|
||||
spec: TargetSpec,
|
||||
manifest_path: Path,
|
||||
*,
|
||||
artifact_label: str,
|
||||
missing_ok: bool = False,
|
||||
) -> DotSlashArtifact | None:
|
||||
manifest = load_manifest(manifest_path)
|
||||
platform_info = manifest.get("platforms", {}).get(spec.dotslash_platform)
|
||||
if platform_info is None:
|
||||
if missing_ok:
|
||||
return None
|
||||
raise RuntimeError(
|
||||
f"{artifact_label} manifest {manifest_path} is missing platform "
|
||||
f"{spec.dotslash_platform!r}"
|
||||
)
|
||||
|
||||
providers = platform_info.get("providers")
|
||||
if not providers:
|
||||
raise RuntimeError(
|
||||
f"{artifact_label} manifest {manifest_path} has no providers for "
|
||||
f"{spec.dotslash_platform!r}"
|
||||
)
|
||||
|
||||
hash_name = platform_info.get("hash")
|
||||
if hash_name != "sha256":
|
||||
raise RuntimeError(
|
||||
f"Unsupported {artifact_label} hash {hash_name!r} for "
|
||||
f"{spec.dotslash_platform!r}; expected sha256"
|
||||
)
|
||||
|
||||
return DotSlashArtifact(
|
||||
size=int(platform_info["size"]),
|
||||
digest=str(platform_info["digest"]),
|
||||
archive_format=str(platform_info["format"]),
|
||||
archive_member=str(platform_info["path"]),
|
||||
url=str(providers[0]["url"]),
|
||||
)
|
||||
|
||||
|
||||
def load_manifest(manifest_path: Path) -> dict:
|
||||
text = manifest_path.read_text(encoding="utf-8")
|
||||
if text.startswith("#!"):
|
||||
text = "\n".join(text.splitlines()[1:])
|
||||
return json.loads(text)
|
||||
|
||||
|
||||
def default_cache_root() -> Path:
|
||||
return Path(tempfile.gettempdir()) / "codex-package"
|
||||
|
||||
|
||||
def archive_filename(url: str) -> str:
|
||||
filename = Path(urlparse(url).path).name
|
||||
if not filename:
|
||||
raise RuntimeError(f"Unable to determine archive filename from {url}")
|
||||
return filename
|
||||
|
||||
|
||||
def archive_is_valid(
|
||||
archive_path: Path,
|
||||
artifact: DotSlashArtifact,
|
||||
artifact_label: str,
|
||||
) -> bool:
|
||||
if not archive_path.is_file():
|
||||
return False
|
||||
try:
|
||||
verify_archive(archive_path, artifact, artifact_label)
|
||||
except RuntimeError:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def verify_archive(
|
||||
archive_path: Path,
|
||||
artifact: DotSlashArtifact,
|
||||
artifact_label: str,
|
||||
) -> None:
|
||||
actual_size = archive_path.stat().st_size
|
||||
if actual_size != artifact.size:
|
||||
raise RuntimeError(
|
||||
f"{artifact_label} archive {archive_path} has size {actual_size}, "
|
||||
f"expected {artifact.size}"
|
||||
)
|
||||
|
||||
digest = hashlib.sha256()
|
||||
with open(archive_path, "rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
|
||||
actual_digest = digest.hexdigest()
|
||||
if actual_digest != artifact.digest:
|
||||
raise RuntimeError(
|
||||
f"{artifact_label} archive {archive_path} has sha256 {actual_digest}, "
|
||||
f"expected {artifact.digest}"
|
||||
)
|
||||
|
||||
|
||||
def download_archive(url: str, archive_path: Path) -> None:
|
||||
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temp_path = archive_path.with_suffix(f"{archive_path.suffix}.tmp")
|
||||
temp_path.unlink(missing_ok=True)
|
||||
try:
|
||||
with urlopen(url, timeout=DOWNLOAD_TIMEOUT_SECS) as response:
|
||||
with open(temp_path, "wb") as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
temp_path.replace(archive_path)
|
||||
finally:
|
||||
temp_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def extract_archive_member(
|
||||
archive_path: Path,
|
||||
artifact: DotSlashArtifact,
|
||||
dest: Path,
|
||||
artifact_label: str,
|
||||
) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
dest.unlink(missing_ok=True)
|
||||
|
||||
if artifact.archive_format == "tar.gz":
|
||||
with tarfile.open(archive_path, "r:gz") as archive:
|
||||
try:
|
||||
member = archive.getmember(artifact.archive_member)
|
||||
except KeyError as exc:
|
||||
raise RuntimeError(
|
||||
f"{artifact_label} archive {archive_path} is missing "
|
||||
f"{artifact.archive_member!r}"
|
||||
) from exc
|
||||
|
||||
extracted = archive.extractfile(member)
|
||||
if extracted is None:
|
||||
raise RuntimeError(
|
||||
f"{artifact_label} archive member {artifact.archive_member!r} is not a file"
|
||||
)
|
||||
with extracted, open(dest, "wb") as out:
|
||||
shutil.copyfileobj(extracted, out)
|
||||
return
|
||||
|
||||
if artifact.archive_format == "zip":
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
try:
|
||||
with archive.open(artifact.archive_member) as extracted:
|
||||
with open(dest, "wb") as out:
|
||||
shutil.copyfileobj(extracted, out)
|
||||
except KeyError as exc:
|
||||
raise RuntimeError(
|
||||
f"{artifact_label} archive {archive_path} is missing "
|
||||
f"{artifact.archive_member!r}"
|
||||
) from exc
|
||||
return
|
||||
|
||||
raise RuntimeError(
|
||||
f"Unsupported {artifact_label} archive format {artifact.archive_format!r}; "
|
||||
"expected tar.gz or zip"
|
||||
)
|
||||
@@ -1,33 +1,14 @@
|
||||
"""Fetch ripgrep from the DotSlash manifest used by the package builder."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import stat
|
||||
import tarfile
|
||||
import tempfile
|
||||
import zipfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
from .dotslash import fetch_dotslash_executable
|
||||
from .targets import REPO_ROOT
|
||||
from .targets import TargetSpec
|
||||
from .targets import resolve_input_path
|
||||
|
||||
|
||||
RG_MANIFEST = REPO_ROOT / "scripts" / "codex_package" / "rg"
|
||||
DOWNLOAD_TIMEOUT_SECS = 60
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RgArtifact:
|
||||
size: int
|
||||
digest: str
|
||||
archive_format: str
|
||||
archive_member: str
|
||||
url: str
|
||||
|
||||
|
||||
def resolve_rg_bin(spec: TargetSpec, rg_bin: Path | None) -> Path:
|
||||
@@ -41,155 +22,14 @@ def fetch_rg(
|
||||
spec: TargetSpec,
|
||||
*,
|
||||
manifest_path: Path = RG_MANIFEST,
|
||||
cache_root: Path | None = None,
|
||||
) -> Path:
|
||||
artifact = artifact_for_target(spec, manifest_path)
|
||||
cache_dir = (cache_root or default_cache_root()) / f"{spec.target}-rg"
|
||||
archive_path = cache_dir / archive_filename(artifact.url)
|
||||
|
||||
if not archive_is_valid(archive_path, artifact):
|
||||
download_archive(artifact.url, archive_path)
|
||||
try:
|
||||
verify_archive(archive_path, artifact)
|
||||
except RuntimeError:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
dest = cache_dir / spec.rg_name
|
||||
extract_rg(archive_path, artifact, dest)
|
||||
if not spec.is_windows:
|
||||
mode = dest.stat().st_mode
|
||||
dest.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
return dest
|
||||
|
||||
|
||||
def artifact_for_target(spec: TargetSpec, manifest_path: Path) -> RgArtifact:
|
||||
manifest = load_manifest(manifest_path)
|
||||
try:
|
||||
platform_info = manifest["platforms"][spec.dotslash_platform]
|
||||
except KeyError as exc:
|
||||
raise RuntimeError(
|
||||
f"ripgrep manifest {manifest_path} is missing platform {spec.dotslash_platform!r}"
|
||||
) from exc
|
||||
|
||||
providers = platform_info.get("providers")
|
||||
if not providers:
|
||||
raise RuntimeError(
|
||||
f"ripgrep manifest {manifest_path} has no providers for {spec.dotslash_platform!r}"
|
||||
)
|
||||
|
||||
hash_name = platform_info.get("hash")
|
||||
if hash_name != "sha256":
|
||||
raise RuntimeError(
|
||||
f"Unsupported ripgrep hash {hash_name!r} for "
|
||||
f"{spec.dotslash_platform!r}; expected sha256"
|
||||
)
|
||||
|
||||
return RgArtifact(
|
||||
size=int(platform_info["size"]),
|
||||
digest=str(platform_info["digest"]),
|
||||
archive_format=str(platform_info["format"]),
|
||||
archive_member=str(platform_info["path"]),
|
||||
url=str(providers[0]["url"]),
|
||||
)
|
||||
|
||||
|
||||
def load_manifest(manifest_path: Path) -> dict:
|
||||
text = manifest_path.read_text(encoding="utf-8")
|
||||
if text.startswith("#!"):
|
||||
text = "\n".join(text.splitlines()[1:])
|
||||
return json.loads(text)
|
||||
|
||||
|
||||
def default_cache_root() -> Path:
|
||||
return Path(tempfile.gettempdir()) / "codex-package"
|
||||
|
||||
|
||||
def archive_filename(url: str) -> str:
|
||||
filename = Path(urlparse(url).path).name
|
||||
if not filename:
|
||||
raise RuntimeError(f"Unable to determine archive filename from {url}")
|
||||
return filename
|
||||
|
||||
|
||||
def archive_is_valid(archive_path: Path, artifact: RgArtifact) -> bool:
|
||||
if not archive_path.is_file():
|
||||
return False
|
||||
try:
|
||||
verify_archive(archive_path, artifact)
|
||||
except RuntimeError:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def verify_archive(archive_path: Path, artifact: RgArtifact) -> None:
|
||||
actual_size = archive_path.stat().st_size
|
||||
if actual_size != artifact.size:
|
||||
raise RuntimeError(
|
||||
f"ripgrep archive {archive_path} has size {actual_size}, expected {artifact.size}"
|
||||
)
|
||||
|
||||
digest = hashlib.sha256()
|
||||
with open(archive_path, "rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
|
||||
actual_digest = digest.hexdigest()
|
||||
if actual_digest != artifact.digest:
|
||||
raise RuntimeError(
|
||||
f"ripgrep archive {archive_path} has sha256 {actual_digest}, "
|
||||
f"expected {artifact.digest}"
|
||||
)
|
||||
|
||||
|
||||
def download_archive(url: str, archive_path: Path) -> None:
|
||||
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temp_path = archive_path.with_suffix(f"{archive_path.suffix}.tmp")
|
||||
temp_path.unlink(missing_ok=True)
|
||||
try:
|
||||
with urlopen(url, timeout=DOWNLOAD_TIMEOUT_SECS) as response:
|
||||
with open(temp_path, "wb") as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
temp_path.replace(archive_path)
|
||||
finally:
|
||||
temp_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def extract_rg(archive_path: Path, artifact: RgArtifact, dest: Path) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
dest.unlink(missing_ok=True)
|
||||
|
||||
if artifact.archive_format == "tar.gz":
|
||||
with tarfile.open(archive_path, "r:gz") as archive:
|
||||
try:
|
||||
member = archive.getmember(artifact.archive_member)
|
||||
except KeyError as exc:
|
||||
raise RuntimeError(
|
||||
f"ripgrep archive {archive_path} is missing {artifact.archive_member!r}"
|
||||
) from exc
|
||||
|
||||
extracted = archive.extractfile(member)
|
||||
if extracted is None:
|
||||
raise RuntimeError(
|
||||
f"ripgrep archive member {artifact.archive_member!r} is not a file"
|
||||
)
|
||||
with extracted, open(dest, "wb") as out:
|
||||
shutil.copyfileobj(extracted, out)
|
||||
return
|
||||
|
||||
if artifact.archive_format == "zip":
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
try:
|
||||
with archive.open(artifact.archive_member) as extracted:
|
||||
with open(dest, "wb") as out:
|
||||
shutil.copyfileobj(extracted, out)
|
||||
except KeyError as exc:
|
||||
raise RuntimeError(
|
||||
f"ripgrep archive {archive_path} is missing {artifact.archive_member!r}"
|
||||
) from exc
|
||||
return
|
||||
|
||||
raise RuntimeError(
|
||||
f"Unsupported ripgrep archive format {artifact.archive_format!r}; expected tar.gz or zip"
|
||||
rg_bin = fetch_dotslash_executable(
|
||||
spec,
|
||||
manifest_path=manifest_path,
|
||||
artifact_label="ripgrep",
|
||||
cache_key=f"{spec.target}-rg",
|
||||
dest_name=spec.rg_name,
|
||||
)
|
||||
if rg_bin is None:
|
||||
raise AssertionError("ripgrep is required for all package targets")
|
||||
return rg_bin
|
||||
|
||||
Reference in New Issue
Block a user