diff --git a/rust/src/lib.rs b/rust/src/lib.rs index c95ed2087..ddb6a9c3b 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2573,6 +2573,23 @@ impl Client { Ok(serde_json::from_value(value)?) } + /// Set the account identity used to scope persisted session-store operations. + /// + /// Call this after connecting and before local session-store operations such + /// as listing or resuming sessions. Pass `None` to clear the identity, such + /// as when the user logs out. + pub async fn set_session_store_identity( + &self, + identity: Option<&SessionStoreIdentity>, + ) -> Result<()> { + self.call( + "sessionStore.setIdentity", + Some(serde_json::json!({ "identity": identity })), + ) + .await?; + Ok(()) + } + /// List persisted sessions, optionally filtered by working directory, /// repository, or git context. pub async fn list_sessions( diff --git a/rust/src/types.rs b/rust/src/types.rs index a99f00a19..0616509fd 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1078,6 +1078,27 @@ impl ExtensionInfo { } } +/// Identity used to isolate persisted session-store data by GitHub account. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct SessionStoreIdentity { + /// Canonical HTTPS origin for GitHub.com or the GitHub Enterprise Server. + pub authority: String, + /// Positive decimal GitHub account database ID. + pub account_id: String, +} + +impl SessionStoreIdentity { + /// Create a session-store identity. + pub fn new(authority: impl Into, account_id: impl Into) -> Self { + Self { + authority: authority.into(), + account_id: account_id.into(), + } + } +} + /// Stable identity for a host/SDK connection that supplies built-in canvases. /// /// When set on session create or resume, the runtime uses [`id`] verbatim as @@ -2029,6 +2050,8 @@ pub struct SessionConfig { pub enable_host_git_operations: Option, /// When true, enables the session store for this session. pub enable_session_store: Option, + /// Identity used to isolate this session's persisted session-store data. + pub session_store_identity: Option, /// When true, enables skills for this session. pub enable_skills: Option, /// **Experimental.** This option is part of an experimental wire-protocol @@ -2339,6 +2362,7 @@ impl std::fmt::Debug for SessionConfig { &self.enable_host_git_operations, ) .field("enable_session_store", &self.enable_session_store) + .field("session_store_identity", &self.session_store_identity) .field("enable_skills", &self.enable_skills) .field("enable_mcp_apps", &self.enable_mcp_apps) .field("skill_directories", &self.skill_directories) @@ -2466,6 +2490,7 @@ impl Default for SessionConfig { enable_file_hooks: None, enable_host_git_operations: None, enable_session_store: None, + session_store_identity: None, enable_skills: None, embedding_cache_storage: None, enable_mcp_apps: None, @@ -2637,6 +2662,7 @@ impl SessionConfig { enable_file_hooks: self.enable_file_hooks, enable_host_git_operations: self.enable_host_git_operations, enable_session_store: self.enable_session_store, + session_store_identity: self.session_store_identity, enable_skills: self.enable_skills, request_user_input, request_permission: permission_active, @@ -3024,6 +3050,12 @@ impl SessionConfig { self } + /// Set the identity used to isolate persisted session-store data. + pub fn with_session_store_identity(mut self, identity: SessionStoreIdentity) -> Self { + self.session_store_identity = Some(identity); + self + } + /// Set [`Self::enable_skills`]. pub fn with_enable_skills(mut self, value: bool) -> Self { self.enable_skills = Some(value); @@ -3462,6 +3494,8 @@ pub struct ResumeSessionConfig { pub enable_host_git_operations: Option, /// When true, enables the session store on resume. pub enable_session_store: Option, + /// Identity used to isolate the resumed session's persisted session-store data. + pub session_store_identity: Option, /// When true, enables skills on resume. pub enable_skills: Option, /// **Experimental.** This option is part of an experimental wire-protocol @@ -3692,6 +3726,7 @@ impl std::fmt::Debug for ResumeSessionConfig { &self.enable_host_git_operations, ) .field("enable_session_store", &self.enable_session_store) + .field("session_store_identity", &self.session_store_identity) .field("enable_skills", &self.enable_skills) .field("enable_mcp_apps", &self.enable_mcp_apps) .field("skill_directories", &self.skill_directories) @@ -3863,6 +3898,7 @@ impl ResumeSessionConfig { enable_file_hooks: self.enable_file_hooks, enable_host_git_operations: self.enable_host_git_operations, enable_session_store: self.enable_session_store, + session_store_identity: self.session_store_identity, enable_skills: self.enable_skills, request_user_input, request_permission: permission_active, @@ -3970,6 +4006,7 @@ impl ResumeSessionConfig { enable_file_hooks: None, enable_host_git_operations: None, enable_session_store: None, + session_store_identity: None, enable_skills: None, embedding_cache_storage: None, enable_mcp_apps: None, @@ -4331,6 +4368,12 @@ impl ResumeSessionConfig { self } + /// Set the identity used to isolate persisted session-store data on resume. + pub fn with_session_store_identity(mut self, identity: SessionStoreIdentity) -> Self { + self.session_store_identity = Some(identity); + self + } + /// Set [`Self::enable_skills`]. pub fn with_enable_skills(mut self, value: bool) -> Self { self.enable_skills = Some(value); @@ -6147,8 +6190,8 @@ mod tests { InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig, ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, - SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, - ToolResultResponse, ensure_attachment_display_names, + SessionId, SessionStoreIdentity, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, + ToolResultExpanded, ToolResultResponse, ensure_attachment_display_names, }; use crate::generated::session_events::TypedSessionEvent; @@ -6436,6 +6479,60 @@ mod tests { assert!(json.get("askUserVariant").is_none()); } + #[test] + fn session_store_identity_serializes_to_exact_create_and_resume_wire_shape() { + let identity = SessionStoreIdentity::new("https://github.com", "123456"); + assert_eq!( + serde_json::to_value(&identity).unwrap(), + json!({ + "authority": "https://github.com", + "accountId": "123456" + }) + ); + + let (create_wire, _) = SessionConfig::default() + .with_session_store_identity(identity.clone()) + .into_wire(Some(SessionId::from("store-identity-create"))) + .expect("create config has no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!( + create_json["sessionStoreIdentity"], + json!({ + "authority": "https://github.com", + "accountId": "123456" + }) + ); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("store-identity-resume")) + .with_session_store_identity(identity) + .into_wire() + .expect("resume config has no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!( + resume_json["sessionStoreIdentity"], + json!({ + "authority": "https://github.com", + "accountId": "123456" + }) + ); + } + + #[test] + fn session_store_identity_is_omitted_when_absent() { + let (create_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("store-identity-create-unset"))) + .expect("create config has no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert!(create_json.get("sessionStoreIdentity").is_none()); + + let (resume_wire, _) = + ResumeSessionConfig::new(SessionId::from("store-identity-resume-unset")) + .into_wire() + .expect("resume config has no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert!(resume_json.get("sessionStoreIdentity").is_none()); + } + #[test] fn custom_agents_local_only_serializes_on_create_and_resume() { let (create_wire, _) = SessionConfig::default() diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 75e17f4e9..6abf6adce 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -29,7 +29,7 @@ use crate::types::{ CustomAgentConfig, DefaultAgentConfig, ExtensionInfo, GitHubMcpToolConfig, InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig, ProviderModelConfig, SessionId, SessionLimitsConfig, - SystemMessageConfig, Tool, ToolSearchConfig, + SessionStoreIdentity, SystemMessageConfig, Tool, ToolSearchConfig, }; /// Wire representation of a slash command (name + description only). The @@ -111,6 +111,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub enable_session_store: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub session_store_identity: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, pub request_user_input: bool, pub request_permission: bool, @@ -270,6 +272,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub enable_session_store: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub session_store_identity: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, pub request_user_input: bool, pub request_permission: bool, diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index e7bafc683..8f1b505ca 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -31,7 +31,8 @@ use github_copilot_sdk::types::{ ElicitationRequest, ElicitationResult, ExitPlanModeData, ExtensionInfo, ManagedSettings, ManagedSettingsPermissions, MessageOptions, PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, PermissionDecisionSurface, RequestId, - SessionConfig, SessionId, SetModelOptions, Tool, ToolInvocation, ToolResult, + SessionConfig, SessionId, SessionStoreIdentity, SetModelOptions, Tool, ToolInvocation, + ToolResult, }; use github_copilot_sdk::{Client, ContextTier, ErrorKind, ProtocolErrorKind, tool}; use serde_json::Value; @@ -2092,6 +2093,57 @@ async fn list_sessions_returns_typed_metadata() { assert_eq!(sessions[0].summary, Some("test session".to_string())); } +#[tokio::test] +async fn set_session_store_identity_sends_exact_wire_shape() { + let (client, mut server_read, mut server_write) = make_client(); + let identity = SessionStoreIdentity::new("https://github.com", "123456"); + + let handle = tokio::spawn({ + let client = client.clone(); + async move { client.set_session_store_identity(Some(&identity)).await } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "sessionStore.setIdentity"); + assert_eq!( + request["params"], + serde_json::json!({ + "identity": { + "authority": "https://github.com", + "accountId": "123456" + } + }) + ); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": request["id"], + "result": {} + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + handle.await.unwrap().unwrap(); +} + +#[tokio::test] +async fn set_session_store_identity_sends_null_to_clear() { + let (client, mut server_read, mut server_write) = make_client(); + + let handle = tokio::spawn({ + let client = client.clone(); + async move { client.set_session_store_identity(None).await } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "sessionStore.setIdentity"); + assert_eq!(request["params"], serde_json::json!({ "identity": null })); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": request["id"], + "result": {} + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + handle.await.unwrap().unwrap(); +} + #[tokio::test] async fn list_sessions_serializes_typed_filter() { use github_copilot_sdk::SessionListFilter;