Skip to content
Open
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
17 changes: 17 additions & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
101 changes: 99 additions & 2 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
gimenete marked this conversation as resolved.
/// 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<String>, account_id: impl Into<String>) -> 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
Expand Down Expand Up @@ -2029,6 +2050,8 @@ pub struct SessionConfig {
pub enable_host_git_operations: Option<bool>,
/// When true, enables the session store for this session.
pub enable_session_store: Option<bool>,
/// Identity used to isolate this session's persisted session-store data.
pub session_store_identity: Option<SessionStoreIdentity>,
/// When true, enables skills for this session.
pub enable_skills: Option<bool>,
/// **Experimental.** This option is part of an experimental wire-protocol
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -3462,6 +3494,8 @@ pub struct ResumeSessionConfig {
pub enable_host_git_operations: Option<bool>,
/// When true, enables the session store on resume.
pub enable_session_store: Option<bool>,
/// Identity used to isolate the resumed session's persisted session-store data.
pub session_store_identity: Option<SessionStoreIdentity>,
/// When true, enables skills on resume.
pub enable_skills: Option<bool>,
/// **Experimental.** This option is part of an experimental wire-protocol
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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()
Expand Down
6 changes: 5 additions & 1 deletion rust/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -111,6 +111,8 @@ pub(crate) struct SessionCreateWire {
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_session_store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_store_identity: Option<SessionStoreIdentity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_skills: Option<bool>,
pub request_user_input: bool,
pub request_permission: bool,
Expand Down Expand Up @@ -270,6 +272,8 @@ pub(crate) struct SessionResumeWire {
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_session_store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_store_identity: Option<SessionStoreIdentity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_skills: Option<bool>,
pub request_user_input: bool,
pub request_permission: bool,
Expand Down
54 changes: 53 additions & 1 deletion rust/tests/session_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading