Skip to content

Commit f6dc92a

Browse files
authored
Merge branch 'main' into add-github-action-for-nix
2 parents 1477405 + 995f5c3 commit f6dc92a

File tree

26 files changed

+384
-105
lines changed

26 files changed

+384
-105
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ result
3030
# cli tools
3131
CLAUDE.md
3232
.claude/
33+
AGENTS.override.md
3334

3435
# caches
3536
.cache/

codex-rs/app-server-protocol/src/protocol.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use codex_protocol::config_types::ReasoningEffort;
99
use codex_protocol::config_types::ReasoningSummary;
1010
use codex_protocol::config_types::SandboxMode;
1111
use codex_protocol::config_types::Verbosity;
12+
use codex_protocol::parse_command::ParsedCommand;
1213
use codex_protocol::protocol::AskForApproval;
1314
use codex_protocol::protocol::EventMsg;
1415
use codex_protocol::protocol::FileChange;
@@ -697,6 +698,7 @@ pub struct ExecCommandApprovalParams {
697698
pub cwd: PathBuf,
698699
#[serde(skip_serializing_if = "Option::is_none")]
699700
pub reason: Option<String>,
701+
pub parsed_cmd: Vec<ParsedCommand>,
700702
}
701703

702704
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
@@ -904,6 +906,9 @@ mod tests {
904906
command: vec!["echo".to_string(), "hello".to_string()],
905907
cwd: PathBuf::from("/tmp"),
906908
reason: Some("because tests".to_string()),
909+
parsed_cmd: vec![ParsedCommand::Unknown {
910+
cmd: "echo hello".to_string(),
911+
}],
907912
};
908913
let request = ServerRequest::ExecCommandApproval {
909914
request_id: RequestId::Integer(7),
@@ -920,6 +925,12 @@ mod tests {
920925
"command": ["echo", "hello"],
921926
"cwd": "/tmp",
922927
"reason": "because tests",
928+
"parsedCmd": [
929+
{
930+
"type": "unknown",
931+
"cmd": "echo hello"
932+
}
933+
]
923934
}
924935
}),
925936
serde_json::to_value(&request)?,

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,13 +1284,15 @@ async fn apply_bespoke_event_handling(
12841284
command,
12851285
cwd,
12861286
reason,
1287+
parsed_cmd,
12871288
}) => {
12881289
let params = ExecCommandApprovalParams {
12891290
conversation_id,
12901291
call_id,
12911292
command,
12921293
cwd,
12931294
reason,
1295+
parsed_cmd,
12941296
};
12951297
let rx = outgoing
12961298
.send_request(ServerRequestPayload::ExecCommandApproval(params))

codex-rs/app-server/tests/suite/codex_message_processor_flow.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use codex_core::protocol_config_types::ReasoningEffort;
2727
use codex_core::protocol_config_types::ReasoningSummary;
2828
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
2929
use codex_protocol::config_types::SandboxMode;
30+
use codex_protocol::parse_command::ParsedCommand;
3031
use codex_protocol::protocol::Event;
3132
use codex_protocol::protocol::EventMsg;
3233
use codex_protocol::protocol::InputMessageKind;
@@ -311,6 +312,9 @@ async fn test_send_user_turn_changes_approval_policy_behavior() {
311312
],
312313
cwd: working_directory.clone(),
313314
reason: None,
315+
parsed_cmd: vec![ParsedCommand::Unknown {
316+
cmd: "python3 -c 'print(42)'".to_string()
317+
}],
314318
},
315319
params
316320
);

codex-rs/cli/src/mcp_cmd.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use codex_core::mcp::auth::compute_auth_statuses;
1818
use codex_core::protocol::McpAuthStatus;
1919
use codex_rmcp_client::delete_oauth_tokens;
2020
use codex_rmcp_client::perform_oauth_login;
21+
use codex_rmcp_client::supports_oauth_login;
2122

2223
/// [experimental] Launch Codex as an MCP server or manage configured MCP servers.
2324
///
@@ -190,7 +191,10 @@ impl McpCli {
190191

191192
async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<()> {
192193
// Validate any provided overrides even though they are not currently applied.
193-
config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
194+
let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
195+
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
196+
.await
197+
.context("failed to load configuration")?;
194198

195199
let AddArgs {
196200
name,
@@ -226,17 +230,21 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
226230
}
227231
}
228232
AddMcpTransportArgs {
229-
streamable_http: Some(streamable_http),
233+
streamable_http:
234+
Some(AddMcpStreamableHttpArgs {
235+
url,
236+
bearer_token_env_var,
237+
}),
230238
..
231239
} => McpServerTransportConfig::StreamableHttp {
232-
url: streamable_http.url,
233-
bearer_token_env_var: streamable_http.bearer_token_env_var,
240+
url,
241+
bearer_token_env_var,
234242
},
235243
AddMcpTransportArgs { .. } => bail!("exactly one of --command or --url must be provided"),
236244
};
237245

238246
let new_entry = McpServerConfig {
239-
transport,
247+
transport: transport.clone(),
240248
enabled: true,
241249
startup_timeout_sec: None,
242250
tool_timeout_sec: None,
@@ -249,6 +257,17 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
249257

250258
println!("Added global MCP server '{name}'.");
251259

260+
if let McpServerTransportConfig::StreamableHttp {
261+
url,
262+
bearer_token_env_var: None,
263+
} = transport
264+
&& matches!(supports_oauth_login(&url).await, Ok(true))
265+
{
266+
println!("Detected OAuth support. Starting OAuth flow…");
267+
perform_oauth_login(&name, &url, config.mcp_oauth_credentials_store_mode).await?;
268+
println!("Successfully logged in.");
269+
}
270+
252271
Ok(())
253272
}
254273

codex-rs/core/src/codex.rs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use codex_apply_patch::ApplyPatchAction;
1717
use codex_protocol::ConversationId;
1818
use codex_protocol::protocol::ConversationPathResponseEvent;
1919
use codex_protocol::protocol::ExitedReviewModeEvent;
20+
use codex_protocol::protocol::McpAuthStatus;
2021
use codex_protocol::protocol::ReviewRequest;
2122
use codex_protocol::protocol::RolloutItem;
2223
use codex_protocol::protocol::SessionSource;
@@ -372,10 +373,25 @@ impl Session {
372373
);
373374
let default_shell_fut = shell::default_user_shell();
374375
let history_meta_fut = crate::message_history::history_metadata(&config);
376+
let auth_statuses_fut = compute_auth_statuses(
377+
config.mcp_servers.iter(),
378+
config.mcp_oauth_credentials_store_mode,
379+
);
375380

376381
// Join all independent futures.
377-
let (rollout_recorder, mcp_res, default_shell, (history_log_id, history_entry_count)) =
378-
tokio::join!(rollout_fut, mcp_fut, default_shell_fut, history_meta_fut);
382+
let (
383+
rollout_recorder,
384+
mcp_res,
385+
default_shell,
386+
(history_log_id, history_entry_count),
387+
auth_statuses,
388+
) = tokio::join!(
389+
rollout_fut,
390+
mcp_fut,
391+
default_shell_fut,
392+
history_meta_fut,
393+
auth_statuses_fut
394+
);
379395

380396
let rollout_recorder = rollout_recorder.map_err(|e| {
381397
error!("failed to initialize rollout recorder: {e:#}");
@@ -402,11 +418,24 @@ impl Session {
402418
// Surface individual client start-up failures to the user.
403419
if !failed_clients.is_empty() {
404420
for (server_name, err) in failed_clients {
405-
let message = format!("MCP client for `{server_name}` failed to start: {err:#}");
406-
error!("{message}");
421+
let log_message =
422+
format!("MCP client for `{server_name}` failed to start: {err:#}");
423+
error!("{log_message}");
424+
let display_message = if matches!(
425+
auth_statuses.get(&server_name),
426+
Some(McpAuthStatus::NotLoggedIn)
427+
) {
428+
format!(
429+
"The {server_name} MCP server is not logged in. Run `codex mcp login {server_name}` to log in."
430+
)
431+
} else {
432+
log_message
433+
};
407434
post_session_configured_error_events.push(Event {
408435
id: INITIAL_SUBMIT_ID.to_owned(),
409-
msg: EventMsg::Error(ErrorEvent { message }),
436+
msg: EventMsg::Error(ErrorEvent {
437+
message: display_message,
438+
}),
410439
});
411440
}
412441
}
@@ -591,13 +620,15 @@ impl Session {
591620
warn!("Overwriting existing pending approval for sub_id: {event_id}");
592621
}
593622

623+
let parsed_cmd = parse_command(&command);
594624
let event = Event {
595625
id: event_id,
596626
msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
597627
call_id,
598628
command,
599629
cwd,
600630
reason,
631+
parsed_cmd,
601632
}),
602633
};
603634
self.send_event(event).await;
@@ -853,10 +884,7 @@ impl Session {
853884
call_id,
854885
command: command_for_display.clone(),
855886
cwd,
856-
parsed_cmd: parse_command(&command_for_display)
857-
.into_iter()
858-
.map(Into::into)
859-
.collect(),
887+
parsed_cmd: parse_command(&command_for_display),
860888
}),
861889
};
862890
let event = Event {

codex-rs/core/src/config.rs

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ use crate::model_family::find_family_for_model;
2828
use crate::model_provider_info::ModelProviderInfo;
2929
use crate::model_provider_info::built_in_model_providers;
3030
use crate::openai_model_info::get_model_info;
31+
use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
32+
use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME;
3133
use crate::protocol::AskForApproval;
3234
use crate::protocol::SandboxPolicy;
3335
use anyhow::Context;
@@ -1123,6 +1125,15 @@ impl Config {
11231125
.or(cfg.review_model)
11241126
.unwrap_or_else(default_review_model);
11251127

1128+
let mut approval_policy = approval_policy
1129+
.or(config_profile.approval_policy)
1130+
.or(cfg.approval_policy)
1131+
.unwrap_or_else(AskForApproval::default);
1132+
1133+
if features.enabled(Feature::ApproveAll) {
1134+
approval_policy = AskForApproval::OnRequest;
1135+
}
1136+
11261137
let config = Self {
11271138
model,
11281139
review_model,
@@ -1133,10 +1144,7 @@ impl Config {
11331144
model_provider_id,
11341145
model_provider,
11351146
cwd: resolved_cwd,
1136-
approval_policy: approval_policy
1137-
.or(config_profile.approval_policy)
1138-
.or(cfg.approval_policy)
1139-
.unwrap_or_else(AskForApproval::default),
1147+
approval_policy,
11401148
sandbox_policy,
11411149
shell_environment_policy,
11421150
notify: cfg.notify,
@@ -1217,20 +1225,18 @@ impl Config {
12171225
}
12181226

12191227
fn load_instructions(codex_dir: Option<&Path>) -> Option<String> {
1220-
let mut p = match codex_dir {
1221-
Some(p) => p.to_path_buf(),
1222-
None => return None,
1223-
};
1224-
1225-
p.push("AGENTS.md");
1226-
std::fs::read_to_string(&p).ok().and_then(|s| {
1227-
let s = s.trim();
1228-
if s.is_empty() {
1229-
None
1230-
} else {
1231-
Some(s.to_string())
1228+
let base = codex_dir?;
1229+
for candidate in [LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME] {
1230+
let mut path = base.to_path_buf();
1231+
path.push(candidate);
1232+
if let Ok(contents) = std::fs::read_to_string(&path) {
1233+
let trimmed = contents.trim();
1234+
if !trimmed.is_empty() {
1235+
return Some(trimmed.to_string());
1236+
}
12321237
}
1233-
})
1238+
}
1239+
None
12341240
}
12351241

12361242
fn get_base_instructions(
@@ -1432,6 +1438,26 @@ exclude_slash_tmp = true
14321438
);
14331439
}
14341440

1441+
#[test]
1442+
fn approve_all_feature_forces_on_request_policy() -> std::io::Result<()> {
1443+
let cfg = r#"
1444+
[features]
1445+
approve_all = true
1446+
"#;
1447+
let parsed = toml::from_str::<ConfigToml>(cfg)
1448+
.expect("TOML deserialization should succeed for approve_all feature");
1449+
let temp_dir = TempDir::new()?;
1450+
let config = Config::load_from_base_config_with_overrides(
1451+
parsed,
1452+
ConfigOverrides::default(),
1453+
temp_dir.path().to_path_buf(),
1454+
)?;
1455+
1456+
assert!(config.features.enabled(Feature::ApproveAll));
1457+
assert_eq!(config.approval_policy, AskForApproval::OnRequest);
1458+
Ok(())
1459+
}
1460+
14351461
#[test]
14361462
fn config_defaults_to_auto_oauth_store_mode() -> std::io::Result<()> {
14371463
let codex_home = TempDir::new()?;

codex-rs/core/src/features.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ pub enum Feature {
4141
ViewImageTool,
4242
/// Allow the model to request web searches.
4343
WebSearchRequest,
44+
/// Automatically approve all approval requests from the harness.
45+
ApproveAll,
4446
}
4547

4648
impl Feature {
@@ -247,4 +249,10 @@ pub const FEATURES: &[FeatureSpec] = &[
247249
stage: Stage::Stable,
248250
default_enabled: false,
249251
},
252+
FeatureSpec {
253+
id: Feature::ApproveAll,
254+
key: "approve_all",
255+
stage: Stage::Experimental,
256+
default_enabled: false,
257+
},
250258
];

codex-rs/core/src/parse_command.rs

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,9 @@
11
use crate::bash::try_parse_bash;
22
use crate::bash::try_parse_word_only_commands_sequence;
3-
use serde::Deserialize;
4-
use serde::Serialize;
3+
use codex_protocol::parse_command::ParsedCommand;
54
use shlex::split as shlex_split;
65
use shlex::try_join as shlex_try_join;
76

8-
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
9-
pub enum ParsedCommand {
10-
Read {
11-
cmd: String,
12-
name: String,
13-
},
14-
ListFiles {
15-
cmd: String,
16-
path: Option<String>,
17-
},
18-
Search {
19-
cmd: String,
20-
query: Option<String>,
21-
path: Option<String>,
22-
},
23-
Unknown {
24-
cmd: String,
25-
},
26-
}
27-
28-
// Convert core's parsed command enum into the protocol's simplified type so
29-
// events can carry the canonical representation across process boundaries.
30-
impl From<ParsedCommand> for codex_protocol::parse_command::ParsedCommand {
31-
fn from(v: ParsedCommand) -> Self {
32-
use codex_protocol::parse_command::ParsedCommand as P;
33-
match v {
34-
ParsedCommand::Read { cmd, name } => P::Read { cmd, name },
35-
ParsedCommand::ListFiles { cmd, path } => P::ListFiles { cmd, path },
36-
ParsedCommand::Search { cmd, query, path } => P::Search { cmd, query, path },
37-
ParsedCommand::Unknown { cmd } => P::Unknown { cmd },
38-
}
39-
}
40-
}
41-
427
fn shlex_join(tokens: &[String]) -> String {
438
shlex_try_join(tokens.iter().map(String::as_str))
449
.unwrap_or_else(|_| "<command included NUL byte>".to_string())

0 commit comments

Comments
 (0)