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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<ManagedSettings> {
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
Expand Down
7 changes: 7 additions & 0 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ pub struct SessionLifecycleEvent {
pub metadata: Option<SessionLifecycleEventMetadata>,
}

/// 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
Expand Down
80 changes: 80 additions & 0 deletions rust/tests/managed_settings_test.rs
Original file line number Diff line number Diff line change
@@ -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"]
}
})
);
}
Loading