Compare commits

...

5 Commits

Author SHA1 Message Date
Michael Bolin
2c3d906f19 exploration: new protocol format
Here we explore an evolution of the internal protocol that can be mapped to JSON-RPC in a straightforward way.
2025-08-12 11:28:50 -07:00
pakrym-oai
e8670ad840 Support truststore when available and add tracing (#2232)
Supports minimal tracing and detection of working ssl cert.
2025-08-12 09:20:59 -07:00
Michael Bolin
596a9d6a96 fix: take ExecToolCallOutput by value to avoid clone() (#2197)
Since the output could be a large string, it seemed like a win to avoid
the `clone()` in the common case.
2025-08-12 08:59:35 -07:00
ae
320f150c68 fix: update ctrl-z to suspend tui (#2113)
- Lean on ctrl-c and esc to interrupt.
- (Only on unix.)

https://github.com/user-attachments/assets/7ce6c57f-6ee2-40c2-8cd2-b31265f16c1c
2025-08-12 05:03:58 +00:00
dependabot[bot]
7051a528a3 chore(deps-dev): bump @types/bun from 1.2.19 to 1.2.20 in /.github/actions/codex (#2163)
[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/bun&package-manager=bun&previous-version=1.2.19&new-version=1.2.20)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 21:54:30 -07:00
12 changed files with 264 additions and 24 deletions

View File

@@ -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=="],
}
}

View File

@@ -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
View File

@@ -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",

View File

@@ -15,6 +15,7 @@ members = [
"mcp-server",
"mcp-types",
"ollama",
"protocol",
"tui",
]
resolver = "2"

View File

@@ -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,
},
};

View File

@@ -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

View 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"

View File

@@ -0,0 +1 @@
mod notification;

View 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()
);
}
}

View File

@@ -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"] }

View File

@@ -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(),

View File

@@ -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()
}