mirror of
https://github.com/openai/codex.git
synced 2026-03-03 13:13:18 +00:00
Compare commits
3 Commits
fix/notify
...
issue-2346
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d489d10ec1 | ||
|
|
cea7457251 | ||
|
|
793a798c31 |
@@ -411,6 +411,14 @@ set = { PATH = "/usr/bin", MY_FLAG = "1" }
|
||||
|
||||
Currently, `CODEX_SANDBOX_NETWORK_DISABLED=1` is also added to the environment, assuming network is disabled. This is not configurable.
|
||||
|
||||
## exec_timeout_ms
|
||||
|
||||
Default timeout (in milliseconds) for commands and tool calls when an execution does not specify a `timeout`. The default is 10,000 milliseconds (10 seconds).
|
||||
|
||||
```toml
|
||||
exec_timeout_ms = 60000 # allow commands to run for up to 60s
|
||||
```
|
||||
|
||||
## notify
|
||||
|
||||
Specify a program that will be executed to get notified about events generated by Codex. Note that the program will receive the notification argument as a string of JSON, e.g.:
|
||||
|
||||
@@ -297,6 +297,7 @@ pub(crate) struct TurnContext {
|
||||
pub(crate) shell_environment_policy: ShellEnvironmentPolicy,
|
||||
pub(crate) disable_response_storage: bool,
|
||||
pub(crate) tools_config: ToolsConfig,
|
||||
pub(crate) exec_timeout_ms: u64,
|
||||
}
|
||||
|
||||
impl TurnContext {
|
||||
@@ -522,6 +523,7 @@ impl Session {
|
||||
shell_environment_policy: config.shell_environment_policy.clone(),
|
||||
cwd,
|
||||
disable_response_storage,
|
||||
exec_timeout_ms: config.exec_timeout_ms,
|
||||
};
|
||||
let sess = Arc::new(Session {
|
||||
session_id,
|
||||
@@ -1112,6 +1114,7 @@ async fn submission_loop(
|
||||
shell_environment_policy: prev.shell_environment_policy.clone(),
|
||||
cwd: new_cwd.clone(),
|
||||
disable_response_storage: prev.disable_response_storage,
|
||||
exec_timeout_ms: prev.exec_timeout_ms,
|
||||
};
|
||||
|
||||
// Install the new persistent context for subsequent tasks/turns.
|
||||
@@ -1188,6 +1191,7 @@ async fn submission_loop(
|
||||
shell_environment_policy: turn_context.shell_environment_policy.clone(),
|
||||
cwd,
|
||||
disable_response_storage: turn_context.disable_response_storage,
|
||||
exec_timeout_ms: turn_context.exec_timeout_ms,
|
||||
};
|
||||
// TODO: record the new environment context in the conversation history
|
||||
// no current task, spawn a new one with the per‑turn context
|
||||
@@ -2145,8 +2149,7 @@ async fn handle_function_call(
|
||||
_ => {
|
||||
match sess.mcp_connection_manager.parse_tool_name(&name) {
|
||||
Some((server, tool_name)) => {
|
||||
// TODO(mbolin): Determine appropriate timeout for tool call.
|
||||
let timeout = None;
|
||||
let timeout = Some(Duration::from_millis(turn_context.exec_timeout_ms));
|
||||
handle_mcp_tool_call(
|
||||
sess, &sub_id, call_id, server, tool_name, arguments, timeout,
|
||||
)
|
||||
@@ -2223,7 +2226,7 @@ fn to_exec_params(params: ShellToolCallParams, turn_context: &TurnContext) -> Ex
|
||||
ExecParams {
|
||||
command: params.command,
|
||||
cwd: turn_context.resolve_path(params.workdir.clone()),
|
||||
timeout_ms: params.timeout_ms,
|
||||
timeout_ms: params.timeout_ms.or(Some(turn_context.exec_timeout_ms)),
|
||||
env: create_env(&turn_context.shell_environment_policy),
|
||||
with_escalated_permissions: params.with_escalated_permissions,
|
||||
justification: params.justification,
|
||||
@@ -2834,6 +2837,156 @@ async fn drain_to_completed(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::ConfigOverrides;
|
||||
use crate::config::ConfigToml;
|
||||
use crate::config_types::McpServerConfig;
|
||||
use codex_login::AuthManager;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
const DUMMY_MCP_SERVER: &str = r#"
|
||||
import sys, json, time
|
||||
|
||||
def send(msg):
|
||||
sys.stdout.write(json.dumps(msg) + '\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
while True:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
break
|
||||
req = json.loads(line)
|
||||
method = req.get('method')
|
||||
id_ = req.get('id')
|
||||
if method == 'initialize':
|
||||
send({
|
||||
'jsonrpc': '2.0',
|
||||
'id': id_,
|
||||
'result': {
|
||||
'protocolVersion': '2025-06-18',
|
||||
'capabilities': {'tools': {'listChanged': True}},
|
||||
'serverInfo': {'name': 'dummy', 'version': '0.0.0'}
|
||||
}
|
||||
})
|
||||
elif method == 'notifications/initialized':
|
||||
pass
|
||||
elif method == 'tools/list':
|
||||
send({
|
||||
'jsonrpc': '2.0',
|
||||
'id': id_,
|
||||
'result': {
|
||||
'tools': [{
|
||||
'name': 'sleep',
|
||||
'description': 'sleep',
|
||||
'inputSchema': {
|
||||
'type': 'object',
|
||||
'properties': {'duration_ms': {'type': 'integer'}},
|
||||
'required': ['duration_ms']
|
||||
}
|
||||
}]}
|
||||
})
|
||||
elif method == 'tools/call':
|
||||
args = req.get('params', {}).get('arguments') or {}
|
||||
duration_ms = args.get('duration_ms', 0)
|
||||
time.sleep(duration_ms / 1000.0)
|
||||
send({
|
||||
'jsonrpc': '2.0',
|
||||
'id': id_,
|
||||
'result': {
|
||||
'content': [{'type': 'text', 'text': 'slept'}],
|
||||
'isError': False
|
||||
}
|
||||
})
|
||||
else:
|
||||
send({'jsonrpc': '2.0', 'id': id_, 'error': {'code': -32601, 'message': 'unknown method'}})
|
||||
"#;
|
||||
|
||||
async fn setup_session(exec_timeout_ms: u64) -> (Arc<Session>, TurnContext, TempDir) {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let script_path = tmp.path().join("dummy_mcp_server.py");
|
||||
std::fs::write(&script_path, DUMMY_MCP_SERVER).unwrap();
|
||||
|
||||
let mut config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides::default(),
|
||||
tmp.path().to_path_buf(),
|
||||
)
|
||||
.expect("defaults for test should always succeed");
|
||||
config.mcp_servers.insert(
|
||||
"dummy".to_string(),
|
||||
McpServerConfig {
|
||||
command: "python3".to_string(),
|
||||
args: vec!["-u".to_string(), script_path.to_string_lossy().into()],
|
||||
env: None,
|
||||
},
|
||||
);
|
||||
|
||||
let (tx_event, _rx_event) = async_channel::unbounded();
|
||||
let configure_session = ConfigureSession {
|
||||
provider: config.model_provider.clone(),
|
||||
model: config.model.clone(),
|
||||
model_reasoning_effort: config.model_reasoning_effort,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
user_instructions: None,
|
||||
base_instructions: config.base_instructions.clone(),
|
||||
approval_policy: config.approval_policy,
|
||||
sandbox_policy: config.sandbox_policy.clone(),
|
||||
disable_response_storage: config.disable_response_storage,
|
||||
notify: config.notify.clone(),
|
||||
cwd: config.cwd.clone(),
|
||||
resume_path: None,
|
||||
};
|
||||
|
||||
let auth_manager =
|
||||
AuthManager::shared(config.codex_home.clone(), config.preferred_auth_method);
|
||||
let (session, mut turn_context) = Session::new(
|
||||
configure_session,
|
||||
Arc::new(config),
|
||||
auth_manager,
|
||||
tx_event,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
turn_context.exec_timeout_ms = exec_timeout_ms;
|
||||
(session, turn_context, tmp)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_tool_call_times_out() {
|
||||
let (session, turn_context, _tmp) = setup_session(10).await;
|
||||
let args = json!({ "duration_ms": 2000 }).to_string();
|
||||
let mut diff = TurnDiffTracker::default();
|
||||
let response = handle_function_call(
|
||||
&session,
|
||||
&turn_context,
|
||||
&mut diff,
|
||||
"sub".to_string(),
|
||||
"dummy__sleep".to_string(),
|
||||
args,
|
||||
"call".to_string(),
|
||||
)
|
||||
.await;
|
||||
match response {
|
||||
ResponseInputItem::McpToolCallOutput { result, .. } => {
|
||||
assert!(result.is_err(), "expected timeout error, got: {result:?}");
|
||||
}
|
||||
ResponseInputItem::FunctionCallOutput { output, .. } => {
|
||||
// Windows OS
|
||||
assert!(
|
||||
output.success.is_none(),
|
||||
"expected timeout error, got: {output:?}"
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_call_tool_result_to_function_call_output_payload(
|
||||
call_tool_result: &CallToolResult,
|
||||
) -> FunctionCallOutputPayload {
|
||||
@@ -2870,7 +3023,7 @@ fn convert_call_tool_result_to_function_call_output_payload(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod tests_content {
|
||||
use super::*;
|
||||
use mcp_types::ContentBlock;
|
||||
use mcp_types::TextContent;
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::config_types::ShellEnvironmentPolicyToml;
|
||||
use crate::config_types::Tui;
|
||||
use crate::config_types::UriBasedFileOpener;
|
||||
use crate::config_types::Verbosity;
|
||||
use crate::exec::DEFAULT_TIMEOUT_MS;
|
||||
use crate::git_info::resolve_root_git_project_for_trust;
|
||||
use crate::model_family::ModelFamily;
|
||||
use crate::model_family::find_family_for_model;
|
||||
@@ -66,6 +67,9 @@ pub struct Config {
|
||||
|
||||
pub shell_environment_policy: ShellEnvironmentPolicy,
|
||||
|
||||
/// Default timeout for `exec` commands (milliseconds) when a tool call does not specify one.
|
||||
pub exec_timeout_ms: u64,
|
||||
|
||||
/// When `true`, `AgentReasoning` events emitted by the backend will be
|
||||
/// suppressed from the frontend output. This can reduce visual noise when
|
||||
/// users are only interested in the final agent responses.
|
||||
@@ -401,6 +405,9 @@ pub struct ConfigToml {
|
||||
#[serde(default)]
|
||||
pub shell_environment_policy: ShellEnvironmentPolicyToml,
|
||||
|
||||
/// Default timeout for exec commands (milliseconds).
|
||||
pub exec_timeout_ms: Option<u64>,
|
||||
|
||||
/// Sandbox mode to use.
|
||||
pub sandbox_mode: Option<SandboxMode>,
|
||||
|
||||
@@ -655,6 +662,7 @@ impl Config {
|
||||
.clone();
|
||||
|
||||
let shell_environment_policy = cfg.shell_environment_policy.clone().into();
|
||||
let exec_timeout_ms = cfg.exec_timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS);
|
||||
|
||||
let resolved_cwd = {
|
||||
use std::env;
|
||||
@@ -739,6 +747,7 @@ impl Config {
|
||||
.unwrap_or_else(AskForApproval::default),
|
||||
sandbox_policy,
|
||||
shell_environment_policy,
|
||||
exec_timeout_ms,
|
||||
disable_response_storage: config_profile
|
||||
.disable_response_storage
|
||||
.or(cfg.disable_response_storage)
|
||||
@@ -1126,6 +1135,7 @@ disable_response_storage = true
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
exec_timeout_ms: DEFAULT_TIMEOUT_MS,
|
||||
disable_response_storage: false,
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
@@ -1182,6 +1192,7 @@ disable_response_storage = true
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
exec_timeout_ms: DEFAULT_TIMEOUT_MS,
|
||||
disable_response_storage: false,
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
@@ -1253,6 +1264,7 @@ disable_response_storage = true
|
||||
approval_policy: AskForApproval::OnFailure,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
exec_timeout_ms: DEFAULT_TIMEOUT_MS,
|
||||
disable_response_storage: true,
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::spawn::StdioPolicy;
|
||||
use crate::spawn::spawn_child_async;
|
||||
use serde_bytes::ByteBuf;
|
||||
|
||||
const DEFAULT_TIMEOUT_MS: u64 = 10_000;
|
||||
pub const DEFAULT_TIMEOUT_MS: u64 = 10_000;
|
||||
|
||||
// Hardcode these since it does not seem worth including the libc crate just
|
||||
// for these.
|
||||
|
||||
Reference in New Issue
Block a user