diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 65d9dab21..2ad698d06 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -111,6 +111,7 @@ pub use subscription::{EventSubscription, LifecycleSubscription}; /// Minimum protocol version this SDK can communicate with. const MIN_PROTOCOL_VERSION: u32 = 3; const RUNTIME_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10); +const MANAGED_SETTINGS_READ_METHOD: &str = "managedSettings.read"; /// How the SDK communicates with the CLI server. #[derive(Debug, Default)] @@ -2181,6 +2182,16 @@ impl Client { Ok(serde_json::from_value(result)?) } + /// Read validated, canonical device managed settings from the runtime. + /// + /// This is a server-scoped RPC and does not create or require an agent + /// session. The SDK returns the runtime-owned payload as opaque JSON so + /// callers receive exactly the canonical data the runtime produced. + pub async fn read_managed_settings(&self) -> Result { + self.call(MANAGED_SETTINGS_READ_METHOD, Some(serde_json::json!({}))) + .await + } + /// List available models. /// /// When [`ClientOptions::on_list_models`] is set, returns the handler's diff --git a/rust/src/types.rs b/rust/src/types.rs index 81eac81ca..6fd2433c8 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -107,6 +107,13 @@ pub struct SessionLifecycleEvent { pub metadata: Option, } +/// Runtime-owned managed settings payload returned by +/// [`Client::read_managed_settings`](crate::Client::read_managed_settings). +/// +/// The SDK treats the payload as opaque JSON and does not reinterpret policy +/// decisions made by the runtime. +pub type ManagedSettings = Value; + /// Opaque session identifier assigned by the CLI. /// /// A newtype wrapper around `String` that provides type safety — prevents diff --git a/rust/tests/managed_settings_test.rs b/rust/tests/managed_settings_test.rs new file mode 100644 index 000000000..b2c63fbbc --- /dev/null +++ b/rust/tests/managed_settings_test.rs @@ -0,0 +1,80 @@ +#![allow(clippy::unwrap_used)] + +use github_copilot_sdk::Client; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, duplex}; + +async fn write_framed(writer: &mut (impl AsyncWrite + Unpin), body: &[u8]) { + let header = format!("Content-Length: {}\r\n\r\n", body.len()); + writer.write_all(header.as_bytes()).await.unwrap(); + writer.write_all(body).await.unwrap(); + writer.flush().await.unwrap(); +} + +async fn read_framed(reader: &mut (impl AsyncRead + Unpin)) -> serde_json::Value { + let mut header = String::new(); + loop { + let mut byte = [0u8; 1]; + reader.read_exact(&mut byte).await.unwrap(); + header.push(byte[0] as char); + if header.ends_with("\r\n\r\n") { + break; + } + } + let length: usize = header + .trim() + .strip_prefix("Content-Length: ") + .unwrap() + .parse() + .unwrap(); + let mut buf = vec![0u8; length]; + reader.read_exact(&mut buf).await.unwrap(); + serde_json::from_slice(&buf).unwrap() +} + +#[tokio::test] +async fn read_managed_settings_calls_server_scoped_method_before_session_create() { + let tempdir = tempfile::tempdir().unwrap(); + let (client_write, mut server_read) = duplex(8192); + let (mut server_write, client_read) = duplex(8192); + let client = Client::from_streams(client_read, client_write, tempdir.path().to_path_buf()) + .expect("create client"); + + let read_handle = tokio::spawn({ + let client = client.clone(); + async move { client.read_managed_settings().await } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "managedSettings.read"); + assert_eq!(request["params"], serde_json::json!({})); + + let payload = serde_json::json!({ + "source": "device", + "canonical": true, + "policies": { + "bypassPermissions": ["shell.read"] + } + }); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": request["id"], + "result": payload, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + let result = tokio::time::timeout(std::time::Duration::from_secs(2), read_handle) + .await + .unwrap() + .unwrap() + .expect("read managed settings"); + assert_eq!( + result, + serde_json::json!({ + "source": "device", + "canonical": true, + "policies": { + "bypassPermissions": ["shell.read"] + } + }) + ); +}