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
13 changes: 13 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,18 @@ 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. Successful reads return `settings_json`; runtime discovery
/// failures are reported through `error_message`.
pub async fn read_managed_settings(&self) -> Result<ManagedSettingsReadResult> {
let result = self
.call(MANAGED_SETTINGS_READ_METHOD, Some(serde_json::json!({})))
.await?;
Ok(serde_json::from_value(result)?)
}

/// List available models.
///
/// When [`ClientOptions::on_list_models`] is set, returns the handler's
Expand Down
14 changes: 14 additions & 0 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ pub struct SessionLifecycleEvent {
pub metadata: Option<SessionLifecycleEventMetadata>,
}

/// Result returned by
/// [`Client::read_managed_settings`](crate::Client::read_managed_settings).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ManagedSettingsReadResult {
/// Validated, canonical managed settings JSON, when settings are available.
#[serde(skip_serializing_if = "Option::is_none")]
pub settings_json: Option<String>,
/// Runtime-provided error message, when settings could not be read.
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
}

/// Opaque session identifier assigned by the CLI.
///
/// A newtype wrapper around `String` that provides type safety — prevents
Expand Down
109 changes: 109 additions & 0 deletions rust/tests/managed_settings_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#![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 settings = serde_json::json!({
"source": "device",
"canonical": true,
"policies": {
"bypassPermissions": ["shell.read"]
}
});
let response = serde_json::json!({
"jsonrpc": "2.0",
"id": request["id"],
"result": {
"settingsJson": settings.to_string(),
},
});
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.settings_json, Some(settings.to_string()));
assert_eq!(result.error_message, None);
}

#[tokio::test]
async fn read_managed_settings_returns_runtime_error_message() {
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;
let response = serde_json::json!({
"jsonrpc": "2.0",
"id": request["id"],
"result": {
"errorMessage": "Managed settings are unavailable",
},
});
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.settings_json, None);
assert_eq!(
result.error_message,
Some("Managed settings are unavailable".to_string())
);
}
Loading