SaylorTwift HF Staff commited on
Commit
afa0cbf
·
verified ·
1 Parent(s): 9ebf6d4

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. codex-rs/app-server-client/src/lib.rs +2136 -0
  2. codex-rs/app-server-client/src/path.rs +58 -0
  3. codex-rs/app-server-client/src/remote.rs +1088 -0
  4. codex-rs/app-server-protocol/schema/json/AttestationGenerateResponse.json +14 -0
  5. codex-rs/app-server-protocol/schema/json/ClientNotification.json +22 -0
  6. codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json +116 -0
  7. codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json +146 -0
  8. codex-rs/app-server-protocol/schema/json/FileChangeRequestApprovalResponse.json +47 -0
  9. codex-rs/app-server-protocol/schema/json/FuzzyFileSearchParams.json +26 -0
  10. codex-rs/app-server-protocol/schema/json/FuzzyFileSearchSessionUpdatedNotification.json +74 -0
  11. codex-rs/app-server-protocol/schema/json/JSONRPCError.json +48 -0
  12. codex-rs/app-server-protocol/schema/json/JSONRPCNotification.json +15 -0
  13. codex-rs/app-server-protocol/schema/json/JSONRPCRequest.json +60 -0
  14. codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestResponse.json +29 -0
  15. codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json +322 -0
  16. codex-rs/app-server-protocol/schema/json/ToolRequestUserInputResponse.json +34 -0
  17. codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +0 -0
  18. codex-rs/app-server-protocol/schema/typescript/AbsolutePathBuf.ts +14 -0
  19. codex-rs/app-server-protocol/schema/typescript/AgentMessageInputContent.ts +5 -0
  20. codex-rs/app-server-protocol/schema/typescript/AgentPath.ts +5 -0
  21. codex-rs/app-server-protocol/schema/typescript/ApplyPatchApprovalParams.ts +21 -0
  22. codex-rs/app-server-protocol/schema/typescript/ApplyPatchApprovalResponse.ts +6 -0
  23. codex-rs/app-server-protocol/schema/typescript/AuthMode.ts +8 -0
  24. codex-rs/app-server-protocol/schema/typescript/AutoCompactTokenLimitScope.ts +9 -0
  25. codex-rs/app-server-protocol/schema/typescript/ClientInfo.ts +5 -0
  26. codex-rs/app-server-protocol/schema/typescript/ClientNotification.ts +5 -0
  27. codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +107 -0
  28. codex-rs/app-server-protocol/schema/typescript/CodexResponseHandoffMode.ts +5 -0
  29. codex-rs/app-server-protocol/schema/typescript/CollaborationMode.ts +10 -0
  30. codex-rs/app-server-protocol/schema/typescript/ConfigurationReasoning.ts +9 -0
  31. codex-rs/app-server-protocol/schema/typescript/ContentItem.ts +6 -0
  32. codex-rs/app-server-protocol/schema/typescript/ConversationGitInfo.ts +5 -0
  33. codex-rs/app-server-protocol/schema/typescript/ConversationSummary.ts +8 -0
  34. codex-rs/app-server-protocol/schema/typescript/ConversationTextRole.ts +5 -0
  35. codex-rs/app-server-protocol/schema/typescript/ExecCommandApprovalParams.ts +16 -0
  36. codex-rs/app-server-protocol/schema/typescript/ExecCommandApprovalResponse.ts +6 -0
  37. codex-rs/app-server-protocol/schema/typescript/ExecPolicyAmendment.ts +12 -0
  38. codex-rs/app-server-protocol/schema/typescript/FileChange.ts +5 -0
  39. codex-rs/app-server-protocol/schema/typescript/ForcedLoginMethod.ts +5 -0
  40. codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputBody.ts +6 -0
  41. codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputContentItem.ts +10 -0
  42. codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchMatchType.ts +5 -0
  43. codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchParams.ts +5 -0
  44. codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResponse.ts +6 -0
  45. codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResult.ts +9 -0
  46. codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchSessionCompletedNotification.ts +5 -0
  47. codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchSessionUpdatedNotification.ts +6 -0
  48. codex-rs/app-server-protocol/schema/typescript/GetAuthStatusParams.ts +5 -0
  49. codex-rs/app-server-protocol/schema/typescript/GetAuthStatusResponse.ts +6 -0
  50. codex-rs/app-server-protocol/schema/typescript/GetConversationSummaryParams.ts +6 -0
codex-rs/app-server-client/src/lib.rs ADDED
@@ -0,0 +1,2136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Shared in-process app-server client facade for CLI surfaces.
2
+ //!
3
+ //! This crate wraps [`codex_app_server::in_process`] behind a single async API
4
+ //! used by surfaces like TUI and exec. It centralizes:
5
+ //!
6
+ //! - Runtime startup and initialize-capabilities handshake.
7
+ //! - Typed caller-provided startup identity (`SessionSource` + client name).
8
+ //! - Typed and raw request/notification dispatch.
9
+ //! - Server request resolution and rejection.
10
+ //! - Ordered, lossless event consumption that cannot block request processing.
11
+ //! - Bounded graceful shutdown with abort fallback.
12
+ //!
13
+ //! The facade interposes a worker task between the caller and the underlying
14
+ //! [`InProcessClientHandle`](codex_app_server::in_process::InProcessClientHandle),
15
+ //! bridging async `mpsc` channels on both sides. Commands and the underlying
16
+ //! runtime remain bounded; the local consumer event queue is unbounded so
17
+ //! unread notifications cannot prevent request responses from being delivered.
18
+
19
+ mod path;
20
+ mod remote;
21
+
22
+ use std::error::Error;
23
+ use std::fmt;
24
+ use std::io::Error as IoError;
25
+ use std::io::ErrorKind;
26
+ use std::io::Result as IoResult;
27
+ use std::sync::Arc;
28
+ use std::time::Duration;
29
+
30
+ pub use codex_app_server::app_server_control_socket_path;
31
+ pub use codex_app_server::in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY;
32
+ pub use codex_app_server::in_process::InProcessServerEvent;
33
+ use codex_app_server::in_process::InProcessStartArgs;
34
+ use codex_app_server::in_process::LogDbLayer;
35
+ pub use codex_app_server::in_process::StateDbHandle;
36
+ use codex_app_server_protocol::ClientInfo;
37
+ use codex_app_server_protocol::ClientNotification;
38
+ use codex_app_server_protocol::ClientRequest;
39
+ use codex_app_server_protocol::ConfigWarningNotification;
40
+ use codex_app_server_protocol::InitializeCapabilities;
41
+ use codex_app_server_protocol::InitializeParams;
42
+ use codex_app_server_protocol::JSONRPCErrorError;
43
+ use codex_app_server_protocol::RequestId;
44
+ use codex_app_server_protocol::Result as JsonRpcResult;
45
+ use codex_app_server_protocol::ServerNotification;
46
+ use codex_app_server_protocol::ServerRequest;
47
+ use codex_arg0::Arg0DispatchPaths;
48
+ use codex_config::CloudConfigBundleLoader;
49
+ use codex_config::LoaderOverrides;
50
+ use codex_config::NoopThreadConfigLoader;
51
+ use codex_core::config::Config;
52
+ pub use codex_core::otel_init::build_provider as build_otel_provider;
53
+ pub use codex_exec_server::EnvironmentManager;
54
+ pub use codex_exec_server::ExecServerRuntimePaths;
55
+ use codex_feedback::CodexFeedback;
56
+ use codex_protocol::protocol::SessionSource;
57
+ use codex_utils_absolute_path::AbsolutePathBuf;
58
+ use serde::de::DeserializeOwned;
59
+ use tokio::sync::mpsc;
60
+ use tokio::sync::oneshot;
61
+ use tokio::time::timeout;
62
+ use toml::Value as TomlValue;
63
+ use tracing::warn;
64
+
65
+ pub use crate::path::AppServerPath;
66
+ pub use crate::remote::RemoteAppServerClient;
67
+ pub use crate::remote::RemoteAppServerConnectArgs;
68
+ pub use crate::remote::RemoteAppServerEndpoint;
69
+
70
+ /// Transitional access to core-only embedded app-server types.
71
+ ///
72
+ /// New TUI behavior should prefer the app-server protocol methods. This
73
+ /// module exists so clients can remove a direct `codex-core` dependency
74
+ /// while legacy startup/config paths are migrated to RPCs.
75
+ pub mod legacy_core {
76
+ pub mod config {
77
+ pub use codex_core::config::*;
78
+
79
+ pub mod edit {
80
+ pub use codex_core::config::edit::*;
81
+ }
82
+ }
83
+ }
84
+
85
+ const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
86
+ // Covers the embedded drain, its analytics flush, and final task join.
87
+ const IN_PROCESS_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(45);
88
+
89
+ /// Raw app-server request result for typed in-process requests.
90
+ ///
91
+ /// Even on the in-process path, successful responses still travel back through
92
+ /// the same JSON-RPC result envelope used by socket/stdio transports because
93
+ /// `MessageProcessor` continues to produce that shape internally.
94
+ pub type RequestResult = std::result::Result<JsonRpcResult, JSONRPCErrorError>;
95
+
96
+ #[derive(Debug, Clone)]
97
+ pub enum AppServerEvent {
98
+ Lagged { skipped: usize },
99
+ ServerNotification(Box<ServerNotification>),
100
+ ServerRequest(Box<ServerRequest>),
101
+ Disconnected { message: String },
102
+ }
103
+
104
+ impl From<InProcessServerEvent> for AppServerEvent {
105
+ fn from(value: InProcessServerEvent) -> Self {
106
+ match value {
107
+ InProcessServerEvent::Lagged { skipped } => Self::Lagged { skipped },
108
+ InProcessServerEvent::ServerNotification(notification) => {
109
+ Self::ServerNotification(notification)
110
+ }
111
+ InProcessServerEvent::ServerRequest(request) => Self::ServerRequest(request),
112
+ }
113
+ }
114
+ }
115
+
116
+ /// Layered error for [`InProcessAppServerClient::request_typed`].
117
+ ///
118
+ /// This keeps transport failures, server-side JSON-RPC failures, and response
119
+ /// decode failures distinct so callers can decide whether to retry, surface a
120
+ /// server error, or treat the response as an internal request/response mismatch.
121
+ #[derive(Debug)]
122
+ pub enum TypedRequestError {
123
+ Transport {
124
+ method: String,
125
+ source: IoError,
126
+ },
127
+ Server {
128
+ method: String,
129
+ source: JSONRPCErrorError,
130
+ },
131
+ Deserialize {
132
+ method: String,
133
+ source: serde_json::Error,
134
+ },
135
+ }
136
+
137
+ impl fmt::Display for TypedRequestError {
138
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139
+ match self {
140
+ Self::Transport { method, source } => {
141
+ write!(f, "{method} transport error: {source}")
142
+ }
143
+ Self::Server { method, source } => {
144
+ write!(
145
+ f,
146
+ "{method} failed: {} (code {})",
147
+ source.message, source.code
148
+ )?;
149
+ if let Some(data) = source.data.as_ref() {
150
+ write!(f, ", data: {data}")?;
151
+ }
152
+ Ok(())
153
+ }
154
+ Self::Deserialize { method, source } => {
155
+ write!(f, "{method} response decode error: {source}")
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ impl Error for TypedRequestError {
162
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
163
+ match self {
164
+ Self::Transport { source, .. } => Some(source),
165
+ Self::Server { .. } => None,
166
+ Self::Deserialize { source, .. } => Some(source),
167
+ }
168
+ }
169
+ }
170
+
171
+ #[derive(Clone)]
172
+ pub struct InProcessClientStartArgs {
173
+ /// Resolved argv0 dispatch paths used by command execution internals.
174
+ pub arg0_paths: Arg0DispatchPaths,
175
+ /// Shared config used to initialize app-server runtime.
176
+ pub config: Arc<Config>,
177
+ /// CLI config overrides that are already parsed into TOML values.
178
+ pub cli_overrides: Vec<(String, TomlValue)>,
179
+ /// Loader override knobs used by config API paths.
180
+ pub loader_overrides: LoaderOverrides,
181
+ /// Whether config API paths should reject unknown config fields.
182
+ pub strict_config: bool,
183
+ /// Preloaded cloud config bundle provider.
184
+ pub cloud_config_bundle: CloudConfigBundleLoader,
185
+ /// Feedback sink used by app-server/core telemetry and logs.
186
+ pub feedback: CodexFeedback,
187
+ /// SQLite tracing layer used to flush recently emitted logs before feedback upload.
188
+ pub log_db: Option<LogDbLayer>,
189
+ /// Process-wide SQLite state handle shared with the embedded app-server.
190
+ pub state_db: Option<StateDbHandle>,
191
+ /// Environment manager used by core execution and filesystem operations.
192
+ pub environment_manager: Arc<EnvironmentManager>,
193
+ /// Startup warnings emitted after initialize succeeds.
194
+ pub config_warnings: Vec<ConfigWarningNotification>,
195
+ /// Session source recorded in app-server thread metadata.
196
+ pub session_source: SessionSource,
197
+ /// Whether auth loading should honor the `CODEX_API_KEY` environment variable.
198
+ pub enable_codex_api_key_env: bool,
199
+ /// Client name reported during initialize.
200
+ pub client_name: String,
201
+ /// Client version reported during initialize.
202
+ pub client_version: String,
203
+ /// Whether experimental APIs are requested at initialize time.
204
+ pub experimental_api: bool,
205
+ /// Whether MCP servers may send `openai/form` elicitation requests.
206
+ pub mcp_server_openai_form_elicitation: bool,
207
+ /// Notification methods this client opts out of receiving.
208
+ pub opt_out_notification_methods: Vec<String>,
209
+ /// Queue capacity for command and embedded-runtime channels (clamped to at least 1).
210
+ pub channel_capacity: usize,
211
+ }
212
+
213
+ impl InProcessClientStartArgs {
214
+ /// Builds initialize params from caller-provided metadata.
215
+ pub fn initialize_params(&self) -> InitializeParams {
216
+ let capabilities = InitializeCapabilities {
217
+ experimental_api: self.experimental_api,
218
+ request_attestation: false,
219
+ extensions: None,
220
+ opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() {
221
+ None
222
+ } else {
223
+ Some(self.opt_out_notification_methods.clone())
224
+ },
225
+ mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation,
226
+ };
227
+
228
+ InitializeParams {
229
+ client_info: ClientInfo {
230
+ name: self.client_name.clone(),
231
+ title: None,
232
+ version: self.client_version.clone(),
233
+ },
234
+ capabilities: Some(capabilities),
235
+ }
236
+ }
237
+
238
+ fn into_runtime_start_args(self) -> InProcessStartArgs {
239
+ let initialize = self.initialize_params();
240
+ InProcessStartArgs {
241
+ arg0_paths: self.arg0_paths,
242
+ config: self.config,
243
+ cli_overrides: self.cli_overrides,
244
+ loader_overrides: self.loader_overrides,
245
+ strict_config: self.strict_config,
246
+ cloud_config_bundle: self.cloud_config_bundle,
247
+ thread_config_loader: Arc::new(NoopThreadConfigLoader),
248
+ feedback: self.feedback,
249
+ log_db: self.log_db,
250
+ state_db: self.state_db,
251
+ environment_manager: self.environment_manager,
252
+ config_warnings: self.config_warnings,
253
+ session_source: self.session_source,
254
+ enable_codex_api_key_env: self.enable_codex_api_key_env,
255
+ initialize,
256
+ channel_capacity: self.channel_capacity,
257
+ }
258
+ }
259
+ }
260
+
261
+ /// Internal command sent from public facade methods to the worker task.
262
+ ///
263
+ /// Each variant carries a oneshot sender so the caller can `await` the
264
+ /// result without holding a mutable reference to the client.
265
+ enum ClientCommand {
266
+ Request {
267
+ request: Box<ClientRequest>,
268
+ response_tx: oneshot::Sender<IoResult<RequestResult>>,
269
+ },
270
+ Notify {
271
+ notification: ClientNotification,
272
+ response_tx: oneshot::Sender<IoResult<()>>,
273
+ },
274
+ ResolveServerRequest {
275
+ request_id: RequestId,
276
+ result: JsonRpcResult,
277
+ response_tx: oneshot::Sender<IoResult<()>>,
278
+ },
279
+ RejectServerRequest {
280
+ request_id: RequestId,
281
+ error: JSONRPCErrorError,
282
+ response_tx: oneshot::Sender<IoResult<()>>,
283
+ },
284
+ Shutdown {
285
+ response_tx: oneshot::Sender<IoResult<()>>,
286
+ },
287
+ }
288
+
289
+ /// Async facade over the in-process app-server runtime.
290
+ ///
291
+ /// This type owns a worker task that bridges between:
292
+ /// - caller-facing async `mpsc` channels used by TUI/exec
293
+ /// - [`codex_app_server::in_process::InProcessClientHandle`], which speaks to
294
+ /// the embedded `MessageProcessor`
295
+ ///
296
+ /// The facade intentionally preserves the server's request/notification/event
297
+ /// model instead of exposing direct core runtime handles. That keeps in-process
298
+ /// callers aligned with app-server behavior while still avoiding a process
299
+ /// boundary.
300
+ pub struct InProcessAppServerClient {
301
+ command_tx: mpsc::Sender<ClientCommand>,
302
+ event_rx: mpsc::UnboundedReceiver<InProcessServerEvent>,
303
+ worker_handle: tokio::task::JoinHandle<()>,
304
+ }
305
+
306
+ #[derive(Clone)]
307
+ pub struct InProcessAppServerRequestHandle {
308
+ command_tx: mpsc::Sender<ClientCommand>,
309
+ }
310
+
311
+ #[derive(Clone)]
312
+ pub enum AppServerRequestHandle {
313
+ InProcess(InProcessAppServerRequestHandle),
314
+ Remote(crate::remote::RemoteAppServerRequestHandle),
315
+ }
316
+
317
+ pub enum AppServerClient {
318
+ InProcess(InProcessAppServerClient),
319
+ Remote(RemoteAppServerClient),
320
+ }
321
+
322
+ impl InProcessAppServerClient {
323
+ /// Starts the in-process runtime and facade worker task.
324
+ ///
325
+ /// The returned client is ready for requests and ordered event consumption.
326
+ /// Request queues remain bounded without blocking on unread notifications.
327
+ pub async fn start(args: InProcessClientStartArgs) -> IoResult<Self> {
328
+ let channel_capacity = args.channel_capacity.max(1);
329
+ let mut handle =
330
+ codex_app_server::in_process::start(args.into_runtime_start_args()).await?;
331
+ let request_sender = handle.sender();
332
+ let (command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(channel_capacity);
333
+ // e9996ec62a preserved transcript events by awaiting a bounded queue, but that can
334
+ // deadlock a foreground request whose response is behind unread notifications.
335
+ // Match the remote-client fix in 79ea57715636: only this local consumer queue is
336
+ // unbounded; commands and the embedded runtime stay bounded and events remain ordered.
337
+ let (event_tx, event_rx) = mpsc::unbounded_channel::<InProcessServerEvent>();
338
+
339
+ let worker_handle = tokio::spawn(async move {
340
+ let mut event_stream_enabled = true;
341
+ loop {
342
+ tokio::select! {
343
+ command = command_rx.recv() => {
344
+ match command {
345
+ Some(ClientCommand::Request { request, response_tx }) => {
346
+ let request_sender = request_sender.clone();
347
+ // Request waits happen on a detached task so
348
+ // this loop can keep draining runtime events
349
+ // while the request is blocked on client input.
350
+ tokio::spawn(async move {
351
+ // Device ceremonies belong to the waiting UI. Preserve
352
+ // its cancellation through this buffering task.
353
+ let cancellable = matches!(*request,
354
+ ClientRequest::UserVerificationStatus { .. }
355
+ | ClientRequest::UserVerificationEnroll { .. }
356
+ | ClientRequest::UserVerificationDelete { .. }
357
+ | ClientRequest::UserVerificationVerify { .. });
358
+ let mut response_tx = response_tx;
359
+ tokio::select! {
360
+ _ = response_tx.closed(), if cancellable => {}
361
+ result = request_sender.request(*request) => {
362
+ let _ = response_tx.send(result);
363
+ }
364
+ }
365
+ });
366
+ }
367
+ Some(ClientCommand::Notify {
368
+ notification,
369
+ response_tx,
370
+ }) => {
371
+ let result = request_sender.notify(notification);
372
+ let _ = response_tx.send(result);
373
+ }
374
+ Some(ClientCommand::ResolveServerRequest {
375
+ request_id,
376
+ result,
377
+ response_tx,
378
+ }) => {
379
+ let send_result =
380
+ request_sender.respond_to_server_request(request_id, result);
381
+ let _ = response_tx.send(send_result);
382
+ }
383
+ Some(ClientCommand::RejectServerRequest {
384
+ request_id,
385
+ error,
386
+ response_tx,
387
+ }) => {
388
+ let send_result = request_sender.fail_server_request(request_id, error);
389
+ let _ = response_tx.send(send_result);
390
+ }
391
+ Some(ClientCommand::Shutdown { response_tx }) => {
392
+ let shutdown_result = handle.shutdown().await;
393
+ let _ = response_tx.send(shutdown_result);
394
+ break;
395
+ }
396
+ None => {
397
+ let _ = handle.shutdown().await;
398
+ break;
399
+ }
400
+ }
401
+ }
402
+ event = handle.next_event(), if event_stream_enabled => {
403
+ let Some(event) = event else {
404
+ break;
405
+ };
406
+ if let InProcessServerEvent::ServerRequest(request) = &event
407
+ && let ServerRequest::ChatgptAuthTokensRefresh { request_id, .. } =
408
+ request.as_ref()
409
+ {
410
+ let send_result = request_sender.fail_server_request(
411
+ request_id.clone(),
412
+ JSONRPCErrorError {
413
+ code: -32000,
414
+ message: "chatgpt auth token refresh is not supported for in-process app-server clients".to_string(),
415
+ data: None,
416
+ },
417
+ );
418
+ if let Err(err) = send_result {
419
+ warn!(
420
+ "failed to reject unsupported chatgpt auth token refresh request: {err}"
421
+ );
422
+ }
423
+ continue;
424
+ }
425
+
426
+ if event_tx.send(event).is_err() {
427
+ event_stream_enabled = false;
428
+ }
429
+ }
430
+ }
431
+ }
432
+ });
433
+
434
+ Ok(Self {
435
+ command_tx,
436
+ event_rx,
437
+ worker_handle,
438
+ })
439
+ }
440
+
441
+ pub fn request_handle(&self) -> InProcessAppServerRequestHandle {
442
+ InProcessAppServerRequestHandle {
443
+ command_tx: self.command_tx.clone(),
444
+ }
445
+ }
446
+
447
+ /// Sends a typed client request and returns raw JSON-RPC result.
448
+ ///
449
+ /// Callers that expect a concrete response type should usually prefer
450
+ /// [`request_typed`](Self::request_typed).
451
+ pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
452
+ let (response_tx, response_rx) = oneshot::channel();
453
+ self.command_tx
454
+ .send(ClientCommand::Request {
455
+ request: Box::new(request),
456
+ response_tx,
457
+ })
458
+ .await
459
+ .map_err(|_| {
460
+ IoError::new(
461
+ ErrorKind::BrokenPipe,
462
+ "in-process app-server worker channel is closed",
463
+ )
464
+ })?;
465
+ response_rx.await.map_err(|_| {
466
+ IoError::new(
467
+ ErrorKind::BrokenPipe,
468
+ "in-process app-server request channel is closed",
469
+ )
470
+ })?
471
+ }
472
+
473
+ /// Sends a typed client request and decodes the successful response body.
474
+ ///
475
+ /// This still deserializes from a JSON value produced by app-server's
476
+ /// JSON-RPC result envelope. Because the caller chooses `T`, `Deserialize`
477
+ /// failures indicate an internal request/response mismatch at the call site
478
+ /// (or an in-process bug), not transport skew from an external client.
479
+ pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
480
+ where
481
+ T: DeserializeOwned,
482
+ {
483
+ let method = request.method_name();
484
+ let response =
485
+ self.request(request)
486
+ .await
487
+ .map_err(|source| TypedRequestError::Transport {
488
+ method: method.to_string(),
489
+ source,
490
+ })?;
491
+ let result = response.map_err(|source| TypedRequestError::Server {
492
+ method: method.to_string(),
493
+ source,
494
+ })?;
495
+ serde_json::from_value(result).map_err(|source| TypedRequestError::Deserialize {
496
+ method: method.to_string(),
497
+ source,
498
+ })
499
+ }
500
+
501
+ /// Sends a typed client notification.
502
+ pub async fn notify(&self, notification: ClientNotification) -> IoResult<()> {
503
+ let (response_tx, response_rx) = oneshot::channel();
504
+ self.command_tx
505
+ .send(ClientCommand::Notify {
506
+ notification,
507
+ response_tx,
508
+ })
509
+ .await
510
+ .map_err(|_| {
511
+ IoError::new(
512
+ ErrorKind::BrokenPipe,
513
+ "in-process app-server worker channel is closed",
514
+ )
515
+ })?;
516
+ response_rx.await.map_err(|_| {
517
+ IoError::new(
518
+ ErrorKind::BrokenPipe,
519
+ "in-process app-server notify channel is closed",
520
+ )
521
+ })?
522
+ }
523
+
524
+ /// Resolves a pending server request.
525
+ ///
526
+ /// This should only be called with request IDs obtained from the current
527
+ /// client's event stream.
528
+ pub async fn resolve_server_request(
529
+ &self,
530
+ request_id: RequestId,
531
+ result: JsonRpcResult,
532
+ ) -> IoResult<()> {
533
+ let (response_tx, response_rx) = oneshot::channel();
534
+ self.command_tx
535
+ .send(ClientCommand::ResolveServerRequest {
536
+ request_id,
537
+ result,
538
+ response_tx,
539
+ })
540
+ .await
541
+ .map_err(|_| {
542
+ IoError::new(
543
+ ErrorKind::BrokenPipe,
544
+ "in-process app-server worker channel is closed",
545
+ )
546
+ })?;
547
+ response_rx.await.map_err(|_| {
548
+ IoError::new(
549
+ ErrorKind::BrokenPipe,
550
+ "in-process app-server resolve channel is closed",
551
+ )
552
+ })?
553
+ }
554
+
555
+ /// Rejects a pending server request with JSON-RPC error payload.
556
+ pub async fn reject_server_request(
557
+ &self,
558
+ request_id: RequestId,
559
+ error: JSONRPCErrorError,
560
+ ) -> IoResult<()> {
561
+ let (response_tx, response_rx) = oneshot::channel();
562
+ self.command_tx
563
+ .send(ClientCommand::RejectServerRequest {
564
+ request_id,
565
+ error,
566
+ response_tx,
567
+ })
568
+ .await
569
+ .map_err(|_| {
570
+ IoError::new(
571
+ ErrorKind::BrokenPipe,
572
+ "in-process app-server worker channel is closed",
573
+ )
574
+ })?;
575
+ response_rx.await.map_err(|_| {
576
+ IoError::new(
577
+ ErrorKind::BrokenPipe,
578
+ "in-process app-server reject channel is closed",
579
+ )
580
+ })?
581
+ }
582
+
583
+ /// Returns the next in-process event, or `None` when worker exits.
584
+ ///
585
+ /// Events remain ordered and are retained while callers await requests.
586
+ pub async fn next_event(&mut self) -> Option<InProcessServerEvent> {
587
+ self.event_rx.recv().await
588
+ }
589
+
590
+ /// Shuts down worker and in-process runtime with bounded wait.
591
+ ///
592
+ /// If graceful shutdown exceeds timeout, the worker task is aborted to
593
+ /// avoid leaking background tasks in embedding callers.
594
+ pub async fn shutdown(self) -> IoResult<()> {
595
+ let Self {
596
+ command_tx,
597
+ event_rx,
598
+ worker_handle,
599
+ } = self;
600
+ let mut worker_handle = worker_handle;
601
+ // Stop forwarding caller-facing events before asking the worker to shut down.
602
+ drop(event_rx);
603
+ let (response_tx, response_rx) = oneshot::channel();
604
+ if command_tx
605
+ .send(ClientCommand::Shutdown { response_tx })
606
+ .await
607
+ .is_ok()
608
+ && let Ok(command_result) = timeout(IN_PROCESS_SHUTDOWN_TIMEOUT, response_rx).await
609
+ {
610
+ command_result.map_err(|_| {
611
+ IoError::new(
612
+ ErrorKind::BrokenPipe,
613
+ "in-process app-server shutdown channel is closed",
614
+ )
615
+ })??;
616
+ }
617
+
618
+ if let Err(_elapsed) = timeout(IN_PROCESS_SHUTDOWN_TIMEOUT, &mut worker_handle).await {
619
+ worker_handle.abort();
620
+ let _ = worker_handle.await;
621
+ }
622
+ Ok(())
623
+ }
624
+ }
625
+
626
+ impl InProcessAppServerRequestHandle {
627
+ pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
628
+ let (response_tx, response_rx) = oneshot::channel();
629
+ self.command_tx
630
+ .send(ClientCommand::Request {
631
+ request: Box::new(request),
632
+ response_tx,
633
+ })
634
+ .await
635
+ .map_err(|_| {
636
+ IoError::new(
637
+ ErrorKind::BrokenPipe,
638
+ "in-process app-server worker channel is closed",
639
+ )
640
+ })?;
641
+ response_rx.await.map_err(|_| {
642
+ IoError::new(
643
+ ErrorKind::BrokenPipe,
644
+ "in-process app-server request channel is closed",
645
+ )
646
+ })?
647
+ }
648
+
649
+ pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
650
+ where
651
+ T: DeserializeOwned,
652
+ {
653
+ let method = request.method_name();
654
+ let response =
655
+ self.request(request)
656
+ .await
657
+ .map_err(|source| TypedRequestError::Transport {
658
+ method: method.to_string(),
659
+ source,
660
+ })?;
661
+ let result = response.map_err(|source| TypedRequestError::Server {
662
+ method: method.to_string(),
663
+ source,
664
+ })?;
665
+ serde_json::from_value(result).map_err(|source| TypedRequestError::Deserialize {
666
+ method: method.to_string(),
667
+ source,
668
+ })
669
+ }
670
+ }
671
+
672
+ impl AppServerRequestHandle {
673
+ pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
674
+ match self {
675
+ Self::InProcess(handle) => handle.request(request).await,
676
+ Self::Remote(handle) => handle.request(request).await,
677
+ }
678
+ }
679
+
680
+ pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
681
+ where
682
+ T: DeserializeOwned,
683
+ {
684
+ match self {
685
+ Self::InProcess(handle) => handle.request_typed(request).await,
686
+ Self::Remote(handle) => handle.request_typed(request).await,
687
+ }
688
+ }
689
+ }
690
+
691
+ impl AppServerClient {
692
+ /// App-server platform family, which can differ from the executor's platform.
693
+ /// Older remote servers may omit this metadata.
694
+ pub fn platform_family(&self) -> Option<&str> {
695
+ match self {
696
+ Self::InProcess(_) => Some(std::env::consts::FAMILY),
697
+ Self::Remote(client) => client.platform_family(),
698
+ }
699
+ }
700
+
701
+ /// App-server operating system as reported at initialization, including unknown values.
702
+ pub fn platform_os(&self) -> Option<&str> {
703
+ match self {
704
+ Self::InProcess(_) => Some(std::env::consts::OS),
705
+ Self::Remote(client) => client.platform_os(),
706
+ }
707
+ }
708
+
709
+ pub fn codex_home(&self, local_codex_home: &AbsolutePathBuf) -> Option<AppServerPath> {
710
+ match self {
711
+ Self::InProcess(_) => Some(AppServerPath::from_app_server(
712
+ local_codex_home.display().to_string(),
713
+ )),
714
+ Self::Remote(client) => client.codex_home().map(AppServerPath::from_app_server),
715
+ }
716
+ }
717
+
718
+ pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
719
+ match self {
720
+ Self::InProcess(client) => client.request(request).await,
721
+ Self::Remote(client) => client.request(request).await,
722
+ }
723
+ }
724
+
725
+ pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
726
+ where
727
+ T: DeserializeOwned,
728
+ {
729
+ match self {
730
+ Self::InProcess(client) => client.request_typed(request).await,
731
+ Self::Remote(client) => client.request_typed(request).await,
732
+ }
733
+ }
734
+
735
+ pub async fn notify(&self, notification: ClientNotification) -> IoResult<()> {
736
+ match self {
737
+ Self::InProcess(client) => client.notify(notification).await,
738
+ Self::Remote(client) => client.notify(notification).await,
739
+ }
740
+ }
741
+
742
+ pub async fn resolve_server_request(
743
+ &self,
744
+ request_id: RequestId,
745
+ result: JsonRpcResult,
746
+ ) -> IoResult<()> {
747
+ match self {
748
+ Self::InProcess(client) => client.resolve_server_request(request_id, result).await,
749
+ Self::Remote(client) => client.resolve_server_request(request_id, result).await,
750
+ }
751
+ }
752
+
753
+ pub async fn reject_server_request(
754
+ &self,
755
+ request_id: RequestId,
756
+ error: JSONRPCErrorError,
757
+ ) -> IoResult<()> {
758
+ match self {
759
+ Self::InProcess(client) => client.reject_server_request(request_id, error).await,
760
+ Self::Remote(client) => client.reject_server_request(request_id, error).await,
761
+ }
762
+ }
763
+
764
+ pub async fn next_event(&mut self) -> Option<AppServerEvent> {
765
+ match self {
766
+ Self::InProcess(client) => client.next_event().await.map(Into::into),
767
+ Self::Remote(client) => client.next_event().await,
768
+ }
769
+ }
770
+
771
+ pub async fn shutdown(self) -> IoResult<()> {
772
+ match self {
773
+ Self::InProcess(client) => client.shutdown().await,
774
+ Self::Remote(client) => client.shutdown().await,
775
+ }
776
+ }
777
+
778
+ pub fn request_handle(&self) -> AppServerRequestHandle {
779
+ match self {
780
+ Self::InProcess(client) => AppServerRequestHandle::InProcess(client.request_handle()),
781
+ Self::Remote(client) => AppServerRequestHandle::Remote(client.request_handle()),
782
+ }
783
+ }
784
+ }
785
+
786
+ #[cfg(test)]
787
+ mod tests {
788
+ use super::*;
789
+ use codex_app_server_protocol::AccountUpdatedNotification;
790
+ use codex_app_server_protocol::ConfigRequirementsReadResponse;
791
+ use codex_app_server_protocol::GetAccountResponse;
792
+ use codex_app_server_protocol::JSONRPCMessage;
793
+ use codex_app_server_protocol::JSONRPCRequest;
794
+ use codex_app_server_protocol::JSONRPCResponse;
795
+ use codex_app_server_protocol::ServerNotification;
796
+ use codex_app_server_protocol::SessionSource as ApiSessionSource;
797
+ use codex_app_server_protocol::ThreadSettingsUpdateParams;
798
+ use codex_app_server_protocol::ThreadSettingsUpdateResponse;
799
+ use codex_app_server_protocol::ThreadStartParams;
800
+ use codex_app_server_protocol::ThreadStartResponse;
801
+ use codex_app_server_protocol::ToolRequestUserInputParams;
802
+ use codex_app_server_protocol::ToolRequestUserInputQuestion;
803
+ use codex_core::config::ConfigBuilder;
804
+ use codex_core::init_state_db;
805
+ use codex_protocol::config_types::Personality;
806
+ use codex_uds::UnixListener;
807
+ use codex_utils_absolute_path::AbsolutePathBuf;
808
+ use futures::SinkExt;
809
+ use futures::StreamExt;
810
+ use pretty_assertions::assert_eq;
811
+ use std::ops::Deref;
812
+ use std::path::Path;
813
+ use tempfile::TempDir;
814
+ use tokio::net::TcpListener;
815
+ use tokio::time::Duration;
816
+ use tokio::time::timeout;
817
+ use tokio_tungstenite::accept_async;
818
+ use tokio_tungstenite::accept_hdr_async;
819
+ use tokio_tungstenite::tungstenite::Message;
820
+ use tokio_tungstenite::tungstenite::handshake::server::Request as WebSocketRequest;
821
+ use tokio_tungstenite::tungstenite::handshake::server::Response as WebSocketResponse;
822
+ use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
823
+
824
+ async fn build_test_config() -> Config {
825
+ match ConfigBuilder::default().build().await {
826
+ Ok(config) => config,
827
+ Err(_) => Config::load_default_with_cli_overrides(Vec::new())
828
+ .await
829
+ .expect("default config should load"),
830
+ }
831
+ }
832
+
833
+ async fn build_test_config_for_codex_home(codex_home: &Path) -> Config {
834
+ match ConfigBuilder::default()
835
+ .codex_home(codex_home.to_path_buf())
836
+ .build()
837
+ .await
838
+ {
839
+ Ok(config) => config,
840
+ Err(_) => Config::load_default_with_cli_overrides_for_codex_home(
841
+ codex_home.to_path_buf(),
842
+ Vec::new(),
843
+ )
844
+ .await
845
+ .expect("default config should load"),
846
+ }
847
+ }
848
+
849
+ struct TestClient {
850
+ _codex_home: TempDir,
851
+ client: InProcessAppServerClient,
852
+ }
853
+
854
+ impl Deref for TestClient {
855
+ type Target = InProcessAppServerClient;
856
+
857
+ fn deref(&self) -> &Self::Target {
858
+ &self.client
859
+ }
860
+ }
861
+
862
+ impl TestClient {
863
+ async fn shutdown(self) -> IoResult<()> {
864
+ self.client.shutdown().await
865
+ }
866
+ }
867
+
868
+ async fn start_test_client_with_capacity(
869
+ session_source: SessionSource,
870
+ channel_capacity: usize,
871
+ ) -> TestClient {
872
+ let codex_home = TempDir::new().expect("temp dir");
873
+ let config = Arc::new(build_test_config_for_codex_home(codex_home.path()).await);
874
+ let state_db = init_state_db(config.as_ref())
875
+ .await
876
+ .expect("state db should initialize for in-process test");
877
+ let client = InProcessAppServerClient::start(InProcessClientStartArgs {
878
+ arg0_paths: Arg0DispatchPaths::default(),
879
+ config,
880
+ cli_overrides: Vec::new(),
881
+ loader_overrides: LoaderOverrides::default(),
882
+ strict_config: false,
883
+ cloud_config_bundle: CloudConfigBundleLoader::default(),
884
+ feedback: CodexFeedback::new(),
885
+ log_db: None,
886
+ state_db: Some(state_db),
887
+ environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
888
+ config_warnings: Vec::new(),
889
+ session_source,
890
+ enable_codex_api_key_env: false,
891
+ client_name: "codex-app-server-client-test".to_string(),
892
+ client_version: "0.0.0-test".to_string(),
893
+ experimental_api: true,
894
+ mcp_server_openai_form_elicitation: false,
895
+ opt_out_notification_methods: Vec::new(),
896
+ channel_capacity,
897
+ })
898
+ .await
899
+ .expect("in-process app-server client should start");
900
+
901
+ TestClient {
902
+ _codex_home: codex_home,
903
+ client,
904
+ }
905
+ }
906
+
907
+ async fn start_test_client(session_source: SessionSource) -> TestClient {
908
+ start_test_client_with_capacity(session_source, DEFAULT_IN_PROCESS_CHANNEL_CAPACITY).await
909
+ }
910
+
911
+ async fn start_test_remote_server<F, Fut>(handler: F) -> String
912
+ where
913
+ F: FnOnce(tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> Fut
914
+ + Send
915
+ + 'static,
916
+ Fut: std::future::Future<Output = ()> + Send + 'static,
917
+ {
918
+ start_test_remote_server_with_auth(/*expected_auth_token*/ None, handler).await
919
+ }
920
+
921
+ async fn start_test_remote_server_with_auth<F, Fut>(
922
+ expected_auth_token: Option<String>,
923
+ handler: F,
924
+ ) -> String
925
+ where
926
+ F: FnOnce(tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> Fut
927
+ + Send
928
+ + 'static,
929
+ Fut: std::future::Future<Output = ()> + Send + 'static,
930
+ {
931
+ let listener = TcpListener::bind("127.0.0.1:0")
932
+ .await
933
+ .expect("listener should bind");
934
+ let addr = listener.local_addr().expect("listener address");
935
+ tokio::spawn(async move {
936
+ let (stream, _) = listener.accept().await.expect("accept should succeed");
937
+ let websocket = accept_hdr_async(
938
+ stream,
939
+ move |request: &WebSocketRequest, response: WebSocketResponse| {
940
+ let provided_auth_token = request
941
+ .headers()
942
+ .get(AUTHORIZATION)
943
+ .and_then(|value| value.to_str().ok())
944
+ .map(str::to_owned);
945
+ let expected_auth_token = expected_auth_token
946
+ .as_ref()
947
+ .map(|token| format!("Bearer {token}"));
948
+ assert_eq!(provided_auth_token, expected_auth_token);
949
+ Ok(response)
950
+ },
951
+ )
952
+ .await
953
+ .expect("websocket upgrade should succeed");
954
+ handler(websocket).await;
955
+ });
956
+ format!("ws://{addr}")
957
+ }
958
+
959
+ async fn expect_remote_initialize<S>(websocket: &mut tokio_tungstenite::WebSocketStream<S>)
960
+ where
961
+ S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
962
+ {
963
+ expect_remote_initialize_with_metadata(
964
+ websocket,
965
+ serde_json::json!({
966
+ "userAgent": "codex_cli_rs/9.8.7-test (Test OS; x86_64) rust",
967
+ "codexHome": "/server/.codex",
968
+ }),
969
+ )
970
+ .await;
971
+ }
972
+
973
+ async fn expect_remote_initialize_with_metadata<S>(
974
+ websocket: &mut tokio_tungstenite::WebSocketStream<S>,
975
+ metadata: serde_json::Value,
976
+ ) where
977
+ S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
978
+ {
979
+ let JSONRPCMessage::Request(request) = read_websocket_message(websocket).await else {
980
+ panic!("expected initialize request");
981
+ };
982
+ assert_eq!(request.method, "initialize");
983
+ write_websocket_message(
984
+ websocket,
985
+ JSONRPCMessage::Response(JSONRPCResponse {
986
+ id: request.id,
987
+ result: metadata,
988
+ }),
989
+ )
990
+ .await;
991
+
992
+ let JSONRPCMessage::Notification(notification) = read_websocket_message(websocket).await
993
+ else {
994
+ panic!("expected initialized notification");
995
+ };
996
+ assert_eq!(notification.method, "initialized");
997
+ }
998
+
999
+ async fn read_websocket_message<S>(
1000
+ websocket: &mut tokio_tungstenite::WebSocketStream<S>,
1001
+ ) -> JSONRPCMessage
1002
+ where
1003
+ S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
1004
+ {
1005
+ loop {
1006
+ let frame = websocket
1007
+ .next()
1008
+ .await
1009
+ .expect("frame should be available")
1010
+ .expect("frame should decode");
1011
+ match frame {
1012
+ Message::Text(text) => {
1013
+ return serde_json::from_str::<JSONRPCMessage>(&text)
1014
+ .expect("text frame should be valid JSON-RPC");
1015
+ }
1016
+ Message::Binary(_) | Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => {
1017
+ continue;
1018
+ }
1019
+ Message::Close(_) => panic!("unexpected close frame"),
1020
+ }
1021
+ }
1022
+ }
1023
+
1024
+ async fn write_websocket_message<S>(
1025
+ websocket: &mut tokio_tungstenite::WebSocketStream<S>,
1026
+ message: JSONRPCMessage,
1027
+ ) where
1028
+ S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
1029
+ {
1030
+ websocket
1031
+ .send(Message::Text(
1032
+ serde_json::to_string(&message)
1033
+ .expect("message should serialize")
1034
+ .into(),
1035
+ ))
1036
+ .await
1037
+ .expect("message should send");
1038
+ }
1039
+
1040
+ fn command_execution_output_delta_notification(delta: &str) -> ServerNotification {
1041
+ ServerNotification::CommandExecutionOutputDelta(
1042
+ codex_app_server_protocol::CommandExecutionOutputDeltaNotification {
1043
+ thread_id: "thread".to_string(),
1044
+ turn_id: "turn".to_string(),
1045
+ item_id: "item".to_string(),
1046
+ delta: delta.to_string(),
1047
+ },
1048
+ )
1049
+ }
1050
+
1051
+ fn agent_message_delta_notification(delta: &str) -> ServerNotification {
1052
+ ServerNotification::AgentMessageDelta(
1053
+ codex_app_server_protocol::AgentMessageDeltaNotification {
1054
+ thread_id: "thread".to_string(),
1055
+ turn_id: "turn".to_string(),
1056
+ item_id: "item".to_string(),
1057
+ delta: delta.to_string(),
1058
+ },
1059
+ )
1060
+ }
1061
+
1062
+ fn item_completed_notification(text: &str) -> ServerNotification {
1063
+ ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification {
1064
+ thread_id: "thread".to_string(),
1065
+ turn_id: "turn".to_string(),
1066
+ completed_at_ms: 0,
1067
+ item: codex_app_server_protocol::ThreadItem::AgentMessage {
1068
+ id: "item".to_string(),
1069
+ text: text.to_string(),
1070
+ phase: None,
1071
+ memory_citation: None,
1072
+ delivery: None,
1073
+ questions: None,
1074
+ },
1075
+ })
1076
+ }
1077
+
1078
+ fn turn_completed_notification() -> ServerNotification {
1079
+ ServerNotification::TurnCompleted(codex_app_server_protocol::TurnCompletedNotification {
1080
+ thread_id: "thread".to_string(),
1081
+ turn: codex_app_server_protocol::Turn {
1082
+ id: "turn".to_string(),
1083
+ items_view: codex_app_server_protocol::TurnItemsView::Full,
1084
+ items: Vec::new(),
1085
+ status: codex_app_server_protocol::TurnStatus::Completed,
1086
+ error: None,
1087
+ started_at: None,
1088
+ completed_at: Some(0),
1089
+ duration_ms: Some(1),
1090
+ },
1091
+ })
1092
+ }
1093
+
1094
+ fn test_remote_connect_args(websocket_url: String) -> RemoteAppServerConnectArgs {
1095
+ RemoteAppServerConnectArgs {
1096
+ endpoint: RemoteAppServerEndpoint::WebSocket {
1097
+ websocket_url,
1098
+ auth_token: None,
1099
+ },
1100
+ client_name: "codex-app-server-client-test".to_string(),
1101
+ client_version: "0.0.0-test".to_string(),
1102
+ experimental_api: true,
1103
+ mcp_server_openai_form_elicitation: false,
1104
+ opt_out_notification_methods: Vec::new(),
1105
+ channel_capacity: 8,
1106
+ }
1107
+ }
1108
+
1109
+ #[test]
1110
+ fn remote_initialize_params_forward_openai_form_capability() {
1111
+ let mut args = test_remote_connect_args("ws://localhost/rpc".to_string());
1112
+ args.mcp_server_openai_form_elicitation = true;
1113
+
1114
+ assert!(
1115
+ args.initialize_params()
1116
+ .capabilities
1117
+ .expect("initialize capabilities")
1118
+ .mcp_server_openai_form_elicitation
1119
+ );
1120
+ }
1121
+
1122
+ #[tokio::test]
1123
+ async fn typed_request_roundtrip_works() {
1124
+ let TestClient {
1125
+ _codex_home,
1126
+ client,
1127
+ } = start_test_client(SessionSource::Exec).await;
1128
+ let client = AppServerClient::InProcess(client);
1129
+ assert_eq!(
1130
+ (client.platform_family(), client.platform_os()),
1131
+ (Some(std::env::consts::FAMILY), Some(std::env::consts::OS))
1132
+ );
1133
+ let _response: ConfigRequirementsReadResponse = client
1134
+ .request_typed(ClientRequest::ConfigRequirementsRead {
1135
+ request_id: RequestId::Integer(1),
1136
+ params: None,
1137
+ })
1138
+ .await
1139
+ .expect("typed request should succeed");
1140
+ client.shutdown().await.expect("shutdown should complete");
1141
+ }
1142
+
1143
+ #[tokio::test]
1144
+ async fn typed_request_reports_json_rpc_errors() {
1145
+ let client = start_test_client(SessionSource::Exec).await;
1146
+ let err = client
1147
+ .request_typed::<ConfigRequirementsReadResponse>(ClientRequest::ThreadRead {
1148
+ request_id: RequestId::Integer(99),
1149
+ params: codex_app_server_protocol::ThreadReadParams {
1150
+ thread_id: "missing-thread".to_string(),
1151
+ include_turns: false,
1152
+ },
1153
+ })
1154
+ .await
1155
+ .expect_err("missing thread should return a JSON-RPC error");
1156
+ assert!(
1157
+ err.to_string().starts_with("thread/read failed:"),
1158
+ "expected method-qualified JSON-RPC failure message"
1159
+ );
1160
+ client.shutdown().await.expect("shutdown should complete");
1161
+ }
1162
+
1163
+ #[tokio::test]
1164
+ async fn caller_provided_session_source_is_applied() {
1165
+ for (session_source, expected_source) in [
1166
+ (SessionSource::Exec, ApiSessionSource::Exec),
1167
+ (SessionSource::Cli, ApiSessionSource::Cli),
1168
+ ] {
1169
+ let client = start_test_client(session_source).await;
1170
+ let parsed: ThreadStartResponse = client
1171
+ .request_typed(ClientRequest::ThreadStart {
1172
+ request_id: RequestId::Integer(2),
1173
+ params: ThreadStartParams {
1174
+ ephemeral: Some(true),
1175
+ ..ThreadStartParams::default()
1176
+ },
1177
+ })
1178
+ .await
1179
+ .expect("thread/start should succeed");
1180
+ assert_eq!(parsed.thread.source, expected_source);
1181
+ client.shutdown().await.expect("shutdown should complete");
1182
+ }
1183
+ }
1184
+
1185
+ #[tokio::test]
1186
+ async fn threads_started_via_app_server_are_visible_through_typed_requests() {
1187
+ let client = start_test_client(SessionSource::Cli).await;
1188
+
1189
+ let response: ThreadStartResponse = client
1190
+ .request_typed(ClientRequest::ThreadStart {
1191
+ request_id: RequestId::Integer(3),
1192
+ params: ThreadStartParams {
1193
+ ephemeral: Some(true),
1194
+ ..ThreadStartParams::default()
1195
+ },
1196
+ })
1197
+ .await
1198
+ .expect("thread/start should succeed");
1199
+ let read = client
1200
+ .request_typed::<codex_app_server_protocol::ThreadReadResponse>(
1201
+ ClientRequest::ThreadRead {
1202
+ request_id: RequestId::Integer(4),
1203
+ params: codex_app_server_protocol::ThreadReadParams {
1204
+ thread_id: response.thread.id.clone(),
1205
+ include_turns: false,
1206
+ },
1207
+ },
1208
+ )
1209
+ .await
1210
+ .expect("thread/read should return the newly started thread");
1211
+ assert_eq!(read.thread.id, response.thread.id);
1212
+
1213
+ client.shutdown().await.expect("shutdown should complete");
1214
+ }
1215
+
1216
+ #[tokio::test]
1217
+ async fn tiny_channel_capacity_still_supports_request_roundtrip() {
1218
+ let client =
1219
+ start_test_client_with_capacity(SessionSource::Exec, /*channel_capacity*/ 1).await;
1220
+ let _response: ConfigRequirementsReadResponse = client
1221
+ .request_typed(ClientRequest::ConfigRequirementsRead {
1222
+ request_id: RequestId::Integer(1),
1223
+ params: None,
1224
+ })
1225
+ .await
1226
+ .expect("typed request should succeed");
1227
+ client.shutdown().await.expect("shutdown should complete");
1228
+ }
1229
+
1230
+ #[tokio::test]
1231
+ async fn unread_lossless_notifications_do_not_block_in_process_requests() {
1232
+ let mut client =
1233
+ start_test_client_with_capacity(SessionSource::Cli, /*channel_capacity*/ 1).await;
1234
+ let thread: ThreadStartResponse = client
1235
+ .request_typed(ClientRequest::ThreadStart {
1236
+ request_id: RequestId::Integer(1),
1237
+ params: ThreadStartParams {
1238
+ ephemeral: Some(true),
1239
+ personality: Some(Personality::None),
1240
+ ..ThreadStartParams::default()
1241
+ },
1242
+ })
1243
+ .await
1244
+ .expect("thread/start should succeed");
1245
+ let request_handle = client.request_handle();
1246
+
1247
+ timeout(Duration::from_secs(2), async {
1248
+ for (index, personality) in [
1249
+ Personality::Friendly,
1250
+ Personality::Pragmatic,
1251
+ Personality::Friendly,
1252
+ Personality::Pragmatic,
1253
+ ]
1254
+ .into_iter()
1255
+ .enumerate()
1256
+ {
1257
+ let _: ThreadSettingsUpdateResponse = request_handle
1258
+ .request_typed(ClientRequest::ThreadSettingsUpdate {
1259
+ request_id: RequestId::Integer((index + 2) as i64),
1260
+ params: ThreadSettingsUpdateParams {
1261
+ thread_id: thread.thread.id.clone(),
1262
+ personality: Some(personality),
1263
+ ..ThreadSettingsUpdateParams::default()
1264
+ },
1265
+ })
1266
+ .await
1267
+ .expect("thread/settings/update should succeed");
1268
+ }
1269
+
1270
+ let _: ConfigRequirementsReadResponse = request_handle
1271
+ .request_typed(ClientRequest::ConfigRequirementsRead {
1272
+ request_id: RequestId::Integer(10),
1273
+ params: None,
1274
+ })
1275
+ .await
1276
+ .expect("configuration request should succeed");
1277
+ })
1278
+ .await
1279
+ .expect("unread lossless notifications must not block app-server requests");
1280
+
1281
+ let mut personalities = Vec::new();
1282
+ timeout(Duration::from_secs(2), async {
1283
+ while personalities.len() < 4 {
1284
+ if let Some(InProcessServerEvent::ServerNotification(notification)) =
1285
+ client.client.next_event().await
1286
+ && let ServerNotification::ThreadSettingsUpdated(notification) =
1287
+ notification.as_ref()
1288
+ {
1289
+ personalities.push(notification.thread_settings.personality);
1290
+ }
1291
+ }
1292
+ })
1293
+ .await
1294
+ .expect("queued settings notifications should remain readable");
1295
+ assert_eq!(
1296
+ personalities,
1297
+ vec![
1298
+ Some(Personality::Friendly),
1299
+ Some(Personality::Pragmatic),
1300
+ Some(Personality::Friendly),
1301
+ Some(Personality::Pragmatic),
1302
+ ]
1303
+ );
1304
+
1305
+ client.shutdown().await.expect("shutdown should complete");
1306
+ }
1307
+
1308
+ #[tokio::test]
1309
+ async fn remote_platform_metadata_preserves_reported_and_missing_values() {
1310
+ for (family, os) in [
1311
+ (Some("windows"), Some("windows")),
1312
+ (Some("unix"), Some("linux")),
1313
+ (Some("future-family"), Some("future-os")),
1314
+ (Some("unix"), None),
1315
+ (None, Some("linux")),
1316
+ (None, None),
1317
+ ] {
1318
+ let websocket_url = start_test_remote_server(move |mut websocket| async move {
1319
+ let mut metadata = serde_json::json!({});
1320
+ if let Some(family) = family {
1321
+ metadata["platformFamily"] = family.into();
1322
+ }
1323
+ if let Some(os) = os {
1324
+ metadata["platformOs"] = os.into();
1325
+ }
1326
+ expect_remote_initialize_with_metadata(&mut websocket, metadata).await;
1327
+ websocket.close(None).await.expect("close should succeed");
1328
+ })
1329
+ .await;
1330
+ let client = AppServerClient::Remote(
1331
+ RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
1332
+ .await
1333
+ .expect("remote client should connect"),
1334
+ );
1335
+ assert_eq!(
1336
+ (client.platform_family(), client.platform_os()),
1337
+ (family, os)
1338
+ );
1339
+ client.shutdown().await.expect("shutdown should complete");
1340
+ }
1341
+ }
1342
+
1343
+ #[tokio::test]
1344
+ async fn remote_typed_request_roundtrip_works() {
1345
+ let websocket_url = start_test_remote_server(|mut websocket| async move {
1346
+ expect_remote_initialize(&mut websocket).await;
1347
+ let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
1348
+ else {
1349
+ panic!("expected account/read request");
1350
+ };
1351
+ assert_eq!(request.method, "account/read");
1352
+ write_websocket_message(
1353
+ &mut websocket,
1354
+ JSONRPCMessage::Response(JSONRPCResponse {
1355
+ id: request.id,
1356
+ result: serde_json::to_value(GetAccountResponse {
1357
+ workspace_routing: None,
1358
+ account: None,
1359
+ requires_openai_auth: false,
1360
+ })
1361
+ .expect("response should serialize"),
1362
+ }),
1363
+ )
1364
+ .await;
1365
+ websocket.close(None).await.expect("close should succeed");
1366
+ })
1367
+ .await;
1368
+ let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
1369
+ .await
1370
+ .expect("remote client should connect");
1371
+
1372
+ assert_eq!(client.server_version(), Some("9.8.7-test"));
1373
+ assert_eq!(client.codex_home(), Some("/server/.codex"));
1374
+ let response: GetAccountResponse = client
1375
+ .request_typed(ClientRequest::GetAccount {
1376
+ request_id: RequestId::Integer(1),
1377
+ params: codex_app_server_protocol::GetAccountParams {
1378
+ refresh_token: false,
1379
+ },
1380
+ })
1381
+ .await
1382
+ .expect("typed request should succeed");
1383
+ assert_eq!(response.account, None);
1384
+
1385
+ client.shutdown().await.expect("shutdown should complete");
1386
+ }
1387
+
1388
+ #[tokio::test]
1389
+ async fn remote_unix_socket_typed_request_roundtrip_works() {
1390
+ let socket_dir = TempDir::new().expect("socket dir");
1391
+ let socket_path = AbsolutePathBuf::from_absolute_path(socket_dir.path().join("codex.sock"))
1392
+ .expect("socket path should resolve");
1393
+ let mut listener = UnixListener::bind(socket_path.as_path())
1394
+ .await
1395
+ .expect("listener should bind");
1396
+ tokio::spawn(async move {
1397
+ let stream = listener.accept().await.expect("accept should succeed");
1398
+ let mut websocket = accept_async(stream)
1399
+ .await
1400
+ .expect("websocket upgrade should succeed");
1401
+ expect_remote_initialize(&mut websocket).await;
1402
+ let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
1403
+ else {
1404
+ panic!("expected account/read request");
1405
+ };
1406
+ assert_eq!(request.method, "account/read");
1407
+ write_websocket_message(
1408
+ &mut websocket,
1409
+ JSONRPCMessage::Response(JSONRPCResponse {
1410
+ id: request.id,
1411
+ result: serde_json::to_value(GetAccountResponse {
1412
+ workspace_routing: None,
1413
+ account: None,
1414
+ requires_openai_auth: false,
1415
+ })
1416
+ .expect("response should serialize"),
1417
+ }),
1418
+ )
1419
+ .await;
1420
+ websocket.close(None).await.expect("close should succeed");
1421
+ });
1422
+ let client = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
1423
+ endpoint: RemoteAppServerEndpoint::UnixSocket { socket_path },
1424
+ client_name: "codex-app-server-client-test".to_string(),
1425
+ client_version: "0.0.0-test".to_string(),
1426
+ experimental_api: true,
1427
+ mcp_server_openai_form_elicitation: false,
1428
+ opt_out_notification_methods: Vec::new(),
1429
+ channel_capacity: 8,
1430
+ })
1431
+ .await
1432
+ .expect("remote client should connect");
1433
+
1434
+ let response: GetAccountResponse = client
1435
+ .request_typed(ClientRequest::GetAccount {
1436
+ request_id: RequestId::Integer(1),
1437
+ params: codex_app_server_protocol::GetAccountParams {
1438
+ refresh_token: false,
1439
+ },
1440
+ })
1441
+ .await
1442
+ .expect("typed request should succeed");
1443
+ assert_eq!(response.account, None);
1444
+
1445
+ client.shutdown().await.expect("shutdown should complete");
1446
+ }
1447
+
1448
+ #[tokio::test]
1449
+ async fn remote_typed_request_accepts_large_single_frame_response() {
1450
+ let padding = "x".repeat((17 << 20) + 1024);
1451
+ let websocket_url = start_test_remote_server(move |mut websocket| async move {
1452
+ expect_remote_initialize(&mut websocket).await;
1453
+ let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
1454
+ else {
1455
+ panic!("expected account/read request");
1456
+ };
1457
+ assert_eq!(request.method, "account/read");
1458
+ write_websocket_message(
1459
+ &mut websocket,
1460
+ JSONRPCMessage::Response(JSONRPCResponse {
1461
+ id: request.id,
1462
+ result: serde_json::json!({
1463
+ "account": null,
1464
+ "requiresOpenaiAuth": false,
1465
+ "padding": padding,
1466
+ }),
1467
+ }),
1468
+ )
1469
+ .await;
1470
+ websocket.close(None).await.expect("close should succeed");
1471
+ })
1472
+ .await;
1473
+ let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
1474
+ .await
1475
+ .expect("remote client should connect");
1476
+
1477
+ let response: GetAccountResponse = client
1478
+ .request_typed(ClientRequest::GetAccount {
1479
+ request_id: RequestId::Integer(1),
1480
+ params: codex_app_server_protocol::GetAccountParams {
1481
+ refresh_token: false,
1482
+ },
1483
+ })
1484
+ .await
1485
+ .expect("large typed request should succeed");
1486
+ assert_eq!(
1487
+ response,
1488
+ GetAccountResponse {
1489
+ workspace_routing: None,
1490
+ account: None,
1491
+ requires_openai_auth: false,
1492
+ }
1493
+ );
1494
+
1495
+ client.shutdown().await.expect("shutdown should complete");
1496
+ }
1497
+
1498
+ #[tokio::test]
1499
+ async fn remote_connect_includes_auth_header_when_configured() {
1500
+ let auth_token = "remote-bearer-token".to_string();
1501
+ let websocket_url = start_test_remote_server_with_auth(
1502
+ Some(auth_token.clone()),
1503
+ |mut websocket| async move {
1504
+ expect_remote_initialize(&mut websocket).await;
1505
+ websocket.close(None).await.expect("close should succeed");
1506
+ },
1507
+ )
1508
+ .await;
1509
+ let client = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
1510
+ endpoint: RemoteAppServerEndpoint::WebSocket {
1511
+ websocket_url,
1512
+ auth_token: Some(auth_token),
1513
+ },
1514
+ client_name: "codex-app-server-client-test".to_string(),
1515
+ client_version: "0.0.0-test".to_string(),
1516
+ experimental_api: true,
1517
+ mcp_server_openai_form_elicitation: false,
1518
+ opt_out_notification_methods: Vec::new(),
1519
+ channel_capacity: 8,
1520
+ })
1521
+ .await
1522
+ .expect("remote client should connect");
1523
+
1524
+ client.shutdown().await.expect("shutdown should complete");
1525
+ }
1526
+
1527
+ #[tokio::test]
1528
+ async fn remote_connect_rejects_non_loopback_ws_when_auth_configured() {
1529
+ let result = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
1530
+ endpoint: RemoteAppServerEndpoint::WebSocket {
1531
+ websocket_url: "ws://example.com:4500".to_string(),
1532
+ auth_token: Some("remote-bearer-token".to_string()),
1533
+ },
1534
+ client_name: "codex-app-server-client-test".to_string(),
1535
+ client_version: "0.0.0-test".to_string(),
1536
+ experimental_api: true,
1537
+ mcp_server_openai_form_elicitation: false,
1538
+ opt_out_notification_methods: Vec::new(),
1539
+ channel_capacity: 8,
1540
+ })
1541
+ .await;
1542
+ let err = match result {
1543
+ Ok(_) => panic!("non-loopback ws should be rejected before connect"),
1544
+ Err(err) => err,
1545
+ };
1546
+ assert_eq!(err.kind(), ErrorKind::InvalidInput);
1547
+ assert!(
1548
+ err.to_string()
1549
+ .contains("remote auth tokens require `wss://` or loopback `ws://` URLs")
1550
+ );
1551
+ }
1552
+
1553
+ #[test]
1554
+ fn remote_auth_token_transport_policy_allows_wss_and_loopback_ws() {
1555
+ assert!(crate::remote::websocket_url_supports_auth_token(
1556
+ &url::Url::parse("wss://example.com:443").expect("wss URL should parse")
1557
+ ));
1558
+ assert!(crate::remote::websocket_url_supports_auth_token(
1559
+ &url::Url::parse("ws://127.0.0.1:4500").expect("loopback ws URL should parse")
1560
+ ));
1561
+ assert!(!crate::remote::websocket_url_supports_auth_token(
1562
+ &url::Url::parse("ws://example.com:4500").expect("non-loopback ws URL should parse")
1563
+ ));
1564
+ }
1565
+
1566
+ #[tokio::test]
1567
+ async fn remote_duplicate_request_id_keeps_original_waiter() {
1568
+ let (first_request_seen_tx, first_request_seen_rx) = tokio::sync::oneshot::channel();
1569
+ let websocket_url = start_test_remote_server(|mut websocket| async move {
1570
+ expect_remote_initialize(&mut websocket).await;
1571
+ let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
1572
+ else {
1573
+ panic!("expected account/read request");
1574
+ };
1575
+ assert_eq!(request.method, "account/read");
1576
+ first_request_seen_tx
1577
+ .send(request.id.clone())
1578
+ .expect("request id should send");
1579
+ assert!(
1580
+ timeout(
1581
+ Duration::from_millis(100),
1582
+ read_websocket_message(&mut websocket)
1583
+ )
1584
+ .await
1585
+ .is_err(),
1586
+ "duplicate request should not be forwarded to the server"
1587
+ );
1588
+ write_websocket_message(
1589
+ &mut websocket,
1590
+ JSONRPCMessage::Response(JSONRPCResponse {
1591
+ id: request.id,
1592
+ result: serde_json::to_value(GetAccountResponse {
1593
+ workspace_routing: None,
1594
+ account: None,
1595
+ requires_openai_auth: false,
1596
+ })
1597
+ .expect("response should serialize"),
1598
+ }),
1599
+ )
1600
+ .await;
1601
+ let _ = websocket.next().await;
1602
+ })
1603
+ .await;
1604
+ let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
1605
+ .await
1606
+ .expect("remote client should connect");
1607
+ let first_request_handle = client.request_handle();
1608
+ let second_request_handle = first_request_handle.clone();
1609
+
1610
+ let first_request = tokio::spawn(async move {
1611
+ first_request_handle
1612
+ .request_typed::<GetAccountResponse>(ClientRequest::GetAccount {
1613
+ request_id: RequestId::Integer(1),
1614
+ params: codex_app_server_protocol::GetAccountParams {
1615
+ refresh_token: false,
1616
+ },
1617
+ })
1618
+ .await
1619
+ });
1620
+
1621
+ let first_request_id = first_request_seen_rx
1622
+ .await
1623
+ .expect("server should observe the first request");
1624
+ assert_eq!(first_request_id, RequestId::Integer(1));
1625
+
1626
+ let second_err = second_request_handle
1627
+ .request_typed::<GetAccountResponse>(ClientRequest::GetAccount {
1628
+ request_id: RequestId::Integer(1),
1629
+ params: codex_app_server_protocol::GetAccountParams {
1630
+ refresh_token: false,
1631
+ },
1632
+ })
1633
+ .await
1634
+ .expect_err("duplicate request id should be rejected");
1635
+ assert_eq!(
1636
+ second_err.to_string(),
1637
+ "account/read transport error: duplicate remote app-server request id `1`"
1638
+ );
1639
+
1640
+ let first_response = first_request
1641
+ .await
1642
+ .expect("first request task should join")
1643
+ .expect("first request should succeed");
1644
+ assert_eq!(
1645
+ first_response,
1646
+ GetAccountResponse {
1647
+ workspace_routing: None,
1648
+ account: None,
1649
+ requires_openai_auth: false,
1650
+ }
1651
+ );
1652
+
1653
+ client.shutdown().await.expect("shutdown should complete");
1654
+ }
1655
+
1656
+ #[tokio::test]
1657
+ async fn remote_notifications_arrive_over_websocket() {
1658
+ let websocket_url = start_test_remote_server(|mut websocket| async move {
1659
+ expect_remote_initialize(&mut websocket).await;
1660
+ write_websocket_message(
1661
+ &mut websocket,
1662
+ JSONRPCMessage::Notification(
1663
+ serde_json::from_value(
1664
+ serde_json::to_value(ServerNotification::AccountUpdated(
1665
+ AccountUpdatedNotification {
1666
+ auth_mode: None,
1667
+ plan_type: None,
1668
+ },
1669
+ ))
1670
+ .expect("notification should serialize"),
1671
+ )
1672
+ .expect("notification should convert to JSON-RPC"),
1673
+ ),
1674
+ )
1675
+ .await;
1676
+ })
1677
+ .await;
1678
+ let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
1679
+ .await
1680
+ .expect("remote client should connect");
1681
+
1682
+ let event = client.next_event().await.expect("event should arrive");
1683
+ assert!(matches!(
1684
+ event,
1685
+ AppServerEvent::ServerNotification(notification)
1686
+ if matches!(notification.as_ref(), ServerNotification::AccountUpdated(_))
1687
+ ));
1688
+
1689
+ client.shutdown().await.expect("shutdown should complete");
1690
+ }
1691
+
1692
+ #[tokio::test]
1693
+ async fn remote_backpressure_preserves_transcript_notifications() {
1694
+ let (done_tx, done_rx) = tokio::sync::oneshot::channel();
1695
+ let websocket_url = start_test_remote_server(|mut websocket| async move {
1696
+ expect_remote_initialize(&mut websocket).await;
1697
+ for notification in [
1698
+ command_execution_output_delta_notification("stdout-1"),
1699
+ command_execution_output_delta_notification("stdout-2"),
1700
+ agent_message_delta_notification("hello"),
1701
+ item_completed_notification("hello"),
1702
+ turn_completed_notification(),
1703
+ ] {
1704
+ write_websocket_message(
1705
+ &mut websocket,
1706
+ JSONRPCMessage::Notification(
1707
+ serde_json::from_value(
1708
+ serde_json::to_value(notification)
1709
+ .expect("notification should serialize"),
1710
+ )
1711
+ .expect("notification should convert to JSON-RPC"),
1712
+ ),
1713
+ )
1714
+ .await;
1715
+ }
1716
+ let _ = done_rx.await;
1717
+ })
1718
+ .await;
1719
+ let mut client = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
1720
+ channel_capacity: 1,
1721
+ ..test_remote_connect_args(websocket_url)
1722
+ })
1723
+ .await
1724
+ .expect("remote client should connect");
1725
+
1726
+ let first_event = timeout(Duration::from_secs(2), client.next_event())
1727
+ .await
1728
+ .expect("first event should arrive before timeout")
1729
+ .expect("event stream should stay open");
1730
+ assert!(matches!(
1731
+ first_event,
1732
+ AppServerEvent::ServerNotification(notification)
1733
+ if matches!(
1734
+ notification.as_ref(),
1735
+ ServerNotification::CommandExecutionOutputDelta(notification)
1736
+ if notification.delta == "stdout-1"
1737
+ )
1738
+ ));
1739
+
1740
+ let mut remaining_events = Vec::new();
1741
+ for _ in 0..4 {
1742
+ remaining_events.push(
1743
+ timeout(Duration::from_secs(2), client.next_event())
1744
+ .await
1745
+ .expect("event should arrive before timeout")
1746
+ .expect("event stream should stay open"),
1747
+ );
1748
+ }
1749
+
1750
+ let mut transcript_event_names = Vec::new();
1751
+ for event in &remaining_events {
1752
+ match event {
1753
+ AppServerEvent::Lagged { skipped: 1 } => {}
1754
+ AppServerEvent::ServerNotification(notification) => match notification.as_ref() {
1755
+ ServerNotification::CommandExecutionOutputDelta(notification)
1756
+ if notification.delta == "stdout-2" => {}
1757
+ ServerNotification::AgentMessageDelta(notification)
1758
+ if notification.delta == "hello" =>
1759
+ {
1760
+ transcript_event_names.push("agent_message_delta");
1761
+ }
1762
+ ServerNotification::ItemCompleted(notification)
1763
+ if matches!(
1764
+ &notification.item,
1765
+ codex_app_server_protocol::ThreadItem::AgentMessage { text, .. }
1766
+ if text == "hello"
1767
+ ) =>
1768
+ {
1769
+ transcript_event_names.push("item_completed");
1770
+ }
1771
+ ServerNotification::TurnCompleted(notification)
1772
+ if notification.turn.status
1773
+ == codex_app_server_protocol::TurnStatus::Completed =>
1774
+ {
1775
+ transcript_event_names.push("turn_completed");
1776
+ }
1777
+ _ => panic!("unexpected remaining event: {event:?}"),
1778
+ },
1779
+ _ => panic!("unexpected remaining event: {event:?}"),
1780
+ }
1781
+ }
1782
+ assert_eq!(
1783
+ transcript_event_names,
1784
+ vec!["agent_message_delta", "item_completed", "turn_completed"]
1785
+ );
1786
+
1787
+ done_tx
1788
+ .send(())
1789
+ .expect("server completion signal should send");
1790
+ client.shutdown().await.expect("shutdown should complete");
1791
+ }
1792
+
1793
+ #[tokio::test]
1794
+ async fn remote_server_request_resolution_roundtrip_works() {
1795
+ let websocket_url = start_test_remote_server(|mut websocket| async move {
1796
+ expect_remote_initialize(&mut websocket).await;
1797
+ let request_id = RequestId::String("srv-1".to_string());
1798
+ let server_request = JSONRPCRequest {
1799
+ id: request_id.clone(),
1800
+ method: "item/tool/requestUserInput".to_string(),
1801
+ params: Some(
1802
+ serde_json::to_value(ToolRequestUserInputParams {
1803
+ thread_id: "thread-1".to_string(),
1804
+ turn_id: "turn-1".to_string(),
1805
+ item_id: "call-1".to_string(),
1806
+ questions: vec![ToolRequestUserInputQuestion {
1807
+ id: "question-1".to_string(),
1808
+ header: "Mode".to_string(),
1809
+ question: "Pick one".to_string(),
1810
+ is_other: false,
1811
+ is_secret: false,
1812
+ options: Some(vec![]),
1813
+ }],
1814
+ is_blocking: true,
1815
+ auto_resolution_ms: None,
1816
+ })
1817
+ .expect("params should serialize"),
1818
+ ),
1819
+ trace: None,
1820
+ };
1821
+ write_websocket_message(&mut websocket, JSONRPCMessage::Request(server_request)).await;
1822
+
1823
+ let JSONRPCMessage::Response(response) = read_websocket_message(&mut websocket).await
1824
+ else {
1825
+ panic!("expected server request response");
1826
+ };
1827
+ assert_eq!(response.id, request_id);
1828
+ })
1829
+ .await;
1830
+ let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
1831
+ .await
1832
+ .expect("remote client should connect");
1833
+
1834
+ let AppServerEvent::ServerRequest(request) = client
1835
+ .next_event()
1836
+ .await
1837
+ .expect("request event should arrive")
1838
+ else {
1839
+ panic!("expected server request event");
1840
+ };
1841
+ client
1842
+ .resolve_server_request(request.id().clone(), serde_json::json!({}))
1843
+ .await
1844
+ .expect("server request should resolve");
1845
+
1846
+ client.shutdown().await.expect("shutdown should complete");
1847
+ }
1848
+
1849
+ #[tokio::test]
1850
+ async fn remote_server_request_received_during_initialize_is_delivered() {
1851
+ let websocket_url = start_test_remote_server(|mut websocket| async move {
1852
+ let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
1853
+ else {
1854
+ panic!("expected initialize request");
1855
+ };
1856
+ assert_eq!(request.method, "initialize");
1857
+
1858
+ let request_id = RequestId::String("srv-init".to_string());
1859
+ write_websocket_message(
1860
+ &mut websocket,
1861
+ JSONRPCMessage::Request(JSONRPCRequest {
1862
+ id: request_id.clone(),
1863
+ method: "item/tool/requestUserInput".to_string(),
1864
+ params: Some(
1865
+ serde_json::to_value(ToolRequestUserInputParams {
1866
+ thread_id: "thread-1".to_string(),
1867
+ turn_id: "turn-1".to_string(),
1868
+ item_id: "call-1".to_string(),
1869
+ questions: vec![ToolRequestUserInputQuestion {
1870
+ id: "question-1".to_string(),
1871
+ header: "Mode".to_string(),
1872
+ question: "Pick one".to_string(),
1873
+ is_other: false,
1874
+ is_secret: false,
1875
+ options: Some(vec![]),
1876
+ }],
1877
+ is_blocking: true,
1878
+ auto_resolution_ms: None,
1879
+ })
1880
+ .expect("params should serialize"),
1881
+ ),
1882
+ trace: None,
1883
+ }),
1884
+ )
1885
+ .await;
1886
+ write_websocket_message(
1887
+ &mut websocket,
1888
+ JSONRPCMessage::Response(JSONRPCResponse {
1889
+ id: request.id,
1890
+ result: serde_json::json!({}),
1891
+ }),
1892
+ )
1893
+ .await;
1894
+
1895
+ let JSONRPCMessage::Notification(notification) =
1896
+ read_websocket_message(&mut websocket).await
1897
+ else {
1898
+ panic!("expected initialized notification");
1899
+ };
1900
+ assert_eq!(notification.method, "initialized");
1901
+
1902
+ let JSONRPCMessage::Response(response) = read_websocket_message(&mut websocket).await
1903
+ else {
1904
+ panic!("expected server request response");
1905
+ };
1906
+ assert_eq!(response.id, request_id);
1907
+ })
1908
+ .await;
1909
+ let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
1910
+ .await
1911
+ .expect("remote client should connect");
1912
+
1913
+ let AppServerEvent::ServerRequest(request) = client
1914
+ .next_event()
1915
+ .await
1916
+ .expect("request event should arrive")
1917
+ else {
1918
+ panic!("expected server request event");
1919
+ };
1920
+ client
1921
+ .resolve_server_request(request.id().clone(), serde_json::json!({}))
1922
+ .await
1923
+ .expect("server request should resolve");
1924
+
1925
+ client.shutdown().await.expect("shutdown should complete");
1926
+ }
1927
+
1928
+ #[tokio::test]
1929
+ async fn remote_unknown_server_request_is_rejected() {
1930
+ let websocket_url = start_test_remote_server(|mut websocket| async move {
1931
+ expect_remote_initialize(&mut websocket).await;
1932
+ let request_id = RequestId::String("srv-unknown".to_string());
1933
+ write_websocket_message(
1934
+ &mut websocket,
1935
+ JSONRPCMessage::Request(JSONRPCRequest {
1936
+ id: request_id.clone(),
1937
+ method: "thread/unknown".to_string(),
1938
+ params: None,
1939
+ trace: None,
1940
+ }),
1941
+ )
1942
+ .await;
1943
+
1944
+ let JSONRPCMessage::Error(response) = read_websocket_message(&mut websocket).await
1945
+ else {
1946
+ panic!("expected JSON-RPC error response");
1947
+ };
1948
+ assert_eq!(response.id, request_id);
1949
+ assert_eq!(response.error.code, -32601);
1950
+ assert_eq!(
1951
+ response.error.message,
1952
+ "unsupported remote app-server request `thread/unknown`"
1953
+ );
1954
+ })
1955
+ .await;
1956
+ let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
1957
+ .await
1958
+ .expect("remote client should connect");
1959
+
1960
+ client.shutdown().await.expect("shutdown should complete");
1961
+ }
1962
+
1963
+ #[tokio::test]
1964
+ async fn remote_disconnect_surfaces_as_event() {
1965
+ let websocket_url = start_test_remote_server(|mut websocket| async move {
1966
+ expect_remote_initialize(&mut websocket).await;
1967
+ websocket.close(None).await.expect("close should succeed");
1968
+ })
1969
+ .await;
1970
+ let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
1971
+ .await
1972
+ .expect("remote client should connect");
1973
+
1974
+ let event = client
1975
+ .next_event()
1976
+ .await
1977
+ .expect("disconnect event should arrive");
1978
+ assert!(matches!(event, AppServerEvent::Disconnected { .. }));
1979
+ }
1980
+
1981
+ #[test]
1982
+ fn typed_request_error_exposes_sources() {
1983
+ let transport = TypedRequestError::Transport {
1984
+ method: "config/read".to_string(),
1985
+ source: IoError::new(ErrorKind::BrokenPipe, "closed"),
1986
+ };
1987
+ assert_eq!(std::error::Error::source(&transport).is_some(), true);
1988
+
1989
+ let server = TypedRequestError::Server {
1990
+ method: "thread/read".to_string(),
1991
+ source: JSONRPCErrorError {
1992
+ code: -32603,
1993
+ data: Some(serde_json::json!({"detail": "config lock mismatch"})),
1994
+ message: "internal".to_string(),
1995
+ },
1996
+ };
1997
+ assert_eq!(std::error::Error::source(&server).is_some(), false);
1998
+ assert_eq!(
1999
+ server.to_string(),
2000
+ "thread/read failed: internal (code -32603), data: {\"detail\":\"config lock mismatch\"}"
2001
+ );
2002
+
2003
+ let deserialize = TypedRequestError::Deserialize {
2004
+ method: "thread/start".to_string(),
2005
+ source: serde_json::from_str::<u32>("\"nope\"")
2006
+ .expect_err("invalid integer should return deserialize error"),
2007
+ };
2008
+ assert_eq!(std::error::Error::source(&deserialize).is_some(), true);
2009
+ }
2010
+
2011
+ #[tokio::test]
2012
+ async fn next_event_surfaces_lagged_markers() {
2013
+ let (command_tx, _command_rx) = mpsc::channel(1);
2014
+ let (event_tx, event_rx) = mpsc::unbounded_channel();
2015
+ let worker_handle = tokio::spawn(async {});
2016
+ event_tx
2017
+ .send(InProcessServerEvent::Lagged { skipped: 3 })
2018
+ .expect("lagged marker should enqueue");
2019
+ drop(event_tx);
2020
+
2021
+ let mut client = InProcessAppServerClient {
2022
+ command_tx,
2023
+ event_rx,
2024
+ worker_handle,
2025
+ };
2026
+
2027
+ let event = timeout(Duration::from_secs(2), client.next_event())
2028
+ .await
2029
+ .expect("lagged marker should arrive before timeout");
2030
+ assert!(matches!(
2031
+ event,
2032
+ Some(InProcessServerEvent::Lagged { skipped: 3 })
2033
+ ));
2034
+
2035
+ client.shutdown().await.expect("shutdown should complete");
2036
+ }
2037
+
2038
+ #[tokio::test]
2039
+ async fn runtime_start_args_forward_environment_manager_and_openai_form_capability() {
2040
+ let config = Arc::new(build_test_config().await);
2041
+ let environment_manager = Arc::new(
2042
+ EnvironmentManager::create_for_tests(
2043
+ Some("ws://127.0.0.1:8765".to_string()),
2044
+ Some(
2045
+ ExecServerRuntimePaths::new(
2046
+ std::env::current_exe().expect("current exe"),
2047
+ /*codex_linux_sandbox_exe*/ None,
2048
+ )
2049
+ .expect("runtime paths"),
2050
+ ),
2051
+ )
2052
+ .await,
2053
+ );
2054
+
2055
+ let runtime_args = InProcessClientStartArgs {
2056
+ arg0_paths: Arg0DispatchPaths::default(),
2057
+ config: config.clone(),
2058
+ cli_overrides: Vec::new(),
2059
+ loader_overrides: LoaderOverrides::default(),
2060
+ strict_config: false,
2061
+ cloud_config_bundle: CloudConfigBundleLoader::default(),
2062
+ feedback: CodexFeedback::new(),
2063
+ log_db: None,
2064
+ state_db: None,
2065
+ environment_manager: environment_manager.clone(),
2066
+ config_warnings: Vec::new(),
2067
+ session_source: SessionSource::Exec,
2068
+ enable_codex_api_key_env: false,
2069
+ client_name: "codex-app-server-client-test".to_string(),
2070
+ client_version: "0.0.0-test".to_string(),
2071
+ experimental_api: true,
2072
+ mcp_server_openai_form_elicitation: true,
2073
+ opt_out_notification_methods: Vec::new(),
2074
+ channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
2075
+ }
2076
+ .into_runtime_start_args();
2077
+
2078
+ assert_eq!(runtime_args.config, config);
2079
+ assert!(
2080
+ runtime_args
2081
+ .initialize
2082
+ .capabilities
2083
+ .expect("initialize capabilities")
2084
+ .mcp_server_openai_form_elicitation
2085
+ );
2086
+ assert!(Arc::ptr_eq(
2087
+ &runtime_args.environment_manager,
2088
+ &environment_manager
2089
+ ));
2090
+ assert!(
2091
+ runtime_args
2092
+ .environment_manager
2093
+ .default_environment()
2094
+ .expect("default environment")
2095
+ .is_remote()
2096
+ );
2097
+ }
2098
+
2099
+ #[tokio::test]
2100
+ async fn shutdown_completes_promptly_without_retained_managers() {
2101
+ let client = start_test_client(SessionSource::Cli).await;
2102
+
2103
+ timeout(Duration::from_secs(1), client.shutdown())
2104
+ .await
2105
+ .expect("shutdown should not wait for the 5s fallback timeout")
2106
+ .expect("shutdown should complete");
2107
+ }
2108
+
2109
+ #[tokio::test(start_paused = true)]
2110
+ async fn shutdown_waits_for_in_process_drain() {
2111
+ use std::sync::atomic::AtomicBool;
2112
+ use std::sync::atomic::Ordering;
2113
+
2114
+ let (command_tx, mut command_rx) = mpsc::channel(1);
2115
+ let (_event_tx, event_rx) = mpsc::unbounded_channel();
2116
+ let completed = Arc::new(AtomicBool::new(false));
2117
+ let worker_completed = Arc::clone(&completed);
2118
+ let worker_handle = tokio::spawn(async move {
2119
+ let response_tx = match command_rx.recv().await {
2120
+ Some(ClientCommand::Shutdown { response_tx }) => response_tx,
2121
+ _ => panic!("expected shutdown command"),
2122
+ };
2123
+ tokio::time::sleep(Duration::from_secs(30)).await;
2124
+ worker_completed.store(true, Ordering::Release);
2125
+ let _ = response_tx.send(Ok(()));
2126
+ });
2127
+ let client = InProcessAppServerClient {
2128
+ command_tx,
2129
+ event_rx,
2130
+ worker_handle,
2131
+ };
2132
+
2133
+ client.shutdown().await.expect("shutdown should complete");
2134
+ assert!(completed.load(Ordering::Acquire));
2135
+ }
2136
+ }
codex-rs/app-server-client/src/path.rs ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Paths resolved using the app-server host's platform rules.
2
+
3
+ use std::fmt;
4
+
5
+ #[derive(Clone, Debug, PartialEq, Eq)]
6
+ pub struct AppServerPath(String);
7
+
8
+ impl AppServerPath {
9
+ pub fn from_app_server(path: impl Into<String>) -> Self {
10
+ Self(path.into())
11
+ }
12
+
13
+ pub fn from_absolute_str(raw: &str) -> Option<Self> {
14
+ (raw.starts_with('/') || is_windows_absolute_path(raw)).then(|| Self(raw.to_string()))
15
+ }
16
+
17
+ pub fn as_str(&self) -> &str {
18
+ &self.0
19
+ }
20
+
21
+ pub fn components(&self) -> Vec<&str> {
22
+ let separators = if is_windows_absolute_path(&self.0) {
23
+ &['/', '\\'][..]
24
+ } else {
25
+ &['/'][..]
26
+ };
27
+ self.0
28
+ .split(separators)
29
+ .filter(|part| !part.is_empty())
30
+ .collect()
31
+ }
32
+
33
+ pub fn join(&self, segment: impl AsRef<str>) -> Self {
34
+ let is_windows = is_windows_absolute_path(&self.0);
35
+ let (path, separator) = if is_windows {
36
+ (self.0.trim_end_matches(['/', '\\']), '\\')
37
+ } else {
38
+ (self.0.trim_end_matches('/'), '/')
39
+ };
40
+ Self(format!("{path}{separator}{}", segment.as_ref()))
41
+ }
42
+ }
43
+
44
+ impl fmt::Display for AppServerPath {
45
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46
+ self.0.fmt(f)
47
+ }
48
+ }
49
+
50
+ fn is_windows_absolute_path(path: &str) -> bool {
51
+ let bytes = path.as_bytes();
52
+ (bytes.len() >= 3
53
+ && bytes[0].is_ascii_alphabetic()
54
+ && bytes[1] == b':'
55
+ && matches!(bytes[2], b'\\' | b'/'))
56
+ || path.starts_with("\\\\")
57
+ || path.starts_with("//")
58
+ }
codex-rs/app-server-client/src/remote.rs ADDED
@@ -0,0 +1,1088 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ This module implements the remote app-server client transport.
3
+
4
+ It owns the remote connection lifecycle, including the initialize/initialized
5
+ handshake, JSON-RPC request/response routing, server-request resolution, and
6
+ notification streaming. Remote connections always carry WebSocket frames, over
7
+ either TCP WebSocket URLs or local Unix sockets. The rest of the crate uses the
8
+ same `AppServerEvent` surface for both in-process and remote transports, so
9
+ callers such as the TUI can switch between them without changing their
10
+ higher-level session logic.
11
+ */
12
+
13
+ use std::collections::HashMap;
14
+ use std::collections::VecDeque;
15
+ use std::io::Error as IoError;
16
+ use std::io::ErrorKind;
17
+ use std::io::Result as IoResult;
18
+ use std::time::Duration;
19
+
20
+ use crate::AppServerEvent;
21
+ use crate::RequestResult;
22
+ use crate::SHUTDOWN_TIMEOUT;
23
+ use crate::TypedRequestError;
24
+ use codex_app_server_protocol::ClientInfo;
25
+ use codex_app_server_protocol::ClientNotification;
26
+ use codex_app_server_protocol::ClientRequest;
27
+ use codex_app_server_protocol::InitializeCapabilities;
28
+ use codex_app_server_protocol::InitializeParams;
29
+ use codex_app_server_protocol::JSONRPCError;
30
+ use codex_app_server_protocol::JSONRPCErrorError;
31
+ use codex_app_server_protocol::JSONRPCMessage;
32
+ use codex_app_server_protocol::JSONRPCNotification;
33
+ use codex_app_server_protocol::JSONRPCRequest;
34
+ use codex_app_server_protocol::JSONRPCResponse;
35
+ use codex_app_server_protocol::RequestId;
36
+ use codex_app_server_protocol::Result as JsonRpcResult;
37
+ use codex_app_server_protocol::ServerNotification;
38
+ use codex_app_server_protocol::ServerRequest;
39
+ use codex_uds::UnixStream;
40
+ use codex_utils_absolute_path::AbsolutePathBuf;
41
+ use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
42
+ use futures::SinkExt;
43
+ use futures::StreamExt;
44
+ use serde::de::DeserializeOwned;
45
+ use tokio::io::AsyncRead;
46
+ use tokio::io::AsyncWrite;
47
+ use tokio::net::TcpStream;
48
+ use tokio::sync::mpsc;
49
+ use tokio::sync::oneshot;
50
+ use tokio::time::timeout;
51
+ use tokio_tungstenite::MaybeTlsStream;
52
+ use tokio_tungstenite::WebSocketStream;
53
+ use tokio_tungstenite::client_async_with_config;
54
+ use tokio_tungstenite::connect_async_with_config;
55
+ use tokio_tungstenite::tungstenite::Error as TungsteniteError;
56
+ use tokio_tungstenite::tungstenite::Message;
57
+ use tokio_tungstenite::tungstenite::client::IntoClientRequest;
58
+ use tokio_tungstenite::tungstenite::http::HeaderValue;
59
+ use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
60
+ use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
61
+ use tracing::warn;
62
+ use url::Url;
63
+
64
+ const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
65
+ const INITIALIZE_TIMEOUT: Duration = Duration::from_secs(10);
66
+ const REMOTE_APP_SERVER_MAX_WEBSOCKET_MESSAGE_SIZE: usize = 128 << 20;
67
+ // Tungstenite still needs an HTTP request URI for the WebSocket handshake;
68
+ // the bytes travel over the Unix socket, not TCP.
69
+ const UDS_WEBSOCKET_HANDSHAKE_URL: &str = "ws://localhost/rpc";
70
+
71
+ #[derive(Debug, Clone, PartialEq, Eq)]
72
+ pub enum RemoteAppServerEndpoint {
73
+ WebSocket {
74
+ websocket_url: String,
75
+ auth_token: Option<String>,
76
+ },
77
+ UnixSocket {
78
+ socket_path: AbsolutePathBuf,
79
+ },
80
+ }
81
+
82
+ #[derive(Debug, Clone)]
83
+ pub struct RemoteAppServerConnectArgs {
84
+ pub endpoint: RemoteAppServerEndpoint,
85
+ pub client_name: String,
86
+ pub client_version: String,
87
+ pub experimental_api: bool,
88
+ pub mcp_server_openai_form_elicitation: bool,
89
+ pub opt_out_notification_methods: Vec<String>,
90
+ pub channel_capacity: usize,
91
+ }
92
+ impl RemoteAppServerConnectArgs {
93
+ pub(crate) fn initialize_params(&self) -> InitializeParams {
94
+ let capabilities = InitializeCapabilities {
95
+ experimental_api: self.experimental_api,
96
+ request_attestation: false,
97
+ extensions: None,
98
+ opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() {
99
+ None
100
+ } else {
101
+ Some(self.opt_out_notification_methods.clone())
102
+ },
103
+ mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation,
104
+ };
105
+
106
+ InitializeParams {
107
+ client_info: ClientInfo {
108
+ name: self.client_name.clone(),
109
+ title: None,
110
+ version: self.client_version.clone(),
111
+ },
112
+ capabilities: Some(capabilities),
113
+ }
114
+ }
115
+ }
116
+
117
+ pub(crate) fn websocket_url_supports_auth_token(url: &Url) -> bool {
118
+ match (url.scheme(), url.host()) {
119
+ ("wss", Some(_)) => true,
120
+ ("ws", Some(url::Host::Domain(domain))) => domain.eq_ignore_ascii_case("localhost"),
121
+ ("ws", Some(url::Host::Ipv4(addr))) => addr.is_loopback(),
122
+ ("ws", Some(url::Host::Ipv6(addr))) => addr.is_loopback(),
123
+ _ => false,
124
+ }
125
+ }
126
+
127
+ enum RemoteClientCommand {
128
+ Request {
129
+ request: Box<JSONRPCRequest>,
130
+ response_tx: oneshot::Sender<IoResult<RequestResult>>,
131
+ },
132
+ Notify {
133
+ notification: ClientNotification,
134
+ response_tx: oneshot::Sender<IoResult<()>>,
135
+ },
136
+ ResolveServerRequest {
137
+ request_id: RequestId,
138
+ result: JsonRpcResult,
139
+ response_tx: oneshot::Sender<IoResult<()>>,
140
+ },
141
+ RejectServerRequest {
142
+ request_id: RequestId,
143
+ error: JSONRPCErrorError,
144
+ response_tx: oneshot::Sender<IoResult<()>>,
145
+ },
146
+ Shutdown {
147
+ response_tx: oneshot::Sender<IoResult<()>>,
148
+ },
149
+ }
150
+
151
+ #[derive(Default)]
152
+ struct RemoteServerMetadata {
153
+ server_version: Option<String>,
154
+ codex_home: Option<String>,
155
+ platform_family: Option<String>,
156
+ platform_os: Option<String>,
157
+ }
158
+
159
+ pub struct RemoteAppServerClient {
160
+ command_tx: mpsc::Sender<RemoteClientCommand>,
161
+ event_rx: mpsc::UnboundedReceiver<AppServerEvent>,
162
+ pending_events: VecDeque<AppServerEvent>,
163
+ metadata: RemoteServerMetadata,
164
+ worker_handle: tokio::task::JoinHandle<()>,
165
+ }
166
+
167
+ #[derive(Clone)]
168
+ pub struct RemoteAppServerRequestHandle {
169
+ command_tx: mpsc::Sender<RemoteClientCommand>,
170
+ }
171
+
172
+ enum SocketPeerPolicy {
173
+ ExplicitEndpoint,
174
+ #[cfg(windows)]
175
+ NonElevatedCurrentUser,
176
+ }
177
+
178
+ impl RemoteAppServerClient {
179
+ pub async fn connect(args: RemoteAppServerConnectArgs) -> IoResult<Self> {
180
+ Self::connect_with_policy(args, SocketPeerPolicy::ExplicitEndpoint).await
181
+ }
182
+
183
+ /// Connects to an implicitly discovered Windows daemon, verifying its peer
184
+ /// token before the WebSocket handshake or any session requests.
185
+ #[cfg(windows)]
186
+ pub async fn connect_local_daemon(args: RemoteAppServerConnectArgs) -> IoResult<Self> {
187
+ if !matches!(args.endpoint, RemoteAppServerEndpoint::UnixSocket { .. }) {
188
+ return Err(IoError::new(
189
+ ErrorKind::InvalidInput,
190
+ "local daemon requires a Unix socket",
191
+ ));
192
+ }
193
+ Self::connect_with_policy(args, SocketPeerPolicy::NonElevatedCurrentUser).await
194
+ }
195
+
196
+ async fn connect_with_policy(
197
+ args: RemoteAppServerConnectArgs,
198
+ peer_policy: SocketPeerPolicy,
199
+ ) -> IoResult<Self> {
200
+ let channel_capacity = args.channel_capacity.max(1);
201
+ let initialize_params = args.initialize_params();
202
+ match args.endpoint {
203
+ RemoteAppServerEndpoint::WebSocket {
204
+ websocket_url,
205
+ auth_token,
206
+ } => {
207
+ let (endpoint, stream) =
208
+ connect_websocket_endpoint(websocket_url, auth_token).await?;
209
+ Self::connect_with_stream(channel_capacity, endpoint, stream, initialize_params)
210
+ .await
211
+ }
212
+ RemoteAppServerEndpoint::UnixSocket { socket_path } => {
213
+ let (endpoint, stream) =
214
+ connect_unix_socket_endpoint(socket_path, peer_policy).await?;
215
+ Self::connect_with_stream(channel_capacity, endpoint, stream, initialize_params)
216
+ .await
217
+ }
218
+ }
219
+ }
220
+
221
+ pub fn server_version(&self) -> Option<&str> {
222
+ self.metadata.server_version.as_deref()
223
+ }
224
+
225
+ pub fn codex_home(&self) -> Option<&str> {
226
+ self.metadata.codex_home.as_deref()
227
+ }
228
+
229
+ pub fn platform_family(&self) -> Option<&str> {
230
+ self.metadata.platform_family.as_deref()
231
+ }
232
+
233
+ pub fn platform_os(&self) -> Option<&str> {
234
+ self.metadata.platform_os.as_deref()
235
+ }
236
+
237
+ async fn connect_with_stream<S>(
238
+ channel_capacity: usize,
239
+ endpoint: String,
240
+ stream: WebSocketStream<S>,
241
+ initialize_params: InitializeParams,
242
+ ) -> IoResult<Self>
243
+ where
244
+ S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
245
+ {
246
+ let mut stream = stream;
247
+ let (pending_events, metadata) = initialize_remote_connection(
248
+ &mut stream,
249
+ &endpoint,
250
+ initialize_params,
251
+ INITIALIZE_TIMEOUT,
252
+ )
253
+ .await?;
254
+
255
+ let (command_tx, mut command_rx) = mpsc::channel::<RemoteClientCommand>(channel_capacity);
256
+ let (event_tx, event_rx) = mpsc::unbounded_channel::<AppServerEvent>();
257
+ let worker_handle = tokio::spawn(async move {
258
+ let mut pending_requests =
259
+ HashMap::<RequestId, oneshot::Sender<IoResult<RequestResult>>>::new();
260
+ let mut worker_exit_error: Option<(ErrorKind, String)> = None;
261
+ loop {
262
+ tokio::select! {
263
+ command = command_rx.recv() => {
264
+ let Some(command) = command else {
265
+ let _ = stream.close(None).await;
266
+ break;
267
+ };
268
+ match command {
269
+ RemoteClientCommand::Request { request, response_tx } => {
270
+ let request_id = request.id.clone();
271
+ if pending_requests.contains_key(&request_id) {
272
+ let _ = response_tx.send(Err(IoError::new(
273
+ ErrorKind::InvalidInput,
274
+ format!("duplicate remote app-server request id `{request_id}`"),
275
+ )));
276
+ continue;
277
+ }
278
+ pending_requests.insert(request_id.clone(), response_tx);
279
+ if let Err(err) = write_jsonrpc_message(
280
+ &mut stream,
281
+ JSONRPCMessage::Request(*request),
282
+ &endpoint,
283
+ )
284
+ .await
285
+ {
286
+ let err_message = err.to_string();
287
+ let message = format!(
288
+ "remote app server at `{endpoint}` write failed: {err_message}"
289
+ );
290
+ if let Some(response_tx) = pending_requests.remove(&request_id) {
291
+ let _ = response_tx.send(Err(err));
292
+ }
293
+ let _ = deliver_event(
294
+ &event_tx,
295
+ AppServerEvent::Disconnected {
296
+ message: message.clone(),
297
+ },
298
+ );
299
+ worker_exit_error = Some((ErrorKind::BrokenPipe, message));
300
+ break;
301
+ }
302
+ }
303
+ RemoteClientCommand::Notify { notification, response_tx } => {
304
+ let result = write_jsonrpc_message(
305
+ &mut stream,
306
+ JSONRPCMessage::Notification(
307
+ jsonrpc_notification_from_client_notification(notification),
308
+ ),
309
+ &endpoint,
310
+ )
311
+ .await;
312
+ let _ = response_tx.send(result);
313
+ }
314
+ RemoteClientCommand::ResolveServerRequest {
315
+ request_id,
316
+ result,
317
+ response_tx,
318
+ } => {
319
+ let result = write_jsonrpc_message(
320
+ &mut stream,
321
+ JSONRPCMessage::Response(JSONRPCResponse {
322
+ id: request_id,
323
+ result,
324
+ }),
325
+ &endpoint,
326
+ )
327
+ .await;
328
+ let _ = response_tx.send(result);
329
+ }
330
+ RemoteClientCommand::RejectServerRequest {
331
+ request_id,
332
+ error,
333
+ response_tx,
334
+ } => {
335
+ let result = write_jsonrpc_message(
336
+ &mut stream,
337
+ JSONRPCMessage::Error(JSONRPCError {
338
+ error,
339
+ id: request_id,
340
+ }),
341
+ &endpoint,
342
+ )
343
+ .await;
344
+ let _ = response_tx.send(result);
345
+ }
346
+ RemoteClientCommand::Shutdown { response_tx } => {
347
+ let close_result = stream.close(None).await.or_else(|err| {
348
+ if websocket_close_error_is_already_closed(&err) {
349
+ Ok(())
350
+ } else {
351
+ Err(IoError::other(format!(
352
+ "failed to close websocket app server `{endpoint}`: {err}"
353
+ )))
354
+ }
355
+ });
356
+ let _ = response_tx.send(close_result);
357
+ break;
358
+ }
359
+ }
360
+ }
361
+ message = stream.next() => {
362
+ match message {
363
+ Some(Ok(Message::Text(text))) => {
364
+ match serde_json::from_str::<JSONRPCMessage>(&text) {
365
+ Ok(JSONRPCMessage::Response(response)) => {
366
+ if let Some(response_tx) = pending_requests.remove(&response.id) {
367
+ let _ = response_tx.send(Ok(Ok(response.result)));
368
+ }
369
+ }
370
+ Ok(JSONRPCMessage::Error(error)) => {
371
+ if let Some(response_tx) = pending_requests.remove(&error.id) {
372
+ let _ = response_tx.send(Ok(Err(error.error)));
373
+ }
374
+ }
375
+ Ok(JSONRPCMessage::Notification(notification)) => {
376
+ if let Some(event) =
377
+ app_server_event_from_notification(notification)
378
+ && let Err(err) = deliver_event(
379
+ &event_tx,
380
+ event,
381
+ )
382
+ {
383
+ warn!(%err, "failed to deliver remote app-server event");
384
+ break;
385
+ }
386
+ }
387
+ Ok(JSONRPCMessage::Request(request)) => {
388
+ let request_id = request.id.clone();
389
+ let method = request.method.clone();
390
+ match ServerRequest::try_from(request) {
391
+ Ok(request) => {
392
+ if let Err(err) = deliver_event(
393
+ &event_tx,
394
+ AppServerEvent::ServerRequest(Box::new(request)),
395
+ )
396
+ {
397
+ warn!(%err, "failed to deliver remote app-server server request");
398
+ break;
399
+ }
400
+ }
401
+ Err(err) => {
402
+ warn!(%err, method, "rejecting unknown remote app-server request");
403
+ if let Err(reject_err) = write_jsonrpc_message(
404
+ &mut stream,
405
+ JSONRPCMessage::Error(JSONRPCError {
406
+ error: JSONRPCErrorError {
407
+ code: -32601,
408
+ message: format!(
409
+ "unsupported remote app-server request `{method}`"
410
+ ),
411
+ data: None,
412
+ },
413
+ id: request_id,
414
+ }),
415
+ &endpoint,
416
+ )
417
+ .await
418
+ {
419
+ let err_message = reject_err.to_string();
420
+ let message = format!(
421
+ "remote app server at `{endpoint}` write failed: {err_message}"
422
+ );
423
+ let _ = deliver_event(
424
+ &event_tx,
425
+ AppServerEvent::Disconnected {
426
+ message: message.clone(),
427
+ },
428
+ );
429
+ worker_exit_error =
430
+ Some((ErrorKind::BrokenPipe, message));
431
+ break;
432
+ }
433
+ }
434
+ }
435
+ }
436
+ Err(err) => {
437
+ let message = format!(
438
+ "remote app server at `{endpoint}` sent invalid JSON-RPC: {err}"
439
+ );
440
+ let _ = deliver_event(
441
+ &event_tx,
442
+ AppServerEvent::Disconnected {
443
+ message: message.clone(),
444
+ },
445
+ );
446
+ worker_exit_error =
447
+ Some((ErrorKind::InvalidData, message));
448
+ break;
449
+ }
450
+ }
451
+ }
452
+ Some(Ok(Message::Close(frame))) => {
453
+ let reason = frame
454
+ .as_ref()
455
+ .map(|frame| frame.reason.to_string())
456
+ .filter(|reason| !reason.is_empty())
457
+ .unwrap_or_else(|| "connection closed".to_string());
458
+ let message = format!(
459
+ "remote app server at `{endpoint}` disconnected: {reason}"
460
+ );
461
+ let _ = deliver_event(
462
+ &event_tx,
463
+ AppServerEvent::Disconnected {
464
+ message: message.clone(),
465
+ },
466
+ );
467
+ worker_exit_error = Some((
468
+ ErrorKind::ConnectionAborted,
469
+ message,
470
+ ));
471
+ break;
472
+ }
473
+ Some(Ok(Message::Binary(_)))
474
+ | Some(Ok(Message::Ping(_)))
475
+ | Some(Ok(Message::Pong(_)))
476
+ | Some(Ok(Message::Frame(_))) => {}
477
+ Some(Err(err)) => {
478
+ let message = format!(
479
+ "remote app server at `{endpoint}` transport failed: {err}"
480
+ );
481
+ let _ = deliver_event(
482
+ &event_tx,
483
+ AppServerEvent::Disconnected {
484
+ message: message.clone(),
485
+ },
486
+ );
487
+ worker_exit_error = Some((ErrorKind::InvalidData, message));
488
+ break;
489
+ }
490
+ None => {
491
+ let message = format!(
492
+ "remote app server at `{endpoint}` closed the connection"
493
+ );
494
+ let _ = deliver_event(
495
+ &event_tx,
496
+ AppServerEvent::Disconnected {
497
+ message: message.clone(),
498
+ },
499
+ );
500
+ worker_exit_error = Some((ErrorKind::UnexpectedEof, message));
501
+ break;
502
+ }
503
+ }
504
+ }
505
+ }
506
+ }
507
+
508
+ let (err_kind, err_message) = worker_exit_error.unwrap_or_else(|| {
509
+ (
510
+ ErrorKind::BrokenPipe,
511
+ "remote app-server worker channel is closed".to_string(),
512
+ )
513
+ });
514
+ for (_, response_tx) in pending_requests {
515
+ let _ = response_tx.send(Err(IoError::new(err_kind, err_message.clone())));
516
+ }
517
+ });
518
+
519
+ Ok(Self {
520
+ command_tx,
521
+ event_rx,
522
+ pending_events: pending_events.into(),
523
+ metadata,
524
+ worker_handle,
525
+ })
526
+ }
527
+
528
+ pub fn request_handle(&self) -> RemoteAppServerRequestHandle {
529
+ RemoteAppServerRequestHandle {
530
+ command_tx: self.command_tx.clone(),
531
+ }
532
+ }
533
+
534
+ pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
535
+ self.request_handle().request(request).await
536
+ }
537
+
538
+ pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
539
+ where
540
+ T: DeserializeOwned,
541
+ {
542
+ let method = request.method_name();
543
+ let response =
544
+ self.request(request)
545
+ .await
546
+ .map_err(|source| TypedRequestError::Transport {
547
+ method: method.to_string(),
548
+ source,
549
+ })?;
550
+ let result = response.map_err(|source| TypedRequestError::Server {
551
+ method: method.to_string(),
552
+ source,
553
+ })?;
554
+ serde_json::from_value(result).map_err(|source| TypedRequestError::Deserialize {
555
+ method: method.to_string(),
556
+ source,
557
+ })
558
+ }
559
+
560
+ pub async fn notify(&self, notification: ClientNotification) -> IoResult<()> {
561
+ let (response_tx, response_rx) = oneshot::channel();
562
+ self.command_tx
563
+ .send(RemoteClientCommand::Notify {
564
+ notification,
565
+ response_tx,
566
+ })
567
+ .await
568
+ .map_err(|_| {
569
+ IoError::new(
570
+ ErrorKind::BrokenPipe,
571
+ "remote app-server worker channel is closed",
572
+ )
573
+ })?;
574
+ response_rx.await.map_err(|_| {
575
+ IoError::new(
576
+ ErrorKind::BrokenPipe,
577
+ "remote app-server notify channel is closed",
578
+ )
579
+ })?
580
+ }
581
+
582
+ pub async fn resolve_server_request(
583
+ &self,
584
+ request_id: RequestId,
585
+ result: JsonRpcResult,
586
+ ) -> IoResult<()> {
587
+ let (response_tx, response_rx) = oneshot::channel();
588
+ self.command_tx
589
+ .send(RemoteClientCommand::ResolveServerRequest {
590
+ request_id,
591
+ result,
592
+ response_tx,
593
+ })
594
+ .await
595
+ .map_err(|_| {
596
+ IoError::new(
597
+ ErrorKind::BrokenPipe,
598
+ "remote app-server worker channel is closed",
599
+ )
600
+ })?;
601
+ response_rx.await.map_err(|_| {
602
+ IoError::new(
603
+ ErrorKind::BrokenPipe,
604
+ "remote app-server resolve channel is closed",
605
+ )
606
+ })?
607
+ }
608
+
609
+ pub async fn reject_server_request(
610
+ &self,
611
+ request_id: RequestId,
612
+ error: JSONRPCErrorError,
613
+ ) -> IoResult<()> {
614
+ let (response_tx, response_rx) = oneshot::channel();
615
+ self.command_tx
616
+ .send(RemoteClientCommand::RejectServerRequest {
617
+ request_id,
618
+ error,
619
+ response_tx,
620
+ })
621
+ .await
622
+ .map_err(|_| {
623
+ IoError::new(
624
+ ErrorKind::BrokenPipe,
625
+ "remote app-server worker channel is closed",
626
+ )
627
+ })?;
628
+ response_rx.await.map_err(|_| {
629
+ IoError::new(
630
+ ErrorKind::BrokenPipe,
631
+ "remote app-server reject channel is closed",
632
+ )
633
+ })?
634
+ }
635
+
636
+ pub async fn next_event(&mut self) -> Option<AppServerEvent> {
637
+ if let Some(event) = self.pending_events.pop_front() {
638
+ return Some(event);
639
+ }
640
+ self.event_rx.recv().await
641
+ }
642
+
643
+ pub async fn shutdown(self) -> IoResult<()> {
644
+ let Self {
645
+ command_tx,
646
+ event_rx,
647
+ pending_events: _pending_events,
648
+ metadata: _,
649
+ worker_handle,
650
+ } = self;
651
+ let mut worker_handle = worker_handle;
652
+ drop(event_rx);
653
+ let (response_tx, response_rx) = oneshot::channel();
654
+ if command_tx
655
+ .send(RemoteClientCommand::Shutdown { response_tx })
656
+ .await
657
+ .is_ok()
658
+ && let Ok(Ok(close_result)) = timeout(SHUTDOWN_TIMEOUT, response_rx).await
659
+ {
660
+ close_result?;
661
+ }
662
+
663
+ if let Err(_elapsed) = timeout(SHUTDOWN_TIMEOUT, &mut worker_handle).await {
664
+ worker_handle.abort();
665
+ let _ = worker_handle.await;
666
+ }
667
+ Ok(())
668
+ }
669
+ }
670
+
671
+ impl RemoteAppServerRequestHandle {
672
+ pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
673
+ self.request_json_rpc(jsonrpc_request_from_client_request(request))
674
+ .await
675
+ }
676
+
677
+ pub async fn request_json_rpc(&self, request: JSONRPCRequest) -> IoResult<RequestResult> {
678
+ let (response_tx, response_rx) = oneshot::channel();
679
+ self.command_tx
680
+ .send(RemoteClientCommand::Request {
681
+ request: Box::new(request),
682
+ response_tx,
683
+ })
684
+ .await
685
+ .map_err(|_| {
686
+ IoError::new(
687
+ ErrorKind::BrokenPipe,
688
+ "remote app-server worker channel is closed",
689
+ )
690
+ })?;
691
+ response_rx.await.map_err(|_| {
692
+ IoError::new(
693
+ ErrorKind::BrokenPipe,
694
+ "remote app-server request channel is closed",
695
+ )
696
+ })?
697
+ }
698
+
699
+ pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
700
+ where
701
+ T: DeserializeOwned,
702
+ {
703
+ let method = request.method_name();
704
+ let response =
705
+ self.request(request)
706
+ .await
707
+ .map_err(|source| TypedRequestError::Transport {
708
+ method: method.to_string(),
709
+ source,
710
+ })?;
711
+ let result = response.map_err(|source| TypedRequestError::Server {
712
+ method: method.to_string(),
713
+ source,
714
+ })?;
715
+ serde_json::from_value(result).map_err(|source| TypedRequestError::Deserialize {
716
+ method: method.to_string(),
717
+ source,
718
+ })
719
+ }
720
+ }
721
+
722
+ async fn connect_websocket_endpoint(
723
+ websocket_url: String,
724
+ auth_token: Option<String>,
725
+ ) -> IoResult<(String, WebSocketStream<MaybeTlsStream<TcpStream>>)> {
726
+ let url = Url::parse(&websocket_url).map_err(|err| {
727
+ IoError::new(
728
+ ErrorKind::InvalidInput,
729
+ format!("invalid websocket URL `{websocket_url}`: {err}"),
730
+ )
731
+ })?;
732
+ if auth_token.is_some() && !websocket_url_supports_auth_token(&url) {
733
+ return Err(IoError::new(
734
+ ErrorKind::InvalidInput,
735
+ format!(
736
+ "remote auth tokens require `wss://` or loopback `ws://` URLs; got `{websocket_url}`"
737
+ ),
738
+ ));
739
+ }
740
+
741
+ let mut request = url.as_str().into_client_request().map_err(|err| {
742
+ IoError::new(
743
+ ErrorKind::InvalidInput,
744
+ format!("invalid websocket URL `{websocket_url}`: {err}"),
745
+ )
746
+ })?;
747
+ if let Some(auth_token) = auth_token.as_deref() {
748
+ let header_value =
749
+ HeaderValue::from_str(&format!("Bearer {auth_token}")).map_err(|err| {
750
+ IoError::new(
751
+ ErrorKind::InvalidInput,
752
+ format!("invalid remote authorization header value: {err}"),
753
+ )
754
+ })?;
755
+ request.headers_mut().insert(AUTHORIZATION, header_value);
756
+ }
757
+
758
+ ensure_rustls_crypto_provider();
759
+ let websocket_config = remote_websocket_config();
760
+ let stream = timeout(
761
+ CONNECT_TIMEOUT,
762
+ connect_async_with_config(
763
+ request,
764
+ Some(websocket_config),
765
+ /*disable_nagle*/ false,
766
+ ),
767
+ )
768
+ .await
769
+ .map_err(|_| {
770
+ IoError::new(
771
+ ErrorKind::TimedOut,
772
+ format!("timed out connecting to remote app server at `{websocket_url}`"),
773
+ )
774
+ })?
775
+ .map(|(stream, _response)| stream)
776
+ .map_err(|err| {
777
+ IoError::other(format!(
778
+ "failed to connect to remote app server at `{websocket_url}`: {err}"
779
+ ))
780
+ })?;
781
+
782
+ Ok((websocket_url, stream))
783
+ }
784
+
785
+ async fn connect_unix_socket_endpoint(
786
+ socket_path: AbsolutePathBuf,
787
+ peer_policy: SocketPeerPolicy,
788
+ ) -> IoResult<(String, WebSocketStream<UnixStream>)> {
789
+ let endpoint = format!("unix://{}", socket_path.display());
790
+ let request = UDS_WEBSOCKET_HANDSHAKE_URL
791
+ .into_client_request()
792
+ .map_err(|err| {
793
+ IoError::new(
794
+ ErrorKind::InvalidInput,
795
+ format!("invalid UDS websocket handshake URL: {err}"),
796
+ )
797
+ })?;
798
+ let stream = timeout(CONNECT_TIMEOUT, UnixStream::connect(socket_path.as_path()))
799
+ .await
800
+ .map_err(|_| {
801
+ IoError::new(
802
+ ErrorKind::TimedOut,
803
+ format!("timed out connecting to remote app server at `{endpoint}`"),
804
+ )
805
+ })?
806
+ .map_err(|err| {
807
+ IoError::other(format!(
808
+ "failed to connect to remote app server at `{endpoint}`: {err}"
809
+ ))
810
+ })?;
811
+ match peer_policy {
812
+ SocketPeerPolicy::ExplicitEndpoint => {}
813
+ #[cfg(windows)]
814
+ SocketPeerPolicy::NonElevatedCurrentUser => stream.ensure_non_elevated_peer()?,
815
+ }
816
+ let websocket_config = remote_websocket_config();
817
+ let stream = timeout(
818
+ CONNECT_TIMEOUT,
819
+ client_async_with_config(request, stream, Some(websocket_config)),
820
+ )
821
+ .await
822
+ .map_err(|_| {
823
+ IoError::new(
824
+ ErrorKind::TimedOut,
825
+ format!("timed out upgrading remote app server at `{endpoint}`"),
826
+ )
827
+ })?
828
+ .map(|(stream, _response)| stream)
829
+ .map_err(|err| {
830
+ IoError::other(format!(
831
+ "failed to upgrade remote app server at `{endpoint}`: {err}"
832
+ ))
833
+ })?;
834
+
835
+ Ok((endpoint, stream))
836
+ }
837
+
838
+ fn remote_websocket_config() -> WebSocketConfig {
839
+ WebSocketConfig::default()
840
+ .max_frame_size(Some(REMOTE_APP_SERVER_MAX_WEBSOCKET_MESSAGE_SIZE))
841
+ .max_message_size(Some(REMOTE_APP_SERVER_MAX_WEBSOCKET_MESSAGE_SIZE))
842
+ }
843
+
844
+ async fn initialize_remote_connection<S>(
845
+ stream: &mut WebSocketStream<S>,
846
+ endpoint: &str,
847
+ params: InitializeParams,
848
+ initialize_timeout: Duration,
849
+ ) -> IoResult<(Vec<AppServerEvent>, RemoteServerMetadata)>
850
+ where
851
+ S: AsyncRead + AsyncWrite + Unpin,
852
+ {
853
+ let initialize_request_id = RequestId::String("initialize".to_string());
854
+ let mut pending_events = Vec::new();
855
+ let mut metadata = RemoteServerMetadata::default();
856
+ write_jsonrpc_message(
857
+ stream,
858
+ JSONRPCMessage::Request(jsonrpc_request_from_client_request(
859
+ ClientRequest::Initialize {
860
+ request_id: initialize_request_id.clone(),
861
+ params,
862
+ },
863
+ )),
864
+ endpoint,
865
+ )
866
+ .await?;
867
+
868
+ timeout(initialize_timeout, async {
869
+ loop {
870
+ match stream.next().await {
871
+ Some(Ok(Message::Text(text))) => {
872
+ let message = serde_json::from_str::<JSONRPCMessage>(&text).map_err(|err| {
873
+ IoError::other(format!(
874
+ "remote app server at `{endpoint}` sent invalid initialize response: {err}"
875
+ ))
876
+ })?;
877
+ match message {
878
+ JSONRPCMessage::Response(response) if response.id == initialize_request_id => {
879
+ metadata.server_version = response
880
+ .result
881
+ .get("userAgent")
882
+ .and_then(serde_json::Value::as_str)
883
+ .and_then(|user_agent| {
884
+ let (_, rest) = user_agent.split_once('/')?;
885
+ rest.split_whitespace().next().map(str::to_string)
886
+ });
887
+ metadata.codex_home = response
888
+ .result
889
+ .get("codexHome")
890
+ .and_then(serde_json::Value::as_str)
891
+ .filter(|codex_home| !codex_home.is_empty())
892
+ .map(str::to_string);
893
+ metadata.platform_family = response.result.get("platformFamily")
894
+ .and_then(serde_json::Value::as_str).map(str::to_string);
895
+ metadata.platform_os = response.result.get("platformOs")
896
+ .and_then(serde_json::Value::as_str).map(str::to_string);
897
+ break Ok(());
898
+ }
899
+ JSONRPCMessage::Error(error) if error.id == initialize_request_id => {
900
+ break Err(IoError::other(format!(
901
+ "remote app server at `{endpoint}` rejected initialize: {}",
902
+ error.error.message
903
+ )));
904
+ }
905
+ JSONRPCMessage::Notification(notification) => {
906
+ if let Some(event) = app_server_event_from_notification(notification) {
907
+ pending_events.push(event);
908
+ }
909
+ }
910
+ JSONRPCMessage::Request(request) => {
911
+ let request_id = request.id.clone();
912
+ let method = request.method.clone();
913
+ match ServerRequest::try_from(request) {
914
+ Ok(request) => {
915
+ pending_events
916
+ .push(AppServerEvent::ServerRequest(Box::new(request)));
917
+ }
918
+ Err(err) => {
919
+ warn!(%err, method, "rejecting unknown remote app-server request during initialize");
920
+ write_jsonrpc_message(
921
+ stream,
922
+ JSONRPCMessage::Error(JSONRPCError {
923
+ error: JSONRPCErrorError {
924
+ code: -32601,
925
+ message: format!(
926
+ "unsupported remote app-server request `{method}`"
927
+ ),
928
+ data: None,
929
+ },
930
+ id: request_id,
931
+ }),
932
+ endpoint,
933
+ )
934
+ .await?;
935
+ }
936
+ }
937
+ }
938
+ JSONRPCMessage::Response(_) | JSONRPCMessage::Error(_) => {}
939
+ }
940
+ }
941
+ Some(Ok(Message::Binary(_)))
942
+ | Some(Ok(Message::Ping(_)))
943
+ | Some(Ok(Message::Pong(_)))
944
+ | Some(Ok(Message::Frame(_))) => {}
945
+ Some(Ok(Message::Close(frame))) => {
946
+ let reason = frame
947
+ .as_ref()
948
+ .map(|frame| frame.reason.to_string())
949
+ .filter(|reason| !reason.is_empty())
950
+ .unwrap_or_else(|| "connection closed during initialize".to_string());
951
+ break Err(IoError::new(
952
+ ErrorKind::ConnectionAborted,
953
+ format!(
954
+ "remote app server at `{endpoint}` closed during initialize: {reason}"
955
+ ),
956
+ ));
957
+ }
958
+ Some(Err(err)) => {
959
+ break Err(IoError::other(format!(
960
+ "remote app server at `{endpoint}` transport failed during initialize: {err}"
961
+ )));
962
+ }
963
+ None => {
964
+ break Err(IoError::new(
965
+ ErrorKind::UnexpectedEof,
966
+ format!("remote app server at `{endpoint}` closed during initialize"),
967
+ ));
968
+ }
969
+ }
970
+ }
971
+ })
972
+ .await
973
+ .map_err(|_| {
974
+ IoError::new(
975
+ ErrorKind::TimedOut,
976
+ format!("timed out waiting for initialize response from `{endpoint}`"),
977
+ )
978
+ })??;
979
+
980
+ write_jsonrpc_message(
981
+ stream,
982
+ JSONRPCMessage::Notification(jsonrpc_notification_from_client_notification(
983
+ ClientNotification::Initialized,
984
+ )),
985
+ endpoint,
986
+ )
987
+ .await?;
988
+
989
+ Ok((pending_events, metadata))
990
+ }
991
+
992
+ fn app_server_event_from_notification(notification: JSONRPCNotification) -> Option<AppServerEvent> {
993
+ match ServerNotification::try_from(notification) {
994
+ Ok(notification) => Some(AppServerEvent::ServerNotification(Box::new(notification))),
995
+ Err(_) => None,
996
+ }
997
+ }
998
+
999
+ fn deliver_event(
1000
+ event_tx: &mpsc::UnboundedSender<AppServerEvent>,
1001
+ event: AppServerEvent,
1002
+ ) -> IoResult<()> {
1003
+ event_tx.send(event).map_err(|_| {
1004
+ IoError::new(
1005
+ ErrorKind::BrokenPipe,
1006
+ "remote app-server event consumer channel is closed",
1007
+ )
1008
+ })
1009
+ }
1010
+
1011
+ fn jsonrpc_request_from_client_request(request: ClientRequest) -> JSONRPCRequest {
1012
+ let value = match serde_json::to_value(request) {
1013
+ Ok(value) => value,
1014
+ Err(err) => panic!("client request should serialize: {err}"),
1015
+ };
1016
+ match serde_json::from_value(value) {
1017
+ Ok(request) => request,
1018
+ Err(err) => panic!("client request should encode as JSON-RPC request: {err}"),
1019
+ }
1020
+ }
1021
+
1022
+ fn jsonrpc_notification_from_client_notification(
1023
+ notification: ClientNotification,
1024
+ ) -> JSONRPCNotification {
1025
+ let value = match serde_json::to_value(notification) {
1026
+ Ok(value) => value,
1027
+ Err(err) => panic!("client notification should serialize: {err}"),
1028
+ };
1029
+ match serde_json::from_value(value) {
1030
+ Ok(notification) => notification,
1031
+ Err(err) => panic!("client notification should encode as JSON-RPC notification: {err}"),
1032
+ }
1033
+ }
1034
+
1035
+ async fn write_jsonrpc_message<S>(
1036
+ stream: &mut WebSocketStream<S>,
1037
+ message: JSONRPCMessage,
1038
+ endpoint: &str,
1039
+ ) -> IoResult<()>
1040
+ where
1041
+ S: AsyncRead + AsyncWrite + Unpin,
1042
+ {
1043
+ let payload = serde_json::to_string(&message).map_err(IoError::other)?;
1044
+ stream
1045
+ .send(Message::Text(payload.into()))
1046
+ .await
1047
+ .map_err(|err| {
1048
+ IoError::other(format!(
1049
+ "failed to write websocket message to `{endpoint}`: {err}"
1050
+ ))
1051
+ })
1052
+ }
1053
+
1054
+ fn websocket_close_error_is_already_closed(err: &TungsteniteError) -> bool {
1055
+ match err {
1056
+ TungsteniteError::ConnectionClosed | TungsteniteError::AlreadyClosed => true,
1057
+ TungsteniteError::Io(err) => matches!(
1058
+ err.kind(),
1059
+ ErrorKind::BrokenPipe | ErrorKind::ConnectionReset | ErrorKind::NotConnected
1060
+ ),
1061
+ _ => false,
1062
+ }
1063
+ }
1064
+ #[cfg(test)]
1065
+ mod tests {
1066
+ use super::*;
1067
+
1068
+ #[tokio::test]
1069
+ async fn shutdown_tolerates_worker_exit_after_command_is_queued() {
1070
+ let (command_tx, mut command_rx) = mpsc::channel(1);
1071
+ let (_event_tx, event_rx) = mpsc::unbounded_channel::<AppServerEvent>();
1072
+ let worker_handle = tokio::spawn(async move {
1073
+ let _ = command_rx.recv().await;
1074
+ });
1075
+ let client = RemoteAppServerClient {
1076
+ command_tx,
1077
+ event_rx,
1078
+ pending_events: VecDeque::new(),
1079
+ metadata: RemoteServerMetadata::default(),
1080
+ worker_handle,
1081
+ };
1082
+
1083
+ client
1084
+ .shutdown()
1085
+ .await
1086
+ .expect("shutdown should complete when worker exits first");
1087
+ }
1088
+ }
codex-rs/app-server-protocol/schema/json/AttestationGenerateResponse.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "properties": {
4
+ "token": {
5
+ "description": "Opaque client attestation token.",
6
+ "type": "string"
7
+ }
8
+ },
9
+ "required": [
10
+ "token"
11
+ ],
12
+ "title": "AttestationGenerateResponse",
13
+ "type": "object"
14
+ }
codex-rs/app-server-protocol/schema/json/ClientNotification.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "oneOf": [
4
+ {
5
+ "properties": {
6
+ "method": {
7
+ "enum": [
8
+ "initialized"
9
+ ],
10
+ "title": "InitializedNotificationMethod",
11
+ "type": "string"
12
+ }
13
+ },
14
+ "required": [
15
+ "method"
16
+ ],
17
+ "title": "InitializedNotification",
18
+ "type": "object"
19
+ }
20
+ ],
21
+ "title": "ClientNotification"
22
+ }
codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "definitions": {
4
+ "CommandExecutionApprovalDecision": {
5
+ "oneOf": [
6
+ {
7
+ "description": "User approved the command.",
8
+ "enum": [
9
+ "accept"
10
+ ],
11
+ "type": "string"
12
+ },
13
+ {
14
+ "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.",
15
+ "enum": [
16
+ "acceptForSession"
17
+ ],
18
+ "type": "string"
19
+ },
20
+ {
21
+ "additionalProperties": false,
22
+ "description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.",
23
+ "properties": {
24
+ "acceptWithExecpolicyAmendment": {
25
+ "properties": {
26
+ "execpolicy_amendment": {
27
+ "items": {
28
+ "type": "string"
29
+ },
30
+ "type": "array"
31
+ }
32
+ },
33
+ "required": [
34
+ "execpolicy_amendment"
35
+ ],
36
+ "type": "object"
37
+ }
38
+ },
39
+ "required": [
40
+ "acceptWithExecpolicyAmendment"
41
+ ],
42
+ "title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision",
43
+ "type": "object"
44
+ },
45
+ {
46
+ "additionalProperties": false,
47
+ "description": "User chose a persistent network policy rule (allow/deny) for this host.",
48
+ "properties": {
49
+ "applyNetworkPolicyAmendment": {
50
+ "properties": {
51
+ "network_policy_amendment": {
52
+ "$ref": "#/definitions/NetworkPolicyAmendment"
53
+ }
54
+ },
55
+ "required": [
56
+ "network_policy_amendment"
57
+ ],
58
+ "type": "object"
59
+ }
60
+ },
61
+ "required": [
62
+ "applyNetworkPolicyAmendment"
63
+ ],
64
+ "title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision",
65
+ "type": "object"
66
+ },
67
+ {
68
+ "description": "User denied the command. The agent will continue the turn.",
69
+ "enum": [
70
+ "decline"
71
+ ],
72
+ "type": "string"
73
+ },
74
+ {
75
+ "description": "User denied the command. The turn will also be immediately interrupted.",
76
+ "enum": [
77
+ "cancel"
78
+ ],
79
+ "type": "string"
80
+ }
81
+ ]
82
+ },
83
+ "NetworkPolicyAmendment": {
84
+ "properties": {
85
+ "action": {
86
+ "$ref": "#/definitions/NetworkPolicyRuleAction"
87
+ },
88
+ "host": {
89
+ "type": "string"
90
+ }
91
+ },
92
+ "required": [
93
+ "action",
94
+ "host"
95
+ ],
96
+ "type": "object"
97
+ },
98
+ "NetworkPolicyRuleAction": {
99
+ "enum": [
100
+ "allow",
101
+ "deny"
102
+ ],
103
+ "type": "string"
104
+ }
105
+ },
106
+ "properties": {
107
+ "decision": {
108
+ "$ref": "#/definitions/CommandExecutionApprovalDecision"
109
+ }
110
+ },
111
+ "required": [
112
+ "decision"
113
+ ],
114
+ "title": "CommandExecutionRequestApprovalResponse",
115
+ "type": "object"
116
+ }
codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "definitions": {
4
+ "NetworkPolicyAmendment": {
5
+ "properties": {
6
+ "action": {
7
+ "$ref": "#/definitions/NetworkPolicyRuleAction"
8
+ },
9
+ "host": {
10
+ "type": "string"
11
+ }
12
+ },
13
+ "required": [
14
+ "action",
15
+ "host"
16
+ ],
17
+ "type": "object"
18
+ },
19
+ "NetworkPolicyRuleAction": {
20
+ "enum": [
21
+ "allow",
22
+ "deny"
23
+ ],
24
+ "type": "string"
25
+ },
26
+ "ReviewDecision": {
27
+ "description": "User's decision in response to an ExecApprovalRequest.",
28
+ "oneOf": [
29
+ {
30
+ "description": "User has approved this command and the agent should execute it.",
31
+ "enum": [
32
+ "approved"
33
+ ],
34
+ "type": "string"
35
+ },
36
+ {
37
+ "additionalProperties": false,
38
+ "description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.",
39
+ "properties": {
40
+ "approved_execpolicy_amendment": {
41
+ "properties": {
42
+ "proposed_execpolicy_amendment": {
43
+ "items": {
44
+ "type": "string"
45
+ },
46
+ "type": "array"
47
+ }
48
+ },
49
+ "required": [
50
+ "proposed_execpolicy_amendment"
51
+ ],
52
+ "type": "object"
53
+ }
54
+ },
55
+ "required": [
56
+ "approved_execpolicy_amendment"
57
+ ],
58
+ "title": "ApprovedExecpolicyAmendmentReviewDecision",
59
+ "type": "object"
60
+ },
61
+ {
62
+ "description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.",
63
+ "enum": [
64
+ "approved_for_session"
65
+ ],
66
+ "type": "string"
67
+ },
68
+ {
69
+ "description": "User has approved this MCP tool call and wants to amend its policy so matching future calls are automatically approved across sessions.",
70
+ "enum": [
71
+ "approved_mcp_policy_amendment"
72
+ ],
73
+ "type": "string"
74
+ },
75
+ {
76
+ "additionalProperties": false,
77
+ "description": "User chose to persist a network policy rule (allow/deny) for future requests to the same host.",
78
+ "properties": {
79
+ "network_policy_amendment": {
80
+ "properties": {
81
+ "network_policy_amendment": {
82
+ "$ref": "#/definitions/NetworkPolicyAmendment"
83
+ }
84
+ },
85
+ "required": [
86
+ "network_policy_amendment"
87
+ ],
88
+ "type": "object"
89
+ }
90
+ },
91
+ "required": [
92
+ "network_policy_amendment"
93
+ ],
94
+ "title": "NetworkPolicyAmendmentReviewDecision",
95
+ "type": "object"
96
+ },
97
+ {
98
+ "additionalProperties": false,
99
+ "description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.",
100
+ "properties": {
101
+ "denied": {
102
+ "properties": {
103
+ "rejection": {
104
+ "type": "string"
105
+ }
106
+ },
107
+ "required": [
108
+ "rejection"
109
+ ],
110
+ "type": "object"
111
+ }
112
+ },
113
+ "required": [
114
+ "denied"
115
+ ],
116
+ "title": "DeniedReviewDecision",
117
+ "type": "object"
118
+ },
119
+ {
120
+ "description": "Automatic approval review timed out before reaching a decision.",
121
+ "enum": [
122
+ "timed_out"
123
+ ],
124
+ "type": "string"
125
+ },
126
+ {
127
+ "description": "User has denied this command and the agent should not do anything until the user's next command.",
128
+ "enum": [
129
+ "abort"
130
+ ],
131
+ "type": "string"
132
+ }
133
+ ]
134
+ }
135
+ },
136
+ "properties": {
137
+ "decision": {
138
+ "$ref": "#/definitions/ReviewDecision"
139
+ }
140
+ },
141
+ "required": [
142
+ "decision"
143
+ ],
144
+ "title": "ExecCommandApprovalResponse",
145
+ "type": "object"
146
+ }
codex-rs/app-server-protocol/schema/json/FileChangeRequestApprovalResponse.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "definitions": {
4
+ "FileChangeApprovalDecision": {
5
+ "oneOf": [
6
+ {
7
+ "description": "User approved the file changes.",
8
+ "enum": [
9
+ "accept"
10
+ ],
11
+ "type": "string"
12
+ },
13
+ {
14
+ "description": "User approved the file changes and future changes to the same files should run without prompting.",
15
+ "enum": [
16
+ "acceptForSession"
17
+ ],
18
+ "type": "string"
19
+ },
20
+ {
21
+ "description": "User denied the file changes. The agent will continue the turn.",
22
+ "enum": [
23
+ "decline"
24
+ ],
25
+ "type": "string"
26
+ },
27
+ {
28
+ "description": "User denied the file changes. The turn will also be immediately interrupted.",
29
+ "enum": [
30
+ "cancel"
31
+ ],
32
+ "type": "string"
33
+ }
34
+ ]
35
+ }
36
+ },
37
+ "properties": {
38
+ "decision": {
39
+ "$ref": "#/definitions/FileChangeApprovalDecision"
40
+ }
41
+ },
42
+ "required": [
43
+ "decision"
44
+ ],
45
+ "title": "FileChangeRequestApprovalResponse",
46
+ "type": "object"
47
+ }
codex-rs/app-server-protocol/schema/json/FuzzyFileSearchParams.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "properties": {
4
+ "cancellationToken": {
5
+ "type": [
6
+ "string",
7
+ "null"
8
+ ]
9
+ },
10
+ "query": {
11
+ "type": "string"
12
+ },
13
+ "roots": {
14
+ "items": {
15
+ "type": "string"
16
+ },
17
+ "type": "array"
18
+ }
19
+ },
20
+ "required": [
21
+ "query",
22
+ "roots"
23
+ ],
24
+ "title": "FuzzyFileSearchParams",
25
+ "type": "object"
26
+ }
codex-rs/app-server-protocol/schema/json/FuzzyFileSearchSessionUpdatedNotification.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "definitions": {
4
+ "FuzzyFileSearchMatchType": {
5
+ "enum": [
6
+ "file",
7
+ "directory"
8
+ ],
9
+ "type": "string"
10
+ },
11
+ "FuzzyFileSearchResult": {
12
+ "description": "Superset of [`codex_file_search::FileMatch`]",
13
+ "properties": {
14
+ "file_name": {
15
+ "type": "string"
16
+ },
17
+ "indices": {
18
+ "items": {
19
+ "format": "uint32",
20
+ "minimum": 0.0,
21
+ "type": "integer"
22
+ },
23
+ "type": [
24
+ "array",
25
+ "null"
26
+ ]
27
+ },
28
+ "match_type": {
29
+ "$ref": "#/definitions/FuzzyFileSearchMatchType"
30
+ },
31
+ "path": {
32
+ "type": "string"
33
+ },
34
+ "root": {
35
+ "type": "string"
36
+ },
37
+ "score": {
38
+ "format": "uint32",
39
+ "minimum": 0.0,
40
+ "type": "integer"
41
+ }
42
+ },
43
+ "required": [
44
+ "file_name",
45
+ "match_type",
46
+ "path",
47
+ "root",
48
+ "score"
49
+ ],
50
+ "type": "object"
51
+ }
52
+ },
53
+ "properties": {
54
+ "files": {
55
+ "items": {
56
+ "$ref": "#/definitions/FuzzyFileSearchResult"
57
+ },
58
+ "type": "array"
59
+ },
60
+ "query": {
61
+ "type": "string"
62
+ },
63
+ "sessionId": {
64
+ "type": "string"
65
+ }
66
+ },
67
+ "required": [
68
+ "files",
69
+ "query",
70
+ "sessionId"
71
+ ],
72
+ "title": "FuzzyFileSearchSessionUpdatedNotification",
73
+ "type": "object"
74
+ }
codex-rs/app-server-protocol/schema/json/JSONRPCError.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "definitions": {
4
+ "JSONRPCErrorError": {
5
+ "properties": {
6
+ "code": {
7
+ "format": "int64",
8
+ "type": "integer"
9
+ },
10
+ "data": true,
11
+ "message": {
12
+ "type": "string"
13
+ }
14
+ },
15
+ "required": [
16
+ "code",
17
+ "message"
18
+ ],
19
+ "type": "object"
20
+ },
21
+ "RequestId": {
22
+ "anyOf": [
23
+ {
24
+ "type": "string"
25
+ },
26
+ {
27
+ "format": "int64",
28
+ "type": "integer"
29
+ }
30
+ ]
31
+ }
32
+ },
33
+ "description": "A response to a request that indicates an error occurred.",
34
+ "properties": {
35
+ "error": {
36
+ "$ref": "#/definitions/JSONRPCErrorError"
37
+ },
38
+ "id": {
39
+ "$ref": "#/definitions/RequestId"
40
+ }
41
+ },
42
+ "required": [
43
+ "error",
44
+ "id"
45
+ ],
46
+ "title": "JSONRPCError",
47
+ "type": "object"
48
+ }
codex-rs/app-server-protocol/schema/json/JSONRPCNotification.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "description": "A notification which does not expect a response.",
4
+ "properties": {
5
+ "method": {
6
+ "type": "string"
7
+ },
8
+ "params": true
9
+ },
10
+ "required": [
11
+ "method"
12
+ ],
13
+ "title": "JSONRPCNotification",
14
+ "type": "object"
15
+ }
codex-rs/app-server-protocol/schema/json/JSONRPCRequest.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "definitions": {
4
+ "RequestId": {
5
+ "anyOf": [
6
+ {
7
+ "type": "string"
8
+ },
9
+ {
10
+ "format": "int64",
11
+ "type": "integer"
12
+ }
13
+ ]
14
+ },
15
+ "W3cTraceContext": {
16
+ "properties": {
17
+ "traceparent": {
18
+ "type": [
19
+ "string",
20
+ "null"
21
+ ]
22
+ },
23
+ "tracestate": {
24
+ "type": [
25
+ "string",
26
+ "null"
27
+ ]
28
+ }
29
+ },
30
+ "type": "object"
31
+ }
32
+ },
33
+ "description": "A request that expects a response.",
34
+ "properties": {
35
+ "id": {
36
+ "$ref": "#/definitions/RequestId"
37
+ },
38
+ "method": {
39
+ "type": "string"
40
+ },
41
+ "params": true,
42
+ "trace": {
43
+ "anyOf": [
44
+ {
45
+ "$ref": "#/definitions/W3cTraceContext"
46
+ },
47
+ {
48
+ "type": "null"
49
+ }
50
+ ],
51
+ "description": "Optional W3C Trace Context for distributed tracing."
52
+ }
53
+ },
54
+ "required": [
55
+ "id",
56
+ "method"
57
+ ],
58
+ "title": "JSONRPCRequest",
59
+ "type": "object"
60
+ }
codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestResponse.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "definitions": {
4
+ "McpServerElicitationAction": {
5
+ "enum": [
6
+ "accept",
7
+ "decline",
8
+ "cancel"
9
+ ],
10
+ "type": "string"
11
+ }
12
+ },
13
+ "properties": {
14
+ "_meta": {
15
+ "description": "Optional client metadata for form-mode action handling."
16
+ },
17
+ "action": {
18
+ "$ref": "#/definitions/McpServerElicitationAction"
19
+ },
20
+ "content": {
21
+ "description": "Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.\n\nThis is nullable because decline/cancel responses have no content."
22
+ }
23
+ },
24
+ "required": [
25
+ "action"
26
+ ],
27
+ "title": "McpServerElicitationRequestResponse",
28
+ "type": "object"
29
+ }
codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "definitions": {
4
+ "AdditionalFileSystemPermissions": {
5
+ "properties": {
6
+ "entries": {
7
+ "items": {
8
+ "$ref": "#/definitions/FileSystemSandboxEntry"
9
+ },
10
+ "type": [
11
+ "array",
12
+ "null"
13
+ ]
14
+ },
15
+ "globScanMaxDepth": {
16
+ "format": "uint",
17
+ "minimum": 1.0,
18
+ "type": [
19
+ "integer",
20
+ "null"
21
+ ]
22
+ },
23
+ "read": {
24
+ "description": "This will be removed in favor of `entries`.",
25
+ "items": {
26
+ "$ref": "#/definitions/LegacyAppPathString"
27
+ },
28
+ "type": [
29
+ "array",
30
+ "null"
31
+ ]
32
+ },
33
+ "write": {
34
+ "description": "This will be removed in favor of `entries`.",
35
+ "items": {
36
+ "$ref": "#/definitions/LegacyAppPathString"
37
+ },
38
+ "type": [
39
+ "array",
40
+ "null"
41
+ ]
42
+ }
43
+ },
44
+ "type": "object"
45
+ },
46
+ "AdditionalNetworkPermissions": {
47
+ "properties": {
48
+ "enabled": {
49
+ "type": [
50
+ "boolean",
51
+ "null"
52
+ ]
53
+ }
54
+ },
55
+ "type": "object"
56
+ },
57
+ "FileSystemAccessMode": {
58
+ "enum": [
59
+ "read",
60
+ "write",
61
+ "deny"
62
+ ],
63
+ "type": "string"
64
+ },
65
+ "FileSystemPath": {
66
+ "oneOf": [
67
+ {
68
+ "properties": {
69
+ "path": {
70
+ "$ref": "#/definitions/LegacyAppPathString"
71
+ },
72
+ "type": {
73
+ "enum": [
74
+ "path"
75
+ ],
76
+ "title": "PathFileSystemPathType",
77
+ "type": "string"
78
+ }
79
+ },
80
+ "required": [
81
+ "path",
82
+ "type"
83
+ ],
84
+ "title": "PathFileSystemPath",
85
+ "type": "object"
86
+ },
87
+ {
88
+ "properties": {
89
+ "pattern": {
90
+ "type": "string"
91
+ },
92
+ "type": {
93
+ "enum": [
94
+ "glob_pattern"
95
+ ],
96
+ "title": "GlobPatternFileSystemPathType",
97
+ "type": "string"
98
+ }
99
+ },
100
+ "required": [
101
+ "pattern",
102
+ "type"
103
+ ],
104
+ "title": "GlobPatternFileSystemPath",
105
+ "type": "object"
106
+ },
107
+ {
108
+ "properties": {
109
+ "type": {
110
+ "enum": [
111
+ "special"
112
+ ],
113
+ "title": "SpecialFileSystemPathType",
114
+ "type": "string"
115
+ },
116
+ "value": {
117
+ "$ref": "#/definitions/FileSystemSpecialPath"
118
+ }
119
+ },
120
+ "required": [
121
+ "type",
122
+ "value"
123
+ ],
124
+ "title": "SpecialFileSystemPath",
125
+ "type": "object"
126
+ }
127
+ ]
128
+ },
129
+ "FileSystemSandboxEntry": {
130
+ "properties": {
131
+ "access": {
132
+ "$ref": "#/definitions/FileSystemAccessMode"
133
+ },
134
+ "path": {
135
+ "$ref": "#/definitions/FileSystemPath"
136
+ }
137
+ },
138
+ "required": [
139
+ "access",
140
+ "path"
141
+ ],
142
+ "type": "object"
143
+ },
144
+ "FileSystemSpecialPath": {
145
+ "oneOf": [
146
+ {
147
+ "properties": {
148
+ "kind": {
149
+ "enum": [
150
+ "root"
151
+ ],
152
+ "type": "string"
153
+ }
154
+ },
155
+ "required": [
156
+ "kind"
157
+ ],
158
+ "title": "RootFileSystemSpecialPath",
159
+ "type": "object"
160
+ },
161
+ {
162
+ "properties": {
163
+ "kind": {
164
+ "enum": [
165
+ "minimal"
166
+ ],
167
+ "type": "string"
168
+ }
169
+ },
170
+ "required": [
171
+ "kind"
172
+ ],
173
+ "title": "MinimalFileSystemSpecialPath",
174
+ "type": "object"
175
+ },
176
+ {
177
+ "properties": {
178
+ "kind": {
179
+ "enum": [
180
+ "project_roots"
181
+ ],
182
+ "type": "string"
183
+ },
184
+ "subpath": {
185
+ "anyOf": [
186
+ {
187
+ "$ref": "#/definitions/LegacyAppPathString"
188
+ },
189
+ {
190
+ "type": "null"
191
+ }
192
+ ]
193
+ }
194
+ },
195
+ "required": [
196
+ "kind"
197
+ ],
198
+ "title": "KindFileSystemSpecialPath",
199
+ "type": "object"
200
+ },
201
+ {
202
+ "properties": {
203
+ "kind": {
204
+ "enum": [
205
+ "tmpdir"
206
+ ],
207
+ "type": "string"
208
+ }
209
+ },
210
+ "required": [
211
+ "kind"
212
+ ],
213
+ "title": "TmpdirFileSystemSpecialPath",
214
+ "type": "object"
215
+ },
216
+ {
217
+ "properties": {
218
+ "kind": {
219
+ "enum": [
220
+ "slash_tmp"
221
+ ],
222
+ "type": "string"
223
+ }
224
+ },
225
+ "required": [
226
+ "kind"
227
+ ],
228
+ "title": "SlashTmpFileSystemSpecialPath",
229
+ "type": "object"
230
+ },
231
+ {
232
+ "properties": {
233
+ "kind": {
234
+ "enum": [
235
+ "unknown"
236
+ ],
237
+ "type": "string"
238
+ },
239
+ "path": {
240
+ "type": "string"
241
+ },
242
+ "subpath": {
243
+ "anyOf": [
244
+ {
245
+ "$ref": "#/definitions/LegacyAppPathString"
246
+ },
247
+ {
248
+ "type": "null"
249
+ }
250
+ ]
251
+ }
252
+ },
253
+ "required": [
254
+ "kind",
255
+ "path"
256
+ ],
257
+ "type": "object"
258
+ }
259
+ ]
260
+ },
261
+ "GrantedPermissionProfile": {
262
+ "properties": {
263
+ "fileSystem": {
264
+ "anyOf": [
265
+ {
266
+ "$ref": "#/definitions/AdditionalFileSystemPermissions"
267
+ },
268
+ {
269
+ "type": "null"
270
+ }
271
+ ]
272
+ },
273
+ "network": {
274
+ "anyOf": [
275
+ {
276
+ "$ref": "#/definitions/AdditionalNetworkPermissions"
277
+ },
278
+ {
279
+ "type": "null"
280
+ }
281
+ ]
282
+ }
283
+ },
284
+ "type": "object"
285
+ },
286
+ "LegacyAppPathString": {
287
+ "type": "string"
288
+ },
289
+ "PermissionGrantScope": {
290
+ "enum": [
291
+ "turn",
292
+ "session"
293
+ ],
294
+ "type": "string"
295
+ }
296
+ },
297
+ "properties": {
298
+ "permissions": {
299
+ "$ref": "#/definitions/GrantedPermissionProfile"
300
+ },
301
+ "scope": {
302
+ "allOf": [
303
+ {
304
+ "$ref": "#/definitions/PermissionGrantScope"
305
+ }
306
+ ],
307
+ "default": "turn"
308
+ },
309
+ "strictAutoReview": {
310
+ "description": "Review every subsequent command in this turn before normal sandboxed execution.",
311
+ "type": [
312
+ "boolean",
313
+ "null"
314
+ ]
315
+ }
316
+ },
317
+ "required": [
318
+ "permissions"
319
+ ],
320
+ "title": "PermissionsRequestApprovalResponse",
321
+ "type": "object"
322
+ }
codex-rs/app-server-protocol/schema/json/ToolRequestUserInputResponse.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "definitions": {
4
+ "ToolRequestUserInputAnswer": {
5
+ "description": "EXPERIMENTAL. Captures a user's answer to a request_user_input question.",
6
+ "properties": {
7
+ "answers": {
8
+ "items": {
9
+ "type": "string"
10
+ },
11
+ "type": "array"
12
+ }
13
+ },
14
+ "required": [
15
+ "answers"
16
+ ],
17
+ "type": "object"
18
+ }
19
+ },
20
+ "description": "EXPERIMENTAL. Response payload mapping question ids to answers.",
21
+ "properties": {
22
+ "answers": {
23
+ "additionalProperties": {
24
+ "$ref": "#/definitions/ToolRequestUserInputAnswer"
25
+ },
26
+ "type": "object"
27
+ }
28
+ },
29
+ "required": [
30
+ "answers"
31
+ ],
32
+ "title": "ToolRequestUserInputResponse",
33
+ "type": "object"
34
+ }
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json ADDED
The diff for this file is too large to render. See raw diff
 
codex-rs/app-server-protocol/schema/typescript/AbsolutePathBuf.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ /**
6
+ * A path that is guaranteed to be absolute and normalized (though it is not
7
+ * guaranteed to be canonicalized or exist on the filesystem).
8
+ *
9
+ * IMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set
10
+ * using [AbsolutePathBufGuard::new]. If no base path is set, the
11
+ * deserialization will fail unless the path being deserialized is already
12
+ * absolute.
13
+ */
14
+ export type AbsolutePathBuf = string;
codex-rs/app-server-protocol/schema/typescript/AgentMessageInputContent.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type AgentMessageInputContent = { "type": "input_text", text: string, } | { "type": "encrypted_content", encrypted_content: string, };
codex-rs/app-server-protocol/schema/typescript/AgentPath.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type AgentPath = string;
codex-rs/app-server-protocol/schema/typescript/ApplyPatchApprovalParams.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { FileChange } from "./FileChange";
5
+ import type { ThreadId } from "./ThreadId";
6
+
7
+ export type ApplyPatchApprovalParams = { conversationId: ThreadId,
8
+ /**
9
+ * Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent]
10
+ * and [codex_protocol::protocol::PatchApplyEndEvent].
11
+ */
12
+ callId: string, fileChanges: { [key in string]?: FileChange },
13
+ /**
14
+ * Optional explanatory reason (e.g. request for extra write access).
15
+ */
16
+ reason: string | null,
17
+ /**
18
+ * When set, the agent is asking the user to allow writes under this root
19
+ * for the remainder of the session (unclear if this is honored today).
20
+ */
21
+ grantRoot: string | null, };
codex-rs/app-server-protocol/schema/typescript/ApplyPatchApprovalResponse.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { ReviewDecision } from "./ReviewDecision";
5
+
6
+ export type ApplyPatchApprovalResponse = { decision: ReviewDecision, };
codex-rs/app-server-protocol/schema/typescript/AuthMode.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ /**
6
+ * Authentication mode for OpenAI-backed providers.
7
+ */
8
+ export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "headers" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey" | "bedrockAccessKeys";
codex-rs/app-server-protocol/schema/typescript/AutoCompactTokenLimitScope.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ /**
6
+ * Selects which part of the active context is charged against
7
+ * `model_auto_compact_token_limit`.
8
+ */
9
+ export type AutoCompactTokenLimitScope = "total" | "body_after_prefix";
codex-rs/app-server-protocol/schema/typescript/ClientInfo.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type ClientInfo = { name: string, title: string | null, version: string, };
codex-rs/app-server-protocol/schema/typescript/ClientNotification.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type ClientNotification = { "method": "initialized" };
codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams";
5
+ import type { GetAuthStatusParams } from "./GetAuthStatusParams";
6
+ import type { GetConversationSummaryParams } from "./GetConversationSummaryParams";
7
+ import type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams";
8
+ import type { InitializeParams } from "./InitializeParams";
9
+ import type { RequestId } from "./RequestId";
10
+ import type { AppsInstalledParams } from "./v2/AppsInstalledParams";
11
+ import type { AppsListParams } from "./v2/AppsListParams";
12
+ import type { AppsReadParams } from "./v2/AppsReadParams";
13
+ import type { CancelLoginAccountParams } from "./v2/CancelLoginAccountParams";
14
+ import type { CommandExecParams } from "./v2/CommandExecParams";
15
+ import type { CommandExecResizeParams } from "./v2/CommandExecResizeParams";
16
+ import type { CommandExecTerminateParams } from "./v2/CommandExecTerminateParams";
17
+ import type { CommandExecWriteParams } from "./v2/CommandExecWriteParams";
18
+ import type { ConfigBatchWriteParams } from "./v2/ConfigBatchWriteParams";
19
+ import type { ConfigReadParams } from "./v2/ConfigReadParams";
20
+ import type { ConfigValueWriteParams } from "./v2/ConfigValueWriteParams";
21
+ import type { ConsumeAccountRateLimitResetCreditParams } from "./v2/ConsumeAccountRateLimitResetCreditParams";
22
+ import type { ExperimentalFeatureEnablementSetParams } from "./v2/ExperimentalFeatureEnablementSetParams";
23
+ import type { ExperimentalFeatureListParams } from "./v2/ExperimentalFeatureListParams";
24
+ import type { ExternalAgentConfigDetectParams } from "./v2/ExternalAgentConfigDetectParams";
25
+ import type { ExternalAgentConfigImportHistoryRecordParams } from "./v2/ExternalAgentConfigImportHistoryRecordParams";
26
+ import type { ExternalAgentConfigImportParams } from "./v2/ExternalAgentConfigImportParams";
27
+ import type { FeedbackUploadParams } from "./v2/FeedbackUploadParams";
28
+ import type { FsCopyParams } from "./v2/FsCopyParams";
29
+ import type { FsCreateDirectoryParams } from "./v2/FsCreateDirectoryParams";
30
+ import type { FsGetMetadataParams } from "./v2/FsGetMetadataParams";
31
+ import type { FsReadDirectoryParams } from "./v2/FsReadDirectoryParams";
32
+ import type { FsReadFileParams } from "./v2/FsReadFileParams";
33
+ import type { FsRemoveParams } from "./v2/FsRemoveParams";
34
+ import type { FsUnwatchParams } from "./v2/FsUnwatchParams";
35
+ import type { FsWatchParams } from "./v2/FsWatchParams";
36
+ import type { FsWriteFileParams } from "./v2/FsWriteFileParams";
37
+ import type { GetAccountParams } from "./v2/GetAccountParams";
38
+ import type { GetAccountRateLimitsParams } from "./v2/GetAccountRateLimitsParams";
39
+ import type { GetAccountTokenUsageParams } from "./v2/GetAccountTokenUsageParams";
40
+ import type { HooksListParams } from "./v2/HooksListParams";
41
+ import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams";
42
+ import type { LoginAccountParams } from "./v2/LoginAccountParams";
43
+ import type { MarketplaceAddParams } from "./v2/MarketplaceAddParams";
44
+ import type { MarketplaceRemoveParams } from "./v2/MarketplaceRemoveParams";
45
+ import type { MarketplaceUpgradeParams } from "./v2/MarketplaceUpgradeParams";
46
+ import type { McpResourceReadParams } from "./v2/McpResourceReadParams";
47
+ import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams";
48
+ import type { McpServerToolCallParams } from "./v2/McpServerToolCallParams";
49
+ import type { ModelListParams } from "./v2/ModelListParams";
50
+ import type { ModelProviderCapabilitiesReadParams } from "./v2/ModelProviderCapabilitiesReadParams";
51
+ import type { PermissionProfileListParams } from "./v2/PermissionProfileListParams";
52
+ import type { PluginInstallParams } from "./v2/PluginInstallParams";
53
+ import type { PluginInstalledParams } from "./v2/PluginInstalledParams";
54
+ import type { PluginListParams } from "./v2/PluginListParams";
55
+ import type { PluginReadParams } from "./v2/PluginReadParams";
56
+ import type { PluginReconcileParams } from "./v2/PluginReconcileParams";
57
+ import type { PluginShareCheckoutParams } from "./v2/PluginShareCheckoutParams";
58
+ import type { PluginShareDeleteParams } from "./v2/PluginShareDeleteParams";
59
+ import type { PluginShareListParams } from "./v2/PluginShareListParams";
60
+ import type { PluginShareSaveParams } from "./v2/PluginShareSaveParams";
61
+ import type { PluginShareUpdateTargetsParams } from "./v2/PluginShareUpdateTargetsParams";
62
+ import type { PluginSkillReadParams } from "./v2/PluginSkillReadParams";
63
+ import type { PluginUninstallParams } from "./v2/PluginUninstallParams";
64
+ import type { ReviewStartParams } from "./v2/ReviewStartParams";
65
+ import type { SendAddCreditsNudgeEmailParams } from "./v2/SendAddCreditsNudgeEmailParams";
66
+ import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams";
67
+ import type { SkillsExtraRootsSetParams } from "./v2/SkillsExtraRootsSetParams";
68
+ import type { SkillsListParams } from "./v2/SkillsListParams";
69
+ import type { ThreadApproveGuardianDeniedActionParams } from "./v2/ThreadApproveGuardianDeniedActionParams";
70
+ import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams";
71
+ import type { ThreadAttachmentAddParams } from "./v2/ThreadAttachmentAddParams";
72
+ import type { ThreadAttachmentListParams } from "./v2/ThreadAttachmentListParams";
73
+ import type { ThreadAttachmentRemoveParams } from "./v2/ThreadAttachmentRemoveParams";
74
+ import type { ThreadCompactStartParams } from "./v2/ThreadCompactStartParams";
75
+ import type { ThreadDeleteParams } from "./v2/ThreadDeleteParams";
76
+ import type { ThreadForkParams } from "./v2/ThreadForkParams";
77
+ import type { ThreadGoalClearParams } from "./v2/ThreadGoalClearParams";
78
+ import type { ThreadGoalGetParams } from "./v2/ThreadGoalGetParams";
79
+ import type { ThreadGoalSetParams } from "./v2/ThreadGoalSetParams";
80
+ import type { ThreadInjectItemsParams } from "./v2/ThreadInjectItemsParams";
81
+ import type { ThreadItemsListParams } from "./v2/ThreadItemsListParams";
82
+ import type { ThreadListParams } from "./v2/ThreadListParams";
83
+ import type { ThreadLoadedListParams } from "./v2/ThreadLoadedListParams";
84
+ import type { ThreadMetadataUpdateParams } from "./v2/ThreadMetadataUpdateParams";
85
+ import type { ThreadReadParams } from "./v2/ThreadReadParams";
86
+ import type { ThreadResumeParams } from "./v2/ThreadResumeParams";
87
+ import type { ThreadRevertParams } from "./v2/ThreadRevertParams";
88
+ import type { ThreadSectionCreateParams } from "./v2/ThreadSectionCreateParams";
89
+ import type { ThreadSectionDeleteParams } from "./v2/ThreadSectionDeleteParams";
90
+ import type { ThreadSectionListParams } from "./v2/ThreadSectionListParams";
91
+ import type { ThreadSectionMoveParams } from "./v2/ThreadSectionMoveParams";
92
+ import type { ThreadSectionUpdateParams } from "./v2/ThreadSectionUpdateParams";
93
+ import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams";
94
+ import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams";
95
+ import type { ThreadStartParams } from "./v2/ThreadStartParams";
96
+ import type { ThreadTurnsListParams } from "./v2/ThreadTurnsListParams";
97
+ import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams";
98
+ import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams";
99
+ import type { TurnInterruptParams } from "./v2/TurnInterruptParams";
100
+ import type { TurnStartParams } from "./v2/TurnStartParams";
101
+ import type { TurnSteerParams } from "./v2/TurnSteerParams";
102
+ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupStartParams";
103
+
104
+ /**
105
+ * Request from the client to the server.
106
+ */
107
+ export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/attachment/add", id: RequestId, params: ThreadAttachmentAddParams, } | { "method": "thread/attachment/list", id: RequestId, params: ThreadAttachmentListParams, } | { "method": "thread/attachment/remove", id: RequestId, params: ThreadAttachmentRemoveParams, } | { "method": "thread/section/move", id: RequestId, params: ThreadSectionMoveParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/revert", id: RequestId, params: ThreadRevertParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "threadSection/list", id: RequestId, params: ThreadSectionListParams, } | { "method": "threadSection/create", id: RequestId, params: ThreadSectionCreateParams, } | { "method": "threadSection/update", id: RequestId, params: ThreadSectionUpdateParams, } | { "method": "threadSection/delete", id: RequestId, params: ThreadSectionDeleteParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/items/list", id: RequestId, params: ThreadItemsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/reconcile", id: RequestId, params: PluginReconcileParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/read", id: RequestId, params: AppsReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "app/installed", id: RequestId, params: AppsInstalledParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params?: GetAccountRateLimitsParams | undefined, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params?: GetAccountTokenUsageParams | undefined, } | { "method": "account/workspaceMessages/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "externalAgentConfig/import/recordHistory", id: RequestId, params: ExternalAgentConfigImportHistoryRecordParams, } | { "method": "externalAgentConfig/import/readHistories", id: RequestId, params: undefined, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, };
codex-rs/app-server-protocol/schema/typescript/CodexResponseHandoffMode.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type CodexResponseHandoffMode = "thinking" | "commentary" | "bemTags";
codex-rs/app-server-protocol/schema/typescript/CollaborationMode.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { ModeKind } from "./ModeKind";
5
+ import type { Settings } from "./Settings";
6
+
7
+ /**
8
+ * Collaboration mode for a Codex session.
9
+ */
10
+ export type CollaborationMode = { mode: ModeKind, settings: Settings, };
codex-rs/app-server-protocol/schema/typescript/ConfigurationReasoning.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { ReasoningEffort } from "./ReasoningEffort";
5
+
6
+ /**
7
+ * Reasoning settings interpreted by the backend for the routed model.
8
+ */
9
+ export type ConfigurationReasoning = { effort: ReasoningEffort, };
codex-rs/app-server-protocol/schema/typescript/ContentItem.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { ImageDetail } from "./ImageDetail";
5
+
6
+ export type ContentItem = { "type": "input_text", text: string, } | { "type": "input_image", detail?: ImageDetail, } & ({ image_url: string, } | { file_id: string, }) | { "type": "input_audio", audio_url: string, } | { "type": "output_text", text: string, };
codex-rs/app-server-protocol/schema/typescript/ConversationGitInfo.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type ConversationGitInfo = { sha: string | null, branch: string | null, origin_url: string | null, };
codex-rs/app-server-protocol/schema/typescript/ConversationSummary.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { ConversationGitInfo } from "./ConversationGitInfo";
5
+ import type { SessionSource } from "./SessionSource";
6
+ import type { ThreadId } from "./ThreadId";
7
+
8
+ export type ConversationSummary = { conversationId: ThreadId, path: string, preview: string, timestamp: string | null, updatedAt: string | null, modelProvider: string, cwd: string, cliVersion: string, source: SessionSource, gitInfo: ConversationGitInfo | null, };
codex-rs/app-server-protocol/schema/typescript/ConversationTextRole.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type ConversationTextRole = "user" | "developer" | "assistant";
codex-rs/app-server-protocol/schema/typescript/ExecCommandApprovalParams.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { ParsedCommand } from "./ParsedCommand";
5
+ import type { ThreadId } from "./ThreadId";
6
+
7
+ export type ExecCommandApprovalParams = { conversationId: ThreadId,
8
+ /**
9
+ * Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent]
10
+ * and [codex_protocol::protocol::ExecCommandEndEvent].
11
+ */
12
+ callId: string,
13
+ /**
14
+ * Identifier for this specific approval callback.
15
+ */
16
+ approvalId: string | null, command: Array<string>, cwd: string, reason: string | null, parsedCmd: Array<ParsedCommand>, };
codex-rs/app-server-protocol/schema/typescript/ExecCommandApprovalResponse.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { ReviewDecision } from "./ReviewDecision";
5
+
6
+ export type ExecCommandApprovalResponse = { decision: ReviewDecision, };
codex-rs/app-server-protocol/schema/typescript/ExecPolicyAmendment.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ /**
6
+ * Proposed execpolicy change to allow commands starting with this prefix.
7
+ *
8
+ * The `command` tokens form the prefix that would be added as an execpolicy
9
+ * `prefix_rule(..., decision="allow")`, letting the agent bypass approval for
10
+ * commands that start with this token sequence.
11
+ */
12
+ export type ExecPolicyAmendment = Array<string>;
codex-rs/app-server-protocol/schema/typescript/FileChange.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type FileChange = { "type": "add", content: string, } | { "type": "delete", content: string, } | { "type": "update", unified_diff: string, move_path: string | null, };
codex-rs/app-server-protocol/schema/typescript/ForcedLoginMethod.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type ForcedLoginMethod = "chatgpt" | "api";
codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputBody.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem";
5
+
6
+ export type FunctionCallOutputBody = string | Array<FunctionCallOutputContentItem>;
codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputContentItem.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { ImageDetail } from "./ImageDetail";
5
+
6
+ /**
7
+ * Responses API compatible content items that can be returned by a tool call.
8
+ * This is a subset of ContentItem with the types we support as function call outputs.
9
+ */
10
+ export type FunctionCallOutputContentItem = { "type": "input_text", text: string, } | { "type": "input_image", detail?: ImageDetail, } & ({ image_url: string, } | { file_id: string, }) | { "type": "input_audio", audio_url: string, } | { "type": "encrypted_content", encrypted_content: string, };
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchMatchType.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type FuzzyFileSearchMatchType = "file" | "directory";
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchParams.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type FuzzyFileSearchParams = { query: string, roots: Array<string>, cancellationToken: string | null, };
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResponse.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult";
5
+
6
+ export type FuzzyFileSearchResponse = { files: Array<FuzzyFileSearchResult>, };
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResult.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType";
5
+
6
+ /**
7
+ * Superset of [`codex_file_search::FileMatch`]
8
+ */
9
+ export type FuzzyFileSearchResult = { root: string, path: string, match_type: FuzzyFileSearchMatchType, file_name: string, score: number, indices: Array<number> | null, };
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchSessionCompletedNotification.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type FuzzyFileSearchSessionCompletedNotification = { sessionId: string, };
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchSessionUpdatedNotification.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult";
5
+
6
+ export type FuzzyFileSearchSessionUpdatedNotification = { sessionId: string, query: string, files: Array<FuzzyFileSearchResult>, };
codex-rs/app-server-protocol/schema/typescript/GetAuthStatusParams.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+
5
+ export type GetAuthStatusParams = { includeToken: boolean | null, refreshToken: boolean | null, };
codex-rs/app-server-protocol/schema/typescript/GetAuthStatusResponse.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { AuthMode } from "./AuthMode";
5
+
6
+ export type GetAuthStatusResponse = { authMethod: AuthMode | null, authToken: string | null, requiresOpenaiAuth: boolean | null, };
codex-rs/app-server-protocol/schema/typescript/GetConversationSummaryParams.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // GENERATED CODE! DO NOT MODIFY BY HAND!
2
+
3
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
4
+ import type { ThreadId } from "./ThreadId";
5
+
6
+ export type GetConversationSummaryParams = { rolloutPath: string, } | { conversationId: ThreadId, };