From 4348ec7cb346fa3a2d817aa25803f6f90b7e79fb Mon Sep 17 00:00:00 2001
From: Copilot <223556219+Copilot@users.noreply.github.com>
Date: Tue, 6 Oct 2026 15:40:49 +0000
Subject: [PATCH 1/2] feat(rust): port read-only original-session policy getter
Port only published leaf 7682348bf1ba6cead28ba7304f3edfa5acb42d0e (parent 6f67591aea97825b61bd866130f3ab6e96688f07, tree 87247671332250222cd88696d3bd96723c229939) onto recorded SDK baseline ef04633cc84e4ba8e79888a39259ca276f5de732. Applied via git cherry-pick --no-commit without conflicts; stable patch ID 84ff457e169aca1de6790c95cbd024e0c449f31d matches exactly.
Generated Rust declarations are reused from the published leaf, not hand-edited or independently regenerated here. Source reports API SHA256 7f2f51e8ea294ec8fa116da77ff58bf0e1fea4d8fb0b4bcfd460195e2fd667f6 and events SHA256 1e363c28ca3b2197ea4baf2ba25980138eda63213cd8a6585151deeb111a882d. Canonical runtime a1c2f57babcdd3dea5a406ca34e726745ef8ba74 and predecessor eaec03ae972f75282fe5122bebd54cba3c6cae01 are caller-supplied provenance, not an independent canonical derivation performed in this port.
Validated serially using locked external-stream-only Cargo builds: 3 focused getter tests, 178 affected session tests, 11 framed JSON-RPC tests, nightly format check and focused Clippy. One unrelated Client::start configuration test requires the runtime feature and was excluded from the passing session run. Preserve all other files, public APIs, dependency manifests/locks, Node/build/harness paths and CLI pins. No app/runtime launch or guard input consumption.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
rust/README.md | 13 +++
rust/src/generated/api_types.rs | 59 +++++++++++++
rust/src/generated/rpc.rs | 28 ++++++
rust/tests/session_test.rs | 150 +++++++++++++++++++++++++++++++-
4 files changed, 249 insertions(+), 1 deletion(-)
diff --git a/rust/README.md b/rust/README.md
index 6e91639175..3d16a7c718 100644
--- a/rust/README.md
+++ b/rust/README.md
@@ -464,6 +464,19 @@ New RPCs land in the namespace immediately as the schema regenerates;
helpers are added on top only when an ergonomic story is worth the
maintenance.
+The experimental `session.rpc().remote().get_policy_inputs().await?` returns
+`github_copilot_sdk::rpc::RemotePolicyInputs` for that original live session,
+using its existing subscription. Its four required booleans are
+`managed_remote_control_setting`, `managed_remote_control_staff_override`,
+`owner_adc_sandbox`, and `owner_codespaces`. The managed flags include native
+defaults; the environment booleans describe the session's owning runtime
+process (`ADC_SANDBOX_ID` nonempty and `CODESPACES` exactly `"true"`).
+The call does not enable export or steering, apply managed policy, or expose
+raw flags or environment values. Missing or unattached sessions, unsupported
+runtimes, and malformed responses return errors rather than inferred inputs.
+Use a matching runtime and its emitted schemas for this unreleased API; the
+SDK change alone does not add support to an older pinned CLI.
+
#### Typed MCP installation and removal payloads (breaking change)
Three payloads in the experimental MCP installation and removal workflow are now typed
diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs
index b06fad33d4..507ba69bcc 100644
--- a/rust/src/generated/api_types.rs
+++ b/rust/src/generated/api_types.rs
@@ -905,6 +905,8 @@ pub mod rpc_methods {
pub const SESSION_REMOTE_ENABLE: &str = "session.remote.enable";
/// `session.remote.disable`
pub const SESSION_REMOTE_DISABLE: &str = "session.remote.disable";
+ /// `session.remote.getPolicyInputs`
+ pub const SESSION_REMOTE_GETPOLICYINPUTS: &str = "session.remote.getPolicyInputs";
/// `session.remote.notifySteerableChanged`
pub const SESSION_REMOTE_NOTIFYSTEERABLECHANGED: &str = "session.remote.notifySteerableChanged";
/// `session.visibility.get`
@@ -18965,6 +18967,27 @@ pub struct RemoteNotifySteerableChangedRequest {
#[serde(rename_all = "camelCase")]
pub struct RemoteNotifySteerableChangedResult {}
+/// Read-only remote-policy inputs from the original live session and its owning runtime process.
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol surface
+/// and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct RemotePolicyInputs {
+ /// Resolved MANAGED_REMOTE_CONTROL_SETTING flag, including the runtime default when no boolean override exists.
+ pub managed_remote_control_setting: bool,
+ /// Resolved MANAGED_REMOTE_CONTROL_SETTING_STAFF_OVERRIDE flag, including the runtime default when no boolean override exists.
+ pub managed_remote_control_staff_override: bool,
+ /// Whether ADC_SANDBOX_ID is nonempty in the original session's owning runtime process.
+ pub owner_adc_sandbox: bool,
+ /// Whether CODESPACES is the literal string "true" in the original session's owning runtime process.
+ pub owner_codespaces: bool,
+}
+
/// Remote session connection result.
///
///
@@ -34706,6 +34729,42 @@ pub struct SessionRemoteDisableParams {
pub session_id: SessionId,
}
+/// Identifies the target session.
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol surface
+/// and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SessionRemoteGetPolicyInputsParams {
+ /// Target session identifier
+ pub session_id: SessionId,
+}
+
+/// Read-only remote-policy inputs from the original live session and its owning runtime process.
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol surface
+/// and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SessionRemoteGetPolicyInputsResult {
+ /// Resolved MANAGED_REMOTE_CONTROL_SETTING flag, including the runtime default when no boolean override exists.
+ pub managed_remote_control_setting: bool,
+ /// Resolved MANAGED_REMOTE_CONTROL_SETTING_STAFF_OVERRIDE flag, including the runtime default when no boolean override exists.
+ pub managed_remote_control_staff_override: bool,
+ /// Whether ADC_SANDBOX_ID is nonempty in the original session's owning runtime process.
+ pub owner_adc_sandbox: bool,
+ /// Whether CODESPACES is the literal string "true" in the original session's owning runtime process.
+ pub owner_codespaces: bool,
+}
+
/// Persist a steerability change as a `session.remote_steerable_changed` event. Used by the host (CLI / SDK consumer) when it has just finished enabling or disabling steering on a remote exporter that the runtime does not directly own.
///
///
diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs
index cf87a42bd1..9f76c2d3a6 100644
--- a/rust/src/generated/rpc.rs
+++ b/rust/src/generated/rpc.rs
@@ -12167,6 +12167,34 @@ impl<'a> SessionRpcRemote<'a> {
Ok(())
}
+ /// Reads the original live session's resolved remote-policy feature flags and owner-process environment booleans without changing remote export or steering. Requires the caller's already-established session subscription.
+ ///
+ /// Wire method: `session.remote.getPolicyInputs`.
+ ///
+ /// # Returns
+ ///
+ /// Read-only remote-policy inputs from the original live session and its owning runtime process.
+ ///
+ ///
+ ///
+ /// **Experimental.** This API is part of an experimental wire-protocol surface
+ /// and may change or be removed in future SDK or CLI releases. Pin both the
+ /// SDK and CLI versions if your code depends on it.
+ ///
+ ///
+ pub async fn get_policy_inputs(&self) -> Result
{
+ let wire_params = serde_json::json!({ "sessionId": self.session.id() });
+ let _value = self
+ .session
+ .client()
+ .call(
+ rpc_methods::SESSION_REMOTE_GETPOLICYINPUTS,
+ Some(wire_params),
+ )
+ .await?;
+ Ok(serde_json::from_value(_value)?)
+ }
+
/// Persists a remote-steerability change emitted by the host as a session event.
///
/// Wire method: `session.remote.notifySteerableChanged`.
diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs
index e38b0c427d..49400972bb 100644
--- a/rust/tests/session_test.rs
+++ b/rust/tests/session_test.rs
@@ -24,7 +24,7 @@ use github_copilot_sdk::rpc::{
ConnectorConnectResult, ConnectorContinueRequest, ConnectorDisconnectResult,
ConnectorMcpStatus, ConnectorReconcileOptions, ConnectorReconcileRequest,
ConnectorSessionAccount, ConnectorStatus, ModelSetAllowedModelsRequest, OpenCanvasInstance,
- SendAgentMode, SendMode, SendRequest, SessionRpcConnectors,
+ RemotePolicyInputs, SendAgentMode, SendMode, SendRequest, SessionRpcConnectors,
};
use github_copilot_sdk::session_events::{
ManagedSettingsResolvedSource, McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig,
@@ -7336,6 +7336,154 @@ async fn rpc_namespace_session_connectors_dispatches_all_methods() {
timeout(TIMEOUT, server_handle).await.unwrap().unwrap();
}
+#[tokio::test]
+async fn remote_policy_inputs_preserves_original_session_and_all_boolean_combinations() {
+ let (session, mut server) =
+ create_session_pair_with_config(|cfg| cfg.with_session_id("original-policy-session")).await;
+ let session = Arc::new(session);
+ assert_eq!(session.id().as_str(), "original-policy-session");
+
+ for bits in 0..16 {
+ let expected = serde_json::json!({
+ "managedRemoteControlSetting": bits & 1 != 0,
+ "managedRemoteControlStaffOverride": bits & 2 != 0,
+ "ownerAdcSandbox": bits & 4 != 0,
+ "ownerCodespaces": bits & 8 != 0,
+ });
+ let handle = tokio::spawn({
+ let session = session.clone();
+ async move { session.rpc().remote().get_policy_inputs().await }
+ });
+ let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
+ assert_eq!(request["method"], "session.remote.getPolicyInputs");
+ assert_eq!(
+ request["params"],
+ serde_json::json!({ "sessionId": "original-policy-session" })
+ );
+ server.respond(&request, expected.clone()).await;
+
+ let result: RemotePolicyInputs = timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap();
+ assert_eq!(result.managed_remote_control_setting, bits & 1 != 0);
+ assert_eq!(result.managed_remote_control_staff_override, bits & 2 != 0);
+ assert_eq!(result.owner_adc_sandbox, bits & 4 != 0);
+ assert_eq!(result.owner_codespaces, bits & 8 != 0);
+ assert_eq!(serde_json::to_value(result).unwrap(), expected);
+ }
+}
+
+#[tokio::test]
+async fn remote_policy_inputs_rejects_missing_and_malformed_required_booleans() {
+ let (session, mut server) = create_session_pair().await;
+ let session = Arc::new(session);
+ let valid = serde_json::json!({
+ "managedRemoteControlSetting": false,
+ "managedRemoteControlStaffOverride": false,
+ "ownerAdcSandbox": false,
+ "ownerCodespaces": false,
+ });
+ let mut malformed = vec![Value::Null, serde_json::json!([]), serde_json::json!({})];
+ for field in [
+ "managedRemoteControlSetting",
+ "managedRemoteControlStaffOverride",
+ "ownerAdcSandbox",
+ "ownerCodespaces",
+ ] {
+ let mut missing = valid.clone();
+ missing.as_object_mut().unwrap().remove(field);
+ malformed.push(missing);
+ for value in [
+ Value::Null,
+ serde_json::json!("false"),
+ serde_json::json!(0),
+ serde_json::json!([]),
+ serde_json::json!({}),
+ ] {
+ let mut response = valid.clone();
+ response[field] = value;
+ malformed.push(response);
+ }
+ }
+
+ for response in malformed {
+ let handle = tokio::spawn({
+ let session = session.clone();
+ async move { session.rpc().remote().get_policy_inputs().await }
+ });
+ let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
+ assert_eq!(request["method"], "session.remote.getPolicyInputs");
+ assert_eq!(
+ request["params"],
+ serde_json::json!({ "sessionId": server.session_id })
+ );
+ server.respond(&request, response.clone()).await;
+
+ let error = timeout(TIMEOUT, handle)
+ .await
+ .unwrap()
+ .unwrap()
+ .expect_err("malformed policy inputs must not default to false");
+ assert_eq!(error.kind(), &ErrorKind::Json, "response: {response}");
+ }
+}
+
+#[tokio::test]
+async fn remote_policy_inputs_propagates_rpc_errors_without_fallback() {
+ let (session, mut server) = create_session_pair().await;
+ let session = Arc::new(session);
+ for (code, message) in [
+ (-32601, "Method not found"),
+ (-32000, "Original session not found"),
+ (-32000, "Original session is not attached"),
+ ] {
+ let handle = tokio::spawn({
+ let session = session.clone();
+ async move { session.rpc().remote().get_policy_inputs().await }
+ });
+ let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
+ assert_eq!(request["method"], "session.remote.getPolicyInputs");
+ assert_eq!(
+ request["params"],
+ serde_json::json!({ "sessionId": server.session_id })
+ );
+ let data = serde_json::json!({ "sessionId": server.session_id });
+ let response = serde_json::json!({
+ "jsonrpc": "2.0",
+ "id": request["id"],
+ "error": { "code": code, "message": message, "data": data },
+ });
+ write_framed(&mut server.write, &serde_json::to_vec(&response).unwrap()).await;
+
+ let error = timeout(TIMEOUT, handle)
+ .await
+ .unwrap()
+ .unwrap()
+ .expect_err("RPC rejection must propagate");
+ assert_eq!(error.kind(), &ErrorKind::Rpc { code });
+ assert_eq!(error.message(), Some(message));
+ assert_eq!(error.rpc_data(), Some(&data));
+ }
+
+ let handle = tokio::spawn(async move { session.rpc().remote().get_policy_inputs().await });
+ let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
+ assert_eq!(request["method"], "session.remote.getPolicyInputs");
+ assert_eq!(
+ request["params"],
+ serde_json::json!({ "sessionId": server.session_id })
+ );
+ server
+ .respond(
+ &request,
+ serde_json::json!({
+ "managedRemoteControlSetting": false,
+ "managedRemoteControlStaffOverride": false,
+ "ownerAdcSandbox": false,
+ "ownerCodespaces": false,
+ }),
+ )
+ .await;
+ timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap();
+}
+
#[tokio::test]
async fn rpc_namespace_client_models_list_dispatches_correctly() {
let (session, mut server) = create_session_pair().await;
From bbb80a920920b9feba9eb56dcc7a75217432a1fe Mon Sep 17 00:00:00 2001
From: Copilot <223556219+Copilot@users.noreply.github.com>
Date: Tue, 6 Oct 2026 16:25:55 +0000
Subject: [PATCH 2/2] feat(rust): add typed original-owner export guard RPC
Port only the canonical guardExport method constant, RemoteGuardExportResult, SessionRemoteGuardExportParams/Result and SessionRpcRemote::guard_export from published github/copilot-agent-runtime eae65d27df485bad0e2849d01672436e2c726470 (parent 1855ddfafd3194a4a28c6631c45a9cd1ddc688e3, tree ce1f17a33732584582dcc5e5a1bdd8c54454d35b) onto SDK 4348ec7cb346fa3a2d817aa25803f6f90b7e79fb.
Independently verified remote source ref, immutable commit/tree and full Git blobs. API schema blob b92419fc405c1f0212691bd72ebc209512113c0f is 2234776 bytes, SHA256 cfa51e2835189c419dd68c3b90921817673802a7bbf58b48777c83ce17953be5. Events blob 7c2ce2b282daefa116f47f3a6712502ff80662ed SHA256 1e363c28ca3b2197ea4baf2ba25980138eda63213cd8a6585151deeb111a882d. Generated source blobs: api_types.rs 1908532835dfde2ca3aa12dae07b252ebb32df78, rpc.rs a25bcc79c9f1578179c1ff3632f23d81aec2bcf1. Canonical guard-only blocks extracted mechanically and verified byte-for-byte, not regenerated independently or hand-authored. Removing the additions reproduces both SDK baseline generated files exactly.
Framed tests cover retained original sessionId, exact method and params, true/false receipts, missing/null/wrong types, unknown/unsubscribed/non-origin/missing-session RPC refusals and absence of extra/fallback/mutating RPCs. Serial locked external-stream validation: 6 getter/guard tests, 181 applicable session tests and 11 JSON-RPC tests passed; nightly format and focused Clippy passed. Unchanged Client::start validation test still requires runtime feature and fails in external-stream-only mode; no assertions weakened. No live runtime ownership validation claimed.
Only four approved Rust leaf paths change. Preserve getter, all other APIs/files, CLI/Node/dependency/build/harness pins and protocol/events. No runtime/app launch, listener/export creation, mode change, PR or packages. SDK transport availability does not admit an executable/provider or complete github/github-app#18298.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
rust/README.md | 15 ++++
rust/src/generated/api_types.rs | 47 ++++++++++++
rust/src/generated/rpc.rs | 25 +++++++
rust/tests/session_test.rs | 125 +++++++++++++++++++++++++++++++-
4 files changed, 211 insertions(+), 1 deletion(-)
diff --git a/rust/README.md b/rust/README.md
index 3d16a7c718..557d6bfb04 100644
--- a/rust/README.md
+++ b/rust/README.md
@@ -477,6 +477,21 @@ runtimes, and malformed responses return errors rather than inferred inputs.
Use a matching runtime and its emitted schemas for this unreleased API; the
SDK change alone does not add support to an older pinned CLI.
+The experimental `original_session.rpc().remote().guard_export().await?`
+returns `github_copilot_sdk::rpc::RemoteGuardExportResult` with a required
+`guarded` boolean. It sends only `session.remote.guardExport` with that retained
+session's own ID; missing or malformed receipts and runtime refusals are errors.
+This explicit RPC irreversibly enrolls the original resident session's
+non-steerable export protection. Native authority belongs only to the still-live
+original subscribed Create or cold-Resume owner, not attached/imported peers or
+resident-resume callers. Peers cannot enroll or clear an owner's guard, and
+protection survives owner disconnect until actual native session retirement.
+Caller policy configuration cannot impersonate authoritative storage revocation;
+real native storage denial or disposing-owner retirement still permits shutdown.
+The SDK does not enable or disable Native Mode, create an Export, start a
+listener, or issue additional RPCs. Use a matching runtime; this API alone does
+not admit an executable or provider.
+
#### Typed MCP installation and removal payloads (breaking change)
Three payloads in the experimental MCP installation and removal workflow are now typed
diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs
index 507ba69bcc..516dcdb2a8 100644
--- a/rust/src/generated/api_types.rs
+++ b/rust/src/generated/api_types.rs
@@ -907,6 +907,8 @@ pub mod rpc_methods {
pub const SESSION_REMOTE_DISABLE: &str = "session.remote.disable";
/// `session.remote.getPolicyInputs`
pub const SESSION_REMOTE_GETPOLICYINPUTS: &str = "session.remote.getPolicyInputs";
+ /// `session.remote.guardExport`
+ pub const SESSION_REMOTE_GUARDEXPORT: &str = "session.remote.guardExport";
/// `session.remote.notifySteerableChanged`
pub const SESSION_REMOTE_NOTIFYSTEERABLECHANGED: &str = "session.remote.notifySteerableChanged";
/// `session.visibility.get`
@@ -18940,6 +18942,21 @@ pub struct RemoteEnableResult {
pub url: Option,
}
+/// Successful original-owner enrollment of the resident session's monotonic export protection.
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol surface
+/// and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct RemoteGuardExportResult {
+ /// True after successful enrollment. There is no reset operation.
+ pub guarded: bool,
+}
+
/// New remote-steerability state to persist as a `session.remote_steerable_changed` event.
///
///
@@ -34765,6 +34782,36 @@ pub struct SessionRemoteGetPolicyInputsResult {
pub owner_codespaces: bool,
}
+/// Identifies the target session.
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol surface
+/// and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SessionRemoteGuardExportParams {
+ /// Target session identifier
+ pub session_id: SessionId,
+}
+
+/// Successful original-owner enrollment of the resident session's monotonic export protection.
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol surface
+/// and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SessionRemoteGuardExportResult {
+ /// True after successful enrollment. There is no reset operation.
+ pub guarded: bool,
+}
+
/// Persist a steerability change as a `session.remote_steerable_changed` event. Used by the host (CLI / SDK consumer) when it has just finished enabling or disabling steering on a remote exporter that the runtime does not directly own.
///
///
diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs
index 9f76c2d3a6..a7020a22eb 100644
--- a/rust/src/generated/rpc.rs
+++ b/rust/src/generated/rpc.rs
@@ -12195,6 +12195,31 @@ impl<'a> SessionRpcRemote<'a> {
Ok(serde_json::from_value(_value)?)
}
+ /// Irreversibly protects the original resident session's non-steerable export from native steering or disposal. Requires its still-live original SDK creation authority. Attachment, import, and resident resume do not confer this authority; protection survives owner disconnect until actual native session retirement.
+ ///
+ /// Wire method: `session.remote.guardExport`.
+ ///
+ /// # Returns
+ ///
+ /// Successful original-owner enrollment of the resident session's monotonic export protection.
+ ///
+ ///
+ ///
+ /// **Experimental.** This API is part of an experimental wire-protocol surface
+ /// and may change or be removed in future SDK or CLI releases. Pin both the
+ /// SDK and CLI versions if your code depends on it.
+ ///
+ ///
+ pub async fn guard_export(&self) -> Result
{
+ let wire_params = serde_json::json!({ "sessionId": self.session.id() });
+ let _value = self
+ .session
+ .client()
+ .call(rpc_methods::SESSION_REMOTE_GUARDEXPORT, Some(wire_params))
+ .await?;
+ Ok(serde_json::from_value(_value)?)
+ }
+
/// Persists a remote-steerability change emitted by the host as a session event.
///
/// Wire method: `session.remote.notifySteerableChanged`.
diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs
index 49400972bb..588535e83c 100644
--- a/rust/tests/session_test.rs
+++ b/rust/tests/session_test.rs
@@ -24,7 +24,8 @@ use github_copilot_sdk::rpc::{
ConnectorConnectResult, ConnectorContinueRequest, ConnectorDisconnectResult,
ConnectorMcpStatus, ConnectorReconcileOptions, ConnectorReconcileRequest,
ConnectorSessionAccount, ConnectorStatus, ModelSetAllowedModelsRequest, OpenCanvasInstance,
- RemotePolicyInputs, SendAgentMode, SendMode, SendRequest, SessionRpcConnectors,
+ RemoteGuardExportResult, RemotePolicyInputs, SendAgentMode, SendMode, SendRequest,
+ SessionRpcConnectors,
};
use github_copilot_sdk::session_events::{
ManagedSettingsResolvedSource, McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig,
@@ -7484,6 +7485,128 @@ async fn remote_policy_inputs_propagates_rpc_errors_without_fallback() {
timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap();
}
+#[tokio::test]
+async fn remote_guard_export_preserves_original_session_and_boolean_receipts() {
+ let (session, mut server) =
+ create_session_pair_with_config(|cfg| cfg.with_session_id("original-guard-session")).await;
+ let session = Arc::new(session);
+ assert_eq!(session.id().as_str(), "original-guard-session");
+
+ for guarded in [true, false] {
+ let handle = tokio::spawn({
+ let session = session.clone();
+ async move { session.rpc().remote().guard_export().await }
+ });
+ let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
+ assert_eq!(request["method"], "session.remote.guardExport");
+ assert_eq!(
+ request["params"],
+ serde_json::json!({ "sessionId": "original-guard-session" })
+ );
+ let expected = serde_json::json!({ "guarded": guarded });
+ server.respond(&request, expected.clone()).await;
+
+ let result: RemoteGuardExportResult =
+ timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap();
+ assert_eq!(result.guarded, guarded);
+ assert_eq!(serde_json::to_value(result).unwrap(), expected);
+ assert!(
+ timeout(Duration::from_millis(50), server.read_request())
+ .await
+ .is_err(),
+ "guard enrollment must not issue extra or mutating RPCs"
+ );
+ }
+}
+
+#[tokio::test]
+async fn remote_guard_export_rejects_missing_and_malformed_required_boolean() {
+ let (session, mut server) =
+ create_session_pair_with_config(|cfg| cfg.with_session_id("original-guard-session")).await;
+ let session = Arc::new(session);
+ for response in [
+ Value::Null,
+ serde_json::json!([]),
+ serde_json::json!({}),
+ serde_json::json!({ "guarded": null }),
+ serde_json::json!({ "guarded": "false" }),
+ serde_json::json!({ "guarded": 0 }),
+ serde_json::json!({ "guarded": [] }),
+ serde_json::json!({ "guarded": {} }),
+ ] {
+ let handle = tokio::spawn({
+ let session = session.clone();
+ async move { session.rpc().remote().guard_export().await }
+ });
+ let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
+ assert_eq!(request["method"], "session.remote.guardExport");
+ assert_eq!(
+ request["params"],
+ serde_json::json!({ "sessionId": "original-guard-session" })
+ );
+ server.respond(&request, response.clone()).await;
+
+ let error = timeout(TIMEOUT, handle)
+ .await
+ .unwrap()
+ .unwrap()
+ .expect_err("malformed guard receipts must not default to false");
+ assert_eq!(error.kind(), &ErrorKind::Json, "response: {response}");
+ assert!(
+ timeout(Duration::from_millis(50), server.read_request())
+ .await
+ .is_err(),
+ "malformed guard receipts must not trigger fallback RPCs"
+ );
+ }
+}
+
+#[tokio::test]
+async fn remote_guard_export_propagates_rpc_refusals_without_fallback() {
+ let (session, mut server) =
+ create_session_pair_with_config(|cfg| cfg.with_session_id("original-guard-session")).await;
+ let session = Arc::new(session);
+ for (code, message) in [
+ (-32601, "Method not found"),
+ (-32000, "Original session is not attached"),
+ (-32000, "Caller is not the original session owner"),
+ (-32000, "Original session not found"),
+ ] {
+ let handle = tokio::spawn({
+ let session = session.clone();
+ async move { session.rpc().remote().guard_export().await }
+ });
+ let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
+ assert_eq!(request["method"], "session.remote.guardExport");
+ assert_eq!(
+ request["params"],
+ serde_json::json!({ "sessionId": "original-guard-session" })
+ );
+ let data = serde_json::json!({ "sessionId": "original-guard-session" });
+ let response = serde_json::json!({
+ "jsonrpc": "2.0",
+ "id": request["id"],
+ "error": { "code": code, "message": message, "data": data },
+ });
+ write_framed(&mut server.write, &serde_json::to_vec(&response).unwrap()).await;
+
+ let error = timeout(TIMEOUT, handle)
+ .await
+ .unwrap()
+ .unwrap()
+ .expect_err("guard enrollment refusals must propagate");
+ assert_eq!(error.kind(), &ErrorKind::Rpc { code });
+ assert_eq!(error.message(), Some(message));
+ assert_eq!(error.rpc_data(), Some(&data));
+ assert!(
+ timeout(Duration::from_millis(50), server.read_request())
+ .await
+ .is_err(),
+ "guard enrollment refusals must not trigger fallback RPCs"
+ );
+ }
+}
+
#[tokio::test]
async fn rpc_namespace_client_models_list_dispatches_correctly() {
let (session, mut server) = create_session_pair().await;