mirror of
https://github.com/openai/codex.git
synced 2026-03-05 21:45:28 +03:00
Compare commits
5 Commits
rust-v0.21
...
pr2239
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c3d906f19 | ||
|
|
e8670ad840 | ||
|
|
596a9d6a96 | ||
|
|
320f150c68 | ||
|
|
7051a528a3 |
10
.github/actions/codex/bun.lock
vendored
10
.github/actions/codex/bun.lock
vendored
@@ -8,7 +8,7 @@
|
||||
"@actions/github": "^6.0.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.19",
|
||||
"@types/bun": "^1.2.20",
|
||||
"@types/node": "^24.2.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2",
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
"@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
|
||||
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
"before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
||||
"bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
@@ -82,12 +82,8 @@
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
}
|
||||
}
|
||||
|
||||
2
.github/actions/codex/package.json
vendored
2
.github/actions/codex/package.json
vendored
@@ -13,7 +13,7 @@
|
||||
"@actions/github": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.19",
|
||||
"@types/bun": "^1.2.20",
|
||||
"@types/node": "^24.2.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
|
||||
13
codex-rs/Cargo.lock
generated
13
codex-rs/Cargo.lock
generated
@@ -860,6 +860,18 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-protocol"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.2",
|
||||
"strum_macros 0.27.2",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-tui"
|
||||
version = "0.0.0"
|
||||
@@ -881,6 +893,7 @@ dependencies = [
|
||||
"image",
|
||||
"insta",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mcp-types",
|
||||
"path-clean",
|
||||
"pretty_assertions",
|
||||
|
||||
@@ -15,6 +15,7 @@ members = [
|
||||
"mcp-server",
|
||||
"mcp-types",
|
||||
"ollama",
|
||||
"protocol",
|
||||
"tui",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@@ -1981,7 +1981,7 @@ async fn handle_container_exec_with_params(
|
||||
let ExecToolCallOutput { exit_code, .. } = &output;
|
||||
|
||||
let is_success = *exit_code == 0;
|
||||
let content = format_exec_output(&output);
|
||||
let content = format_exec_output(output);
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id: call_id.clone(),
|
||||
output: FunctionCallOutputPayload {
|
||||
@@ -2113,7 +2113,7 @@ async fn handle_sandbox_error(
|
||||
let ExecToolCallOutput { exit_code, .. } = &retry_output;
|
||||
|
||||
let is_success = *exit_code == 0;
|
||||
let content = format_exec_output(&retry_output);
|
||||
let content = format_exec_output(retry_output);
|
||||
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id: call_id.clone(),
|
||||
@@ -2146,7 +2146,7 @@ async fn handle_sandbox_error(
|
||||
}
|
||||
|
||||
/// Exec output is a pre-serialized JSON payload
|
||||
fn format_exec_output(exec_output: &ExecToolCallOutput) -> String {
|
||||
fn format_exec_output(exec_output: ExecToolCallOutput) -> String {
|
||||
let ExecToolCallOutput {
|
||||
exit_code,
|
||||
stdout,
|
||||
@@ -2169,10 +2169,10 @@ fn format_exec_output(exec_output: &ExecToolCallOutput) -> String {
|
||||
// round to 1 decimal place
|
||||
let duration_seconds = ((duration.as_secs_f32()) * 10.0).round() / 10.0;
|
||||
|
||||
let is_success = *exit_code == 0;
|
||||
let is_success = exit_code == 0;
|
||||
let output = if is_success { stdout } else { stderr };
|
||||
|
||||
let mut formatted_output = output.text.clone();
|
||||
let mut formatted_output = output.text;
|
||||
if let Some(truncated_after_lines) = output.truncated_after_lines {
|
||||
formatted_output.push_str(&format!(
|
||||
"\n\n[Output truncated after {truncated_after_lines} lines: too many lines or bytes.]",
|
||||
@@ -2182,7 +2182,7 @@ fn format_exec_output(exec_output: &ExecToolCallOutput) -> String {
|
||||
let payload = ExecOutput {
|
||||
output: &formatted_output,
|
||||
metadata: ExecMetadata {
|
||||
exit_code: *exit_code,
|
||||
exit_code,
|
||||
duration_seconds,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -45,11 +45,54 @@ DEFAULT_ISSUER = "https://auth.openai.com"
|
||||
EXIT_CODE_WHEN_ADDRESS_ALREADY_IN_USE = 13
|
||||
|
||||
CA_CONTEXT = None
|
||||
try:
|
||||
import ssl
|
||||
import certifi as _certifi
|
||||
CODEX_LOGIN_TRACE = os.environ.get("CODEX_LOGIN_TRACE", "false") in ["true", "1"]
|
||||
|
||||
try:
|
||||
|
||||
def trace(msg: str) -> None:
|
||||
if CODEX_LOGIN_TRACE:
|
||||
print(msg)
|
||||
|
||||
def attempt_request(method: str) -> bool:
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
f"{DEFAULT_ISSUER}/.well-known/openid-configuration",
|
||||
method="GET",
|
||||
),
|
||||
context=CA_CONTEXT,
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
trace(f"Request using {method} failed: {resp.status}")
|
||||
return False
|
||||
|
||||
trace(f"Request using {method} succeeded")
|
||||
return True
|
||||
except Exception as e:
|
||||
trace(f"Request using {method} failed: {e}")
|
||||
return False
|
||||
|
||||
status = attempt_request("default settings")
|
||||
if not status:
|
||||
try:
|
||||
import truststore
|
||||
|
||||
truststore.inject_into_ssl()
|
||||
status = attempt_request("truststore")
|
||||
except Exception as e:
|
||||
trace(f"Failed to use truststore: {e}")
|
||||
|
||||
if not status:
|
||||
try:
|
||||
import ssl
|
||||
import certifi as _certifi
|
||||
|
||||
CA_CONTEXT = ssl.create_default_context(cafile=_certifi.where())
|
||||
status = attempt_request("certify")
|
||||
except Exception as e:
|
||||
trace(f"Failed to use certify: {e}")
|
||||
|
||||
|
||||
CA_CONTEXT = ssl.create_default_context(cafile=_certifi.where())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
21
codex-rs/protocol/Cargo.toml
Normal file
21
codex-rs/protocol/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "codex-protocol"
|
||||
version = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "codex_protocol"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
strum = "0.27.2"
|
||||
strum_macros = "0.27.2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
serde_json = "1"
|
||||
1
codex-rs/protocol/src/lib.rs
Normal file
1
codex-rs/protocol/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod notification;
|
||||
147
codex-rs/protocol/src/notification.rs
Normal file
147
codex-rs/protocol/src/notification.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum_macros::Display;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Display)]
|
||||
#[serde(tag = "method", rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum NotificationMessage {
|
||||
Conversation(ConversationNotification),
|
||||
ShutdownComplete,
|
||||
}
|
||||
|
||||
/// Notification associated with a conversation. The `conversation_id` is key
|
||||
/// so clients can dispatch messages to the correct conversation.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct ConversationNotification {
|
||||
conversation_id: Uuid,
|
||||
#[serde(flatten)]
|
||||
message: ConversationNotificationMessage,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Display)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
enum ConversationNotificationMessage {
|
||||
Initialized(ConversationInitialized),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct ConversationInitialized {
|
||||
model: String,
|
||||
}
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde::de::Error as _;
|
||||
use serde_json::json;
|
||||
|
||||
// The idea is that the way we map `NotificationMessage` to an MCP
|
||||
// notification is to use the tuple type as the `params`.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct JsonrpcNotification {
|
||||
jsonrpc: String,
|
||||
method: String,
|
||||
params: serde_json::Value,
|
||||
}
|
||||
|
||||
// Example of how JSON-RPC serialization could work.
|
||||
fn to_jsonrpc_message(
|
||||
notification: NotificationMessage,
|
||||
) -> Result<serde_json::Value, serde_json::Error> {
|
||||
let method = notification.to_string();
|
||||
let params = match notification {
|
||||
NotificationMessage::Conversation(notification) => serde_json::to_value(notification)?,
|
||||
NotificationMessage::ShutdownComplete => serde_json::Value::Null,
|
||||
};
|
||||
|
||||
let jsonrpc_notification = JsonrpcNotification {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
method,
|
||||
params,
|
||||
};
|
||||
|
||||
serde_json::to_value(jsonrpc_notification)
|
||||
}
|
||||
|
||||
fn from_jsonrpc_message(
|
||||
jsonrpc_notification: JsonrpcNotification,
|
||||
) -> Result<NotificationMessage, serde_json::Error> {
|
||||
let JsonrpcNotification {
|
||||
jsonrpc: _,
|
||||
method,
|
||||
params,
|
||||
} = jsonrpc_notification;
|
||||
|
||||
match method.as_str() {
|
||||
"conversation" => {
|
||||
let conversation_notification: ConversationNotification =
|
||||
serde_json::from_value(params)?;
|
||||
Ok(NotificationMessage::Conversation(conversation_notification))
|
||||
}
|
||||
"shutdown_complete" => Ok(NotificationMessage::ShutdownComplete),
|
||||
_ => Err(serde_json::Error::custom(format!(
|
||||
"Unknown method: {method}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_notification_message_conversation() {
|
||||
let conversation_id = Uuid::parse_str("123e4567-e89b-12d3-a456-426614174000").unwrap();
|
||||
let message = NotificationMessage::Conversation(ConversationNotification {
|
||||
conversation_id,
|
||||
message: ConversationNotificationMessage::Initialized(ConversationInitialized {
|
||||
model: "gpt-5".to_string(),
|
||||
}),
|
||||
});
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "conversation",
|
||||
"conversation_id": conversation_id.to_string(),
|
||||
"type": "initialized",
|
||||
"model": "gpt-5",
|
||||
}),
|
||||
serde_json::to_value(message.clone()).unwrap()
|
||||
);
|
||||
|
||||
let expected_jsonrpc_message = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "conversation",
|
||||
"params": {
|
||||
"conversation_id": conversation_id.to_string(),
|
||||
"type": "initialized",
|
||||
"model": "gpt-5",
|
||||
}
|
||||
});
|
||||
assert_eq!(
|
||||
expected_jsonrpc_message,
|
||||
to_jsonrpc_message(message.clone()).unwrap()
|
||||
);
|
||||
|
||||
let serialized_json_rpc_message = serde_json::to_string(&expected_jsonrpc_message).unwrap();
|
||||
let deserialized_json_rpc_message =
|
||||
serde_json::from_str::<JsonrpcNotification>(&serialized_json_rpc_message).unwrap();
|
||||
assert_eq!(
|
||||
JsonrpcNotification {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
method: "conversation".to_string(),
|
||||
params: json!({
|
||||
"conversation_id": conversation_id.to_string(),
|
||||
"type": "initialized",
|
||||
"model": "gpt-5",
|
||||
}),
|
||||
},
|
||||
deserialized_json_rpc_message
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
message,
|
||||
from_jsonrpc_message(deserialized_json_rpc_message).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,9 @@ unicode-segmentation = "1.12.0"
|
||||
unicode-width = "0.1"
|
||||
uuid = "1"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
@@ -255,9 +255,11 @@ impl App<'_> {
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
} => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.on_ctrl_z();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
self.suspend(terminal)?;
|
||||
}
|
||||
// No-op on non-Unix platforms.
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('d'),
|
||||
@@ -459,6 +461,23 @@ impl App<'_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn suspend(&mut self, terminal: &mut tui::Tui) -> Result<()> {
|
||||
tui::restore()?;
|
||||
// SAFETY: Unix-only code path. We intentionally send SIGTSTP to the
|
||||
// current process group (pid 0) to trigger standard job-control
|
||||
// suspension semantics. This FFI does not involve any raw pointers,
|
||||
// is not called from a signal handler, and uses a constant signal.
|
||||
// Errors from kill are acceptable (e.g., if already stopped) — the
|
||||
// subsequent re-init path will still leave the terminal in a good state.
|
||||
// We considered `nix`, but didn't think it was worth pulling in for this one call.
|
||||
unsafe { libc::kill(0, libc::SIGTSTP) };
|
||||
*terminal = tui::init(&self.config)?;
|
||||
terminal.clear()?;
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn token_usage(&self) -> codex_core::protocol::TokenUsage {
|
||||
match &self.app_state {
|
||||
AppState::Chat { widget } => widget.token_usage().clone(),
|
||||
|
||||
@@ -652,10 +652,6 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_ctrl_z(&mut self) {
|
||||
self.interrupt_running_task();
|
||||
}
|
||||
|
||||
pub(crate) fn composer_is_empty(&self) -> bool {
|
||||
self.bottom_pane.composer_is_empty()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user