mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
feat(app-server): add permission request capabilities
Add the `supportedServerRequests` initialize capability across the app-server protocol, clients, and connection session state. This lets surfaces explicitly advertise which conversational permission approval requests they know how to receive. Keep this branch limited to the opt-in contract and the bookkeeping needed to store that capability per connection. Delivery and UI handling stay in later stacked PRs so existing clients remain backward compatible and the review surface stays small.
This commit is contained in:
@@ -500,6 +500,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
supported_server_requests: None,
|
||||
opt_out_notification_methods: None,
|
||||
}),
|
||||
},
|
||||
@@ -591,6 +592,7 @@ async fn compaction_event_ingests_custom_fact() {
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
supported_server_requests: None,
|
||||
opt_out_notification_methods: None,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -39,6 +39,7 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::Result as JsonRpcResult;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::SupportedServerRequestMethod;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
@@ -367,6 +368,8 @@ pub struct InProcessClientStartArgs {
|
||||
pub client_version: String,
|
||||
/// Whether experimental APIs are requested at initialize time.
|
||||
pub experimental_api: bool,
|
||||
/// Server-initiated requests this client can render and answer.
|
||||
pub supported_server_requests: Vec<SupportedServerRequestMethod>,
|
||||
/// Notification methods this client opts out of receiving.
|
||||
pub opt_out_notification_methods: Vec<String>,
|
||||
/// Queue capacity for command/event channels (clamped to at least 1).
|
||||
@@ -378,6 +381,11 @@ impl InProcessClientStartArgs {
|
||||
pub fn initialize_params(&self) -> InitializeParams {
|
||||
let capabilities = InitializeCapabilities {
|
||||
experimental_api: self.experimental_api,
|
||||
supported_server_requests: if self.supported_server_requests.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.supported_server_requests.clone())
|
||||
},
|
||||
opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -988,6 +996,7 @@ mod tests {
|
||||
client_name: "codex-app-server-client-test".to_string(),
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
supported_server_requests: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity,
|
||||
})
|
||||
@@ -999,6 +1008,41 @@ mod tests {
|
||||
start_test_client_with_capacity(session_source, DEFAULT_IN_PROCESS_CHANNEL_CAPACITY).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn in_process_initialize_params_include_supported_server_requests() {
|
||||
let expected_supported = vec![
|
||||
SupportedServerRequestMethod::PermissionsRequestApproval,
|
||||
SupportedServerRequestMethod::PermissionPresetRequestApproval,
|
||||
];
|
||||
let args = InProcessClientStartArgs {
|
||||
arg0_paths: Arg0DispatchPaths::default(),
|
||||
config: Arc::new(build_test_config().await),
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
feedback: CodexFeedback::new(),
|
||||
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli,
|
||||
enable_codex_api_key_env: false,
|
||||
client_name: "codex-app-server-client-test".to_string(),
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
supported_server_requests: expected_supported.clone(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
};
|
||||
|
||||
let capabilities = args
|
||||
.initialize_params()
|
||||
.capabilities
|
||||
.expect("capabilities should be populated");
|
||||
assert_eq!(
|
||||
capabilities.supported_server_requests,
|
||||
Some(expected_supported)
|
||||
);
|
||||
}
|
||||
|
||||
async fn start_test_remote_server<F, Fut>(handler: F) -> String
|
||||
where
|
||||
F: FnOnce(tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> Fut
|
||||
@@ -1163,6 +1207,7 @@ mod tests {
|
||||
client_name: "codex-app-server-client-test".to_string(),
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
supported_server_requests: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: 8,
|
||||
}
|
||||
@@ -1607,6 +1652,7 @@ mod tests {
|
||||
client_name: "codex-app-server-client-test".to_string(),
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
supported_server_requests: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: 1,
|
||||
})
|
||||
@@ -2000,6 +2046,7 @@ mod tests {
|
||||
client_name: "codex-app-server-client-test".to_string(),
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
supported_server_requests: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::Result as JsonRpcResult;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::SupportedServerRequestMethod;
|
||||
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
@@ -64,6 +65,7 @@ pub struct RemoteAppServerConnectArgs {
|
||||
pub client_name: String,
|
||||
pub client_version: String,
|
||||
pub experimental_api: bool,
|
||||
pub supported_server_requests: Vec<SupportedServerRequestMethod>,
|
||||
pub opt_out_notification_methods: Vec<String>,
|
||||
pub channel_capacity: usize,
|
||||
}
|
||||
@@ -72,6 +74,11 @@ impl RemoteAppServerConnectArgs {
|
||||
fn initialize_params(&self) -> InitializeParams {
|
||||
let capabilities = InitializeCapabilities {
|
||||
experimental_api: self.experimental_api,
|
||||
supported_server_requests: if self.supported_server_requests.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.supported_server_requests.clone())
|
||||
},
|
||||
opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -1022,6 +1022,16 @@
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"supportedServerRequests": {
|
||||
"description": "Server request methods this connection knows how to render and answer.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SupportedServerRequestMethod"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -2569,6 +2579,14 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SupportedServerRequestMethod": {
|
||||
"description": "Server-initiated request methods a client can opt into during initialize.",
|
||||
"enum": [
|
||||
"item/permissions/requestApproval",
|
||||
"item/permissionPreset/requestApproval"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
"properties": {
|
||||
"byteRange": {
|
||||
|
||||
@@ -2295,6 +2295,16 @@
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"supportedServerRequests": {
|
||||
"description": "Server request methods this connection knows how to render and answer.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SupportedServerRequestMethod"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -4721,6 +4731,14 @@
|
||||
],
|
||||
"title": "ServerRequest"
|
||||
},
|
||||
"SupportedServerRequestMethod": {
|
||||
"description": "Server-initiated request methods a client can opt into during initialize.",
|
||||
"enum": [
|
||||
"item/permissions/requestApproval",
|
||||
"item/permissionPreset/requestApproval"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ToolRequestUserInputAnswer": {
|
||||
"description": "EXPERIMENTAL. Captures a user's answer to a request_user_input question.",
|
||||
"properties": {
|
||||
|
||||
@@ -5347,6 +5347,16 @@
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"supportedServerRequests": {
|
||||
"description": "Server request methods this connection knows how to render and answer.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SupportedServerRequestMethod"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -10257,6 +10267,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"SupportedServerRequestMethod": {
|
||||
"description": "Server-initiated request methods a client can opt into during initialize.",
|
||||
"enum": [
|
||||
"item/permissions/requestApproval",
|
||||
"item/permissionPreset/requestApproval"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TerminalInteractionNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
@@ -39,9 +39,27 @@
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"supportedServerRequests": {
|
||||
"description": "Server request methods this connection knows how to render and answer.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SupportedServerRequestMethod"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SupportedServerRequestMethod": {
|
||||
"description": "Server-initiated request methods a client can opt into during initialize.",
|
||||
"enum": [
|
||||
"item/permissions/requestApproval",
|
||||
"item/permissionPreset/requestApproval"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { SupportedServerRequestMethod } from "./SupportedServerRequestMethod";
|
||||
|
||||
/**
|
||||
* Client-declared capabilities negotiated during initialize.
|
||||
@@ -10,6 +11,10 @@ export type InitializeCapabilities = {
|
||||
* Opt into receiving experimental API methods and fields.
|
||||
*/
|
||||
experimentalApi: boolean,
|
||||
/**
|
||||
* Server request methods this connection knows how to render and answer.
|
||||
*/
|
||||
supportedServerRequests?: Array<SupportedServerRequestMethod> | null,
|
||||
/**
|
||||
* Exact notification method names that should be suppressed for this
|
||||
* connection (for example `thread/started`).
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Server-initiated request methods a client can opt into during initialize.
|
||||
*/
|
||||
export type SupportedServerRequestMethod = "item/permissions/requestApproval" | "item/permissionPreset/requestApproval";
|
||||
@@ -67,6 +67,7 @@ export type { ServiceTier } from "./ServiceTier";
|
||||
export type { SessionSource } from "./SessionSource";
|
||||
export type { Settings } from "./Settings";
|
||||
export type { SubAgentSource } from "./SubAgentSource";
|
||||
export type { SupportedServerRequestMethod } from "./SupportedServerRequestMethod";
|
||||
export type { ThreadId } from "./ThreadId";
|
||||
export type { Tool } from "./Tool";
|
||||
export type { Verbosity } from "./Verbosity";
|
||||
|
||||
@@ -37,6 +37,7 @@ pub use protocol::v1::InterruptConversationResponse;
|
||||
pub use protocol::v1::LoginApiKeyParams;
|
||||
pub use protocol::v1::Profile;
|
||||
pub use protocol::v1::SandboxSettings;
|
||||
pub use protocol::v1::SupportedServerRequestMethod;
|
||||
pub use protocol::v1::Tools;
|
||||
pub use protocol::v1::UserSavedConfig;
|
||||
pub use protocol::v2::*;
|
||||
|
||||
@@ -1097,6 +1097,10 @@ mod tests {
|
||||
},
|
||||
capabilities: Some(v1::InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
supported_server_requests: Some(vec![
|
||||
v1::SupportedServerRequestMethod::PermissionsRequestApproval,
|
||||
v1::SupportedServerRequestMethod::PermissionPresetRequestApproval,
|
||||
]),
|
||||
opt_out_notification_methods: Some(vec![
|
||||
"thread/started".to_string(),
|
||||
"item/agentMessage/delta".to_string(),
|
||||
@@ -1117,6 +1121,10 @@ mod tests {
|
||||
},
|
||||
"capabilities": {
|
||||
"experimentalApi": true,
|
||||
"supportedServerRequests": [
|
||||
"item/permissions/requestApproval",
|
||||
"item/permissionPreset/requestApproval"
|
||||
],
|
||||
"optOutNotificationMethods": [
|
||||
"thread/started",
|
||||
"item/agentMessage/delta"
|
||||
@@ -1142,6 +1150,10 @@ mod tests {
|
||||
},
|
||||
"capabilities": {
|
||||
"experimentalApi": true,
|
||||
"supportedServerRequests": [
|
||||
"item/permissions/requestApproval",
|
||||
"item/permissionPreset/requestApproval"
|
||||
],
|
||||
"optOutNotificationMethods": [
|
||||
"thread/started",
|
||||
"item/agentMessage/delta"
|
||||
@@ -1162,6 +1174,10 @@ mod tests {
|
||||
},
|
||||
capabilities: Some(v1::InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
supported_server_requests: Some(vec![
|
||||
v1::SupportedServerRequestMethod::PermissionsRequestApproval,
|
||||
v1::SupportedServerRequestMethod::PermissionPresetRequestApproval,
|
||||
]),
|
||||
opt_out_notification_methods: Some(vec![
|
||||
"thread/started".to_string(),
|
||||
"item/agentMessage/delta".to_string(),
|
||||
@@ -1173,6 +1189,31 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_initialize_without_supported_server_requests() -> Result<()> {
|
||||
let request: ClientRequest = serde_json::from_value(json!({
|
||||
"method": "initialize",
|
||||
"id": 42,
|
||||
"params": {
|
||||
"clientInfo": {
|
||||
"name": "codex_vscode",
|
||||
"title": "Codex VS Code Extension",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"capabilities": {
|
||||
"experimentalApi": true
|
||||
}
|
||||
}
|
||||
}))?;
|
||||
|
||||
let ClientRequest::Initialize { params, .. } = request else {
|
||||
panic!("expected initialize request");
|
||||
};
|
||||
let capabilities = params.capabilities.expect("capabilities");
|
||||
assert_eq!(capabilities.supported_server_requests, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conversation_id_serializes_as_plain_string() -> Result<()> {
|
||||
let id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
|
||||
|
||||
@@ -46,12 +46,26 @@ pub struct InitializeCapabilities {
|
||||
/// Opt into receiving experimental API methods and fields.
|
||||
#[serde(default)]
|
||||
pub experimental_api: bool,
|
||||
/// Server request methods this connection knows how to render and answer.
|
||||
#[ts(optional = nullable)]
|
||||
pub supported_server_requests: Option<Vec<SupportedServerRequestMethod>>,
|
||||
/// Exact notification method names that should be suppressed for this
|
||||
/// connection (for example `thread/started`).
|
||||
#[ts(optional = nullable)]
|
||||
pub opt_out_notification_methods: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Server-initiated request methods a client can opt into during initialize.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, TS)]
|
||||
pub enum SupportedServerRequestMethod {
|
||||
#[serde(rename = "item/permissions/requestApproval")]
|
||||
#[ts(rename = "item/permissions/requestApproval")]
|
||||
PermissionsRequestApproval,
|
||||
#[serde(rename = "item/permissionPreset/requestApproval")]
|
||||
#[ts(rename = "item/permissionPreset/requestApproval")]
|
||||
PermissionPresetRequestApproval,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InitializeResponse {
|
||||
|
||||
@@ -1559,6 +1559,7 @@ impl CodexClient {
|
||||
.map(|method| (*method).to_string())
|
||||
.collect(),
|
||||
),
|
||||
supported_server_requests: None,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -89,6 +89,8 @@ Clients must send a single `initialize` request per transport connection before
|
||||
|
||||
`initialize.params.capabilities` also supports per-connection notification opt-out via `optOutNotificationMethods`, which is a list of exact method names to suppress for that connection. Matching is exact (no wildcards/prefixes). Unknown method names are accepted and ignored.
|
||||
|
||||
Clients can also advertise `supportedServerRequests` during initialize. This is a per-connection opt-in list of server-initiated request methods the surface knows how to render and answer. App-server uses it for conversational permission confirmation flows so existing clients keep their current behavior until they explicitly opt in.
|
||||
|
||||
Applications building on top of `codex app-server` should identify themselves via the `clientInfo` parameter.
|
||||
|
||||
**Important**: `clientInfo.name` is used to identify the client for the OpenAI Compliance Logs Platform. If
|
||||
@@ -111,7 +113,7 @@ Example (from OpenAI's official VSCode extension):
|
||||
}
|
||||
```
|
||||
|
||||
Example with notification opt-out:
|
||||
Example with notification opt-out and explicit server-request support:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -125,6 +127,10 @@ Example with notification opt-out:
|
||||
},
|
||||
"capabilities": {
|
||||
"experimentalApi": true,
|
||||
"supportedServerRequests": [
|
||||
"item/permissions/requestApproval",
|
||||
"item/permissionPreset/requestApproval"
|
||||
],
|
||||
"optOutNotificationMethods": ["thread/started", "item/agentMessage/delta"]
|
||||
}
|
||||
}
|
||||
@@ -1071,6 +1077,8 @@ the client can offer session-scoped and/or persistent approval choices.
|
||||
|
||||
### Permission requests
|
||||
|
||||
Clients must opt into conversational permission confirmation request methods by listing them in `initialize.params.capabilities.supportedServerRequests`. The opt-in is per connection and per method, which lets older clients continue to initialize without promising UI they do not implement.
|
||||
|
||||
The built-in `request_permissions` tool sends an `item/permissions/requestApproval` JSON-RPC request to the client with the requested permission profile. This v2 payload mirrors the command-execution `additionalPermissions` shape: it can request network access and additional filesystem access.
|
||||
|
||||
```json
|
||||
|
||||
@@ -117,6 +117,7 @@ use codex_app_server_protocol::SkillsConfigWriteParams;
|
||||
use codex_app_server_protocol::SkillsConfigWriteResponse;
|
||||
use codex_app_server_protocol::SkillsListParams;
|
||||
use codex_app_server_protocol::SkillsListResponse;
|
||||
use codex_app_server_protocol::SupportedServerRequestMethod;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
use codex_app_server_protocol::ThreadArchiveResponse;
|
||||
@@ -3738,9 +3739,13 @@ impl CodexMessageProcessor {
|
||||
self.thread_manager.subscribe_thread_created()
|
||||
}
|
||||
|
||||
pub(crate) async fn connection_initialized(&self, connection_id: ConnectionId) {
|
||||
pub(crate) async fn connection_initialized(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
supported_server_requests: HashSet<SupportedServerRequestMethod>,
|
||||
) {
|
||||
self.thread_state_manager
|
||||
.connection_initialized(connection_id)
|
||||
.connection_initialized(connection_id, supported_server_requests)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -9917,7 +9922,9 @@ mod tests {
|
||||
let connection = ConnectionId(1);
|
||||
let (cancel_tx, cancel_rx) = oneshot::channel();
|
||||
|
||||
manager.connection_initialized(connection).await;
|
||||
manager
|
||||
.connection_initialized(connection, HashSet::new())
|
||||
.await;
|
||||
manager
|
||||
.try_ensure_connection_subscribed(
|
||||
thread_id, connection, /*experimental_raw_events*/ false,
|
||||
@@ -9963,8 +9970,12 @@ mod tests {
|
||||
let connection_b = ConnectionId(2);
|
||||
let (cancel_tx, mut cancel_rx) = oneshot::channel();
|
||||
|
||||
manager.connection_initialized(connection_a).await;
|
||||
manager.connection_initialized(connection_b).await;
|
||||
manager
|
||||
.connection_initialized(connection_a, HashSet::new())
|
||||
.await;
|
||||
manager
|
||||
.connection_initialized(connection_b, HashSet::new())
|
||||
.await;
|
||||
manager
|
||||
.try_ensure_connection_subscribed(
|
||||
thread_id,
|
||||
@@ -10007,7 +10018,9 @@ mod tests {
|
||||
let thread_id = ThreadId::from_string("ad7f0408-99b8-4f6e-a46f-bd0eec433370")?;
|
||||
let connection = ConnectionId(1);
|
||||
|
||||
manager.connection_initialized(connection).await;
|
||||
manager
|
||||
.connection_initialized(connection, HashSet::new())
|
||||
.await;
|
||||
let threads_to_unload = manager.remove_connection(connection).await;
|
||||
assert_eq!(threads_to_unload, Vec::<ThreadId>::new());
|
||||
|
||||
|
||||
@@ -797,7 +797,15 @@ pub async fn run_main_with_transport(
|
||||
connection_id,
|
||||
)
|
||||
.await;
|
||||
processor.connection_initialized(connection_id).await;
|
||||
processor
|
||||
.connection_initialized(
|
||||
connection_id,
|
||||
connection_state
|
||||
.session
|
||||
.supported_server_requests
|
||||
.clone(),
|
||||
)
|
||||
.await;
|
||||
connection_state
|
||||
.outbound_initialized
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
|
||||
@@ -56,6 +56,7 @@ use codex_app_server_protocol::JSONRPCRequest;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::SupportedServerRequestMethod;
|
||||
use codex_app_server_protocol::experimental_required_message;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_chatgpt::connectors;
|
||||
@@ -179,6 +180,7 @@ pub(crate) struct ConnectionSessionState {
|
||||
pub(crate) initialized: bool,
|
||||
pub(crate) experimental_api_enabled: bool,
|
||||
pub(crate) opted_out_notification_methods: HashSet<String>,
|
||||
pub(crate) supported_server_requests: HashSet<SupportedServerRequestMethod>,
|
||||
pub(crate) app_server_client_name: Option<String>,
|
||||
pub(crate) client_version: Option<String>,
|
||||
}
|
||||
@@ -456,9 +458,13 @@ impl MessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn connection_initialized(&self, connection_id: ConnectionId) {
|
||||
pub(crate) async fn connection_initialized(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
supported_server_requests: HashSet<SupportedServerRequestMethod>,
|
||||
) {
|
||||
self.codex_message_processor
|
||||
.connection_initialized(connection_id)
|
||||
.connection_initialized(connection_id, supported_server_requests)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -561,19 +567,28 @@ impl MessageProcessor {
|
||||
// experimental API). Proposed direction is instance-global first-write-wins
|
||||
// with initialize-time mismatch rejection.
|
||||
let analytics_initialize_params = params.clone();
|
||||
let (experimental_api_enabled, opt_out_notification_methods) =
|
||||
match params.capabilities {
|
||||
Some(capabilities) => (
|
||||
capabilities.experimental_api,
|
||||
capabilities
|
||||
.opt_out_notification_methods
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
None => (false, Vec::new()),
|
||||
};
|
||||
let (
|
||||
experimental_api_enabled,
|
||||
opt_out_notification_methods,
|
||||
supported_server_requests,
|
||||
) = match params.capabilities {
|
||||
Some(capabilities) => (
|
||||
capabilities.experimental_api,
|
||||
capabilities
|
||||
.opt_out_notification_methods
|
||||
.unwrap_or_default(),
|
||||
capabilities
|
||||
.supported_server_requests
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
None => (false, Vec::new(), HashSet::new()),
|
||||
};
|
||||
session.experimental_api_enabled = experimental_api_enabled;
|
||||
session.opted_out_notification_methods =
|
||||
opt_out_notification_methods.into_iter().collect();
|
||||
session.supported_server_requests = supported_server_requests;
|
||||
let ClientInfo {
|
||||
name,
|
||||
title: _title,
|
||||
@@ -649,7 +664,10 @@ impl MessageProcessor {
|
||||
// initialize handling for the specific connection.
|
||||
outbound_initialized.store(true, Ordering::Release);
|
||||
self.codex_message_processor
|
||||
.connection_initialized(connection_id)
|
||||
.connection_initialized(
|
||||
connection_id,
|
||||
session.supported_server_requests.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
use crate::outgoing_message::ConnectionRequestId;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SupportedServerRequestMethod;
|
||||
use codex_app_server_protocol::ThreadHistoryBuilder;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnError;
|
||||
@@ -173,6 +174,8 @@ impl Default for ThreadEntry {
|
||||
#[derive(Default)]
|
||||
struct ThreadStateManagerInner {
|
||||
live_connections: HashSet<ConnectionId>,
|
||||
supported_server_requests_by_connection:
|
||||
HashMap<ConnectionId, HashSet<SupportedServerRequestMethod>>,
|
||||
threads: HashMap<ThreadId, ThreadEntry>,
|
||||
thread_ids_by_connection: HashMap<ConnectionId, HashSet<ThreadId>>,
|
||||
}
|
||||
@@ -187,12 +190,16 @@ impl ThreadStateManager {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) async fn connection_initialized(&self, connection_id: ConnectionId) {
|
||||
self.state
|
||||
.lock()
|
||||
.await
|
||||
.live_connections
|
||||
.insert(connection_id);
|
||||
pub(crate) async fn connection_initialized(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
supported_server_requests: HashSet<SupportedServerRequestMethod>,
|
||||
) {
|
||||
let mut state = self.state.lock().await;
|
||||
state.live_connections.insert(connection_id);
|
||||
state
|
||||
.supported_server_requests_by_connection
|
||||
.insert(connection_id, supported_server_requests);
|
||||
}
|
||||
|
||||
pub(crate) async fn subscribed_connection_ids(&self, thread_id: ThreadId) -> Vec<ConnectionId> {
|
||||
@@ -357,6 +364,9 @@ impl ThreadStateManager {
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.live_connections.remove(&connection_id);
|
||||
state
|
||||
.supported_server_requests_by_connection
|
||||
.remove(&connection_id);
|
||||
let thread_ids = state
|
||||
.thread_ids_by_connection
|
||||
.remove(&connection_id)
|
||||
|
||||
@@ -33,6 +33,7 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
supported_server_requests: None,
|
||||
opt_out_notification_methods: None,
|
||||
}),
|
||||
)
|
||||
@@ -63,6 +64,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
supported_server_requests: None,
|
||||
opt_out_notification_methods: None,
|
||||
}),
|
||||
)
|
||||
@@ -99,6 +101,7 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result<
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
supported_server_requests: None,
|
||||
opt_out_notification_methods: None,
|
||||
}),
|
||||
)
|
||||
@@ -139,6 +142,7 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
supported_server_requests: None,
|
||||
opt_out_notification_methods: None,
|
||||
}),
|
||||
)
|
||||
@@ -176,6 +180,7 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
supported_server_requests: None,
|
||||
opt_out_notification_methods: None,
|
||||
}),
|
||||
)
|
||||
@@ -212,6 +217,7 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
supported_server_requests: None,
|
||||
opt_out_notification_methods: None,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -158,6 +158,7 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu
|
||||
},
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
supported_server_requests: None,
|
||||
opt_out_notification_methods: Some(vec!["thread/started".to_string()]),
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -145,6 +145,7 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> {
|
||||
},
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
supported_server_requests: None,
|
||||
opt_out_notification_methods: Some(vec!["thread/status/changed".to_string()]),
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -103,6 +103,7 @@ impl AppServerClient {
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
supported_server_requests: None,
|
||||
opt_out_notification_methods: None,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1510,18 +1510,6 @@ async fn handle_server_request(
|
||||
)
|
||||
.await
|
||||
}
|
||||
ServerRequest::PermissionPresetRequestApproval { request_id, params } => {
|
||||
reject_server_request(
|
||||
client,
|
||||
request_id,
|
||||
&method,
|
||||
format!(
|
||||
"permission preset approval is not supported in exec mode for thread `{}`",
|
||||
params.thread_id
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = handle_result {
|
||||
|
||||
@@ -6692,6 +6692,7 @@ impl ChatWidget {
|
||||
EventMsg::RequestPermissions(ev) => {
|
||||
self.on_request_permissions(ev);
|
||||
}
|
||||
EventMsg::RequestPermissionPreset(_) => {}
|
||||
EventMsg::ExecCommandBegin(ev) => self.on_exec_command_begin(ev),
|
||||
EventMsg::TerminalInteraction(delta) => self.on_terminal_interaction(delta),
|
||||
EventMsg::ExecCommandOutputDelta(delta) => self.on_exec_command_output_delta(delta),
|
||||
|
||||
@@ -33,6 +33,7 @@ use codex_app_server_client::RemoteAppServerConnectArgs;
|
||||
use codex_app_server_protocol::Account as AppServerAccount;
|
||||
use codex_app_server_protocol::AuthMode as AppServerAuthMode;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::SupportedServerRequestMethod;
|
||||
use codex_app_server_protocol::Thread as AppServerThread;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadSortKey as AppServerThreadSortKey;
|
||||
@@ -351,6 +352,7 @@ async fn connect_remote_app_server(
|
||||
client_name: "codex-tui".to_string(),
|
||||
client_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
experimental_api: true,
|
||||
supported_server_requests: tui_supported_server_requests(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
@@ -459,6 +461,7 @@ where
|
||||
client_name: "codex-tui".to_string(),
|
||||
client_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
experimental_api: true,
|
||||
supported_server_requests: tui_supported_server_requests(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
@@ -467,6 +470,10 @@ where
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
fn tui_supported_server_requests() -> Vec<SupportedServerRequestMethod> {
|
||||
vec![SupportedServerRequestMethod::PermissionsRequestApproval]
|
||||
}
|
||||
|
||||
async fn shutdown_app_server_if_present(app_server: Option<AppServerSession>) {
|
||||
if let Some(app_server) = app_server
|
||||
&& let Err(err) = app_server.shutdown().await
|
||||
@@ -1814,6 +1821,15 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_advertises_permission_confirmation_server_requests() {
|
||||
let supported = tui_supported_server_requests();
|
||||
assert_eq!(
|
||||
supported,
|
||||
vec![SupportedServerRequestMethod::PermissionsRequestApproval]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_remote_addr_rejects_invalid_input() {
|
||||
let err = normalize_remote_addr("https://127.0.0.1:4500")
|
||||
|
||||
@@ -997,6 +997,7 @@ mod tests {
|
||||
client_name: "test".to_string(),
|
||||
client_version: "test".to_string(),
|
||||
experimental_api: true,
|
||||
supported_server_requests: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user