Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 37 additions & 13 deletions codex-rs/app-server-protocol/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ use crate::export_server_responses;
use crate::protocol::common::EXPERIMENTAL_CLIENT_METHOD_PARAM_TYPES;
use crate::protocol::common::EXPERIMENTAL_CLIENT_METHOD_RESPONSE_TYPES;
use crate::protocol::common::EXPERIMENTAL_CLIENT_METHODS;
use crate::protocol::common::EXPERIMENTAL_SERVER_METHOD_PARAM_TYPES;
use crate::protocol::common::EXPERIMENTAL_SERVER_METHOD_RESPONSE_TYPES;
use crate::protocol::common::EXPERIMENTAL_SERVER_METHODS;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
Expand Down Expand Up @@ -249,10 +252,10 @@ fn filter_experimental_ts(out_dir: &Path) -> Result<()> {
let registered_fields = experimental_fields();
let experimental_method_types = experimental_method_types();
// Most generated TS files are filtered by schema processing, but
// `ClientRequest.ts` and any type with `#[experimental(...)]` fields need
// direct post-processing because they encode method/field information in
// file-local unions/interfaces.
filter_client_request_ts(out_dir, EXPERIMENTAL_CLIENT_METHODS)?;
// Request unions and types with `#[experimental(...)]` fields need direct
// post-processing because they encode method/field information locally.
filter_request_ts(out_dir, "ClientRequest.ts", EXPERIMENTAL_CLIENT_METHODS)?;
filter_request_ts(out_dir, "ServerRequest.ts", EXPERIMENTAL_SERVER_METHODS)?;
filter_experimental_type_fields_ts(out_dir, &registered_fields)?;
remove_generated_type_files(out_dir, &experimental_method_types, "ts")?;
Ok(())
Expand All @@ -261,10 +264,13 @@ fn filter_experimental_ts(out_dir: &Path) -> Result<()> {
pub(crate) fn filter_experimental_ts_tree(tree: &mut BTreeMap<PathBuf, String>) -> Result<()> {
let registered_fields = experimental_fields();
let experimental_method_types = experimental_method_types();
if let Some(content) = tree.get_mut(Path::new("ClientRequest.ts")) {
let filtered =
filter_client_request_ts_contents(std::mem::take(content), EXPERIMENTAL_CLIENT_METHODS);
*content = filtered;
for (file_name, experimental_methods) in [
("ClientRequest.ts", EXPERIMENTAL_CLIENT_METHODS),
("ServerRequest.ts", EXPERIMENTAL_SERVER_METHODS),
] {
if let Some(content) = tree.get_mut(Path::new(file_name)) {
*content = filter_request_ts_contents(std::mem::take(content), experimental_methods);
}
}

let mut fields_by_type_name: HashMap<String, HashSet<String>> = HashMap::new();
Expand Down Expand Up @@ -293,21 +299,21 @@ pub(crate) fn filter_experimental_ts_tree(tree: &mut BTreeMap<PathBuf, String>)
Ok(())
}

/// Removes union arms from `ClientRequest.ts` for methods marked experimental.
fn filter_client_request_ts(out_dir: &Path, experimental_methods: &[&str]) -> Result<()> {
let path = out_dir.join("ClientRequest.ts");
/// Removes union arms from a generated request type for methods marked experimental.
fn filter_request_ts(out_dir: &Path, file_name: &str, experimental_methods: &[&str]) -> Result<()> {
let path = out_dir.join(file_name);
if !path.exists() {
return Ok(());
}
let mut content =
fs::read_to_string(&path).with_context(|| format!("Failed to read {}", path.display()))?;
content = filter_client_request_ts_contents(content, experimental_methods);
content = filter_request_ts_contents(content, experimental_methods);

fs::write(&path, content).with_context(|| format!("Failed to write {}", path.display()))?;
Ok(())
}

fn filter_client_request_ts_contents(mut content: String, experimental_methods: &[&str]) -> String {
fn filter_request_ts_contents(mut content: String, experimental_methods: &[&str]) -> String {
let Some((prefix, body, suffix)) = split_type_alias(&content) else {
return content;
};
Expand Down Expand Up @@ -404,6 +410,7 @@ fn filter_experimental_schema(bundle: &mut Value) -> Result<()> {
filter_experimental_fields_in_root(bundle, &registered_fields);
filter_experimental_fields_in_definitions(bundle, &registered_fields);
prune_experimental_methods(bundle, EXPERIMENTAL_CLIENT_METHODS);
prune_experimental_methods(bundle, EXPERIMENTAL_SERVER_METHODS);
remove_experimental_method_type_definitions(bundle);
Ok(())
}
Expand Down Expand Up @@ -560,6 +567,8 @@ fn experimental_method_types() -> HashSet<String> {
collect_experimental_type_names(EXPERIMENTAL_CLIENT_METHOD_PARAM_TYPES, &mut type_names);
collect_experimental_type_names(EXPERIMENTAL_CLIENT_METHOD_RESPONSE_TYPES, &mut type_names);
collect_experimental_type_names(EXPERIMENTAL_CLIENT_METHOD_DEPENDENCY_TYPES, &mut type_names);
collect_experimental_type_names(EXPERIMENTAL_SERVER_METHOD_PARAM_TYPES, &mut type_names);
collect_experimental_type_names(EXPERIMENTAL_SERVER_METHOD_RESPONSE_TYPES, &mut type_names);
type_names
}

Expand Down Expand Up @@ -2118,6 +2127,13 @@ mod tests {
client_request_ts.contains("MockExperimentalMethodParams"),
false
);
let server_request_ts = std::str::from_utf8(
fixture_tree
.get(Path::new("ServerRequest.ts"))
.ok_or_else(|| anyhow::anyhow!("missing ServerRequest.ts fixture"))?,
)?;
assert_eq!(server_request_ts.contains("currentTime/read"), false);
assert_eq!(server_request_ts.contains("CurrentTimeReadParams"), false);
let typescript_index = std::str::from_utf8(
fixture_tree
.get(Path::new("index.ts"))
Expand All @@ -2138,6 +2154,14 @@ mod tests {
fixture_tree.contains_key(Path::new("v2/MockExperimentalMethodResponse.ts")),
false
);
assert_eq!(
fixture_tree.contains_key(Path::new("v2/CurrentTimeReadParams.ts")),
false
);
assert_eq!(
fixture_tree.contains_key(Path::new("v2/CurrentTimeReadResponse.ts")),
false
);
assert_eq!(
fixture_tree.contains_key(Path::new("v2/RemoteControlClient.ts")),
false
Expand Down
56 changes: 53 additions & 3 deletions codex-rs/app-server-protocol/src/protocol/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1184,7 +1184,8 @@ client_request_definitions! {
macro_rules! server_request_definitions {
(
$(
$(#[$variant_meta:meta])*
$(#[experimental($reason:expr)])?
$(#[doc = $variant_doc:literal])*
$variant:ident $(=> $wire:literal)? {
params: $params:ty,
response: $response:ty,
Expand All @@ -1197,7 +1198,7 @@ macro_rules! server_request_definitions {
#[serde(tag = "method", rename_all = "camelCase")]
pub enum ServerRequest {
$(
$(#[$variant_meta])*
$(#[doc = $variant_doc])*
$(#[serde(rename = $wire)] #[ts(rename = $wire)])?
$variant {
#[serde(rename = "id")]
Expand Down Expand Up @@ -1237,7 +1238,7 @@ macro_rules! server_request_definitions {
#[serde(tag = "method", rename_all = "camelCase")]
pub enum ServerResponse {
$(
$(#[$variant_meta])*
$(#[doc = $variant_doc])*
$(#[serde(rename = $wire)])?
$variant {
#[serde(rename = "id")]
Expand Down Expand Up @@ -1281,6 +1282,22 @@ macro_rules! server_request_definitions {
}
}

pub(crate) const EXPERIMENTAL_SERVER_METHODS: &[&str] = &[
$(
experimental_method_entry!($(#[experimental($reason)])? $(=> $wire)?),
)*
];
pub(crate) const EXPERIMENTAL_SERVER_METHOD_PARAM_TYPES: &[&str] = &[
$(
experimental_type_entry!($(#[experimental($reason)])? $params),
)*
];
pub(crate) const EXPERIMENTAL_SERVER_METHOD_RESPONSE_TYPES: &[&str] = &[
$(
experimental_type_entry!($(#[experimental($reason)])? $response),
)*
];

pub fn export_server_responses(
out_dir: &::std::path::Path,
) -> ::std::result::Result<(), ::ts_rs::ExportError> {
Expand Down Expand Up @@ -1470,6 +1487,13 @@ server_request_definitions! {
response: v2::AttestationGenerateResponse,
},

#[experimental("currentTime/read")]
/// Read the current time from an external clock owned by the client.
CurrentTimeRead => "currentTime/read" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we mark experimental?

params: v2::CurrentTimeReadParams,
response: v2::CurrentTimeReadResponse,
},

/// DEPRECATED APIs below
/// Request to approve a patch.
/// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).
Expand Down Expand Up @@ -2372,6 +2396,32 @@ mod tests {
Ok(())
}

#[test]
fn serialize_current_time_read_request() -> Result<()> {
let params = v2::CurrentTimeReadParams {
thread_id: "thread-123".to_string(),
};
let request = ServerRequest::CurrentTimeRead {
request_id: RequestId::Integer(10),
params: params.clone(),
};
assert_eq!(
json!({
"method": "currentTime/read",
"id": 10,
"params": {
"threadId": "thread-123"
}
}),
serde_json::to_value(&request)?,
);

let payload = ServerRequestPayload::CurrentTimeRead(params);
assert_eq!(request.id(), &RequestId::Integer(10));
assert_eq!(payload.request_with_id(RequestId::Integer(10)), request);
Ok(())
}

#[test]
fn serialize_server_response() -> Result<()> {
let response = ServerResponse::CommandExecutionRequestApproval {
Expand Down
20 changes: 20 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/current_time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CurrentTimeReadParams {
pub thread_id: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CurrentTimeReadResponse {
/// Current time as whole Unix seconds.
#[ts(type = "number")]
pub current_time_at: i64,
Comment thread
rka-oai marked this conversation as resolved.
}
2 changes: 2 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod attestation;
mod collaboration_mode;
mod command_exec;
mod config;
mod current_time;
mod environment;
mod experimental_feature;
mod feedback;
Expand All @@ -32,6 +33,7 @@ pub use attestation::*;
pub use collaboration_mode::*;
pub use command_exec::*;
pub use config::*;
pub use current_time::*;
pub use environment::*;
pub use experimental_feature::*;
pub use feedback::*;
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,10 @@ When the client responds to `item/tool/requestUserInput`, the server emits `serv

Desktop hosts that provide upstream attestation should set `capabilities.requestAttestation` during `initialize` and handle the server-initiated `attestation/generate` request. App-server issues it just in time before ChatGPT Codex requests that forward `x-oai-attestation`; the client responds with `{ "token": "v1.<opaque>" }`, where `token` is an opaque client-owned value. When app-server receives a client response, it forwards a consistent outer envelope such as `{ "v": 1, "s": 0, "t": "v1.<opaque>" }`, where `t` contains the client token unchanged. If app-server attempts attestation but fails within its own boundary, it sends the same envelope shape with an app-server status code and without `t` (`1 = timeout`, `2 = request failed`, `3 = request canceled`, `4 = malformed response`). If no initialized client opted into attestation, app-server omits `x-oai-attestation` for that upstream request.

### Current time

When `[features.current_time_reminder]` is enabled with `clock_source = "external"`, app-server sends the client subscribed to the thread an experimental `currentTime/read` request with `{ "threadId": "thr_123" }` when a time reminder is due. The client responds with `{ "currentTimeAt": 1781717655 }`, where `currentTimeAt` is an integer Unix timestamp in seconds. A failed, canceled, timed-out, or malformed response stops the turn before the model request is sent.

### MCP server elicitations

MCP servers can interrupt a turn and ask the client for structured input via `mcpServer/elicitation/request`.
Expand Down
Loading
Loading