diff --git a/go/client_test.go b/go/client_test.go index d0139eb11..2332a7701 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -3834,6 +3834,26 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { } }) + t.Run("accepts future bypass-permissions modes", func(t *testing.T) { + req := createSessionRequest{ManagedSettings: &ManagedSettings{ + Permissions: &ManagedSettingsPermissions{ + DisableBypassPermissionsMode: DisableBypassPermissionsMode("future-fail-closed-mode"), + }, + }} + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + perms := m["managedSettings"].(map[string]any)["permissions"].(map[string]any) + if perms["disableBypassPermissionsMode"] != "future-fail-closed-mode" { + t.Errorf("Expected future mode preserved, got %v", perms["disableBypassPermissionsMode"]) + } + }) + t.Run("omits managedSettings when nil", func(t *testing.T) { req := createSessionRequest{} data, _ := json.Marshal(req) diff --git a/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go b/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go index 0267f8d04..60fc2bd83 100644 --- a/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go +++ b/go/internal/e2e/rpc_tasks_and_handlers_e2e_test.go @@ -127,6 +127,8 @@ func TestRPCTasksAndHandlersE2E(t *testing.T) { }) t.Run("should report implemented error for invalid task agent model", func(t *testing.T) { + ctx.ConfigureForTest(t) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) diff --git a/go/types.go b/go/types.go index 60781d1da..f2218b27e 100644 --- a/go/types.go +++ b/go/types.go @@ -1569,11 +1569,16 @@ type ManagedSettings struct { } // DisableBypassPermissionsMode is the managed bypass-permissions policy. -type DisableBypassPermissionsMode = rpc.DisableBypassPermissionsMode +// +// The runtime may introduce additional fail-closed modes. Values are serialized +// as strings so callers can use newer modes without waiting for an SDK release. +type DisableBypassPermissionsMode string const ( // DisableBypassPermissionsModeDisable turns off bypass-permissions mode. - DisableBypassPermissionsModeDisable = rpc.DisableBypassPermissionsModeDisable + DisableBypassPermissionsModeDisable DisableBypassPermissionsMode = "disable" + // DisableBypassPermissionsModeAllowAutoOnly permits only automatic bypass. + DisableBypassPermissionsModeAllowAutoOnly DisableBypassPermissionsMode = "allow-auto-only" ) // ManagedSettingsPermissions is the permissions-only managed policy injected @@ -1581,9 +1586,8 @@ const ( // accepts for fetched managed policy (e.g. "Read(**)", "Shell(git push *)"); // malformed rules are rejected by the runtime at session creation. type ManagedSettingsPermissions struct { - // DisableBypassPermissionsMode, when set to "disable", turns off - // bypass-permissions ("yolo") mode for the session. Deny-wins: no other - // layer can re-enable it. + // DisableBypassPermissionsMode controls bypass-permissions ("yolo") mode for + // the session. Deny-wins: no other layer can grant broader bypass permissions. DisableBypassPermissionsMode DisableBypassPermissionsMode `json:"disableBypassPermissionsMode,omitempty"` // Deny lists operations that must always be denied. Unioned across layers. Deny []string `json:"deny,omitzero"` diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/DisableBypassPermissionsModes.java b/java/sdk/src/main/java/com/github/copilot/rpc/DisableBypassPermissionsModes.java new file mode 100644 index 000000000..cf98f0526 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/DisableBypassPermissionsModes.java @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +package com.github.copilot.rpc; + +/** + * Known values for the managed bypass-permissions policy. + * + *

+ * The wire contract is an open string so callers can pass newer fail-closed + * modes directly to + * {@link ManagedSettingsPermissions#setDisableBypassPermissionsMode(String)}. + */ +public final class DisableBypassPermissionsModes { + /** Turns off bypass-permissions mode. */ + public static final String DISABLE = "disable"; + + /** Permits bypass only for automatic operations. */ + public static final String ALLOW_AUTO_ONLY = "allow-auto-only"; + + private DisableBypassPermissionsModes() { + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java b/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java index 0923cea54..dd714621b 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ManagedSettingsPermissions.java @@ -15,7 +15,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public final class ManagedSettingsPermissions { @JsonProperty("disableBypassPermissionsMode") - private DisableBypassPermissionsMode disableBypassPermissionsMode; + private String disableBypassPermissionsMode; @JsonProperty("deny") private List deny; @@ -27,19 +27,30 @@ public final class ManagedSettingsPermissions { private List allow; /** @return the bypass-permissions policy, or {@code null} when unset */ - public DisableBypassPermissionsMode getDisableBypassPermissionsMode() { + public String getDisableBypassPermissionsMode() { return disableBypassPermissionsMode; } /** - * Disables bypass/allow-all permission modes. + * @param value + * bypass-permissions policy + * @return this policy + */ + public ManagedSettingsPermissions setDisableBypassPermissionsMode(String value) { + this.disableBypassPermissionsMode = value; + return this; + } + + /** + * Sets the bypass-permissions policy from the generated enum retained for + * source compatibility. * * @param value * bypass-permissions policy * @return this policy */ public ManagedSettingsPermissions setDisableBypassPermissionsMode(DisableBypassPermissionsMode value) { - this.disableBypassPermissionsMode = value; + this.disableBypassPermissionsMode = value == null ? null : value.getValue(); return this; } diff --git a/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java b/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java index dbd19f3c9..dd7730351 100644 --- a/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ManagedSettingsTest.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.copilot.generated.rpc.DisableBypassPermissionsMode; +import com.github.copilot.rpc.DisableBypassPermissionsModes; import com.github.copilot.rpc.ManagedSettings; import com.github.copilot.rpc.ManagedSettingsPermissions; import com.github.copilot.rpc.PermissionRequestResult; @@ -41,6 +42,25 @@ void forwardsManagedSettingsOnCreateAndResume() throws Exception { assertTrue(json.contains("\"disableBypassPermissionsMode\":\"disable\"")); } + @Test + void serializesKnownBypassPermissionsModes() throws Exception { + var permissions = new ManagedSettingsPermissions() + .setDisableBypassPermissionsMode(DisableBypassPermissionsModes.ALLOW_AUTO_ONLY); + var json = new ObjectMapper().writeValueAsString(permissions); + + assertEquals(DisableBypassPermissionsModes.ALLOW_AUTO_ONLY, permissions.getDisableBypassPermissionsMode()); + assertTrue(json.contains("\"disableBypassPermissionsMode\":\"allow-auto-only\"")); + } + + @Test + void acceptsFutureBypassPermissionsModes() throws Exception { + var permissions = new ManagedSettingsPermissions().setDisableBypassPermissionsMode("future-fail-closed-mode"); + var json = new ObjectMapper().writeValueAsString(permissions); + + assertEquals("future-fail-closed-mode", permissions.getDisableBypassPermissionsMode()); + assertTrue(json.contains("\"disableBypassPermissionsMode\":\"future-fail-closed-mode\"")); + } + @Test void preservesExplicitEmptyPermissionArrays() throws Exception { // Security-critical: a present empty allow list admits nothing, while an diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 5c0674469..8732938c7 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2155,15 +2155,15 @@ impl Client { /// auto-generated token for SDK-spawned TCP servers) as the `token` /// param. Server-side, the token is required when the server was /// started with `COPILOT_CONNECTION_TOKEN`. + #[expect( + clippy::field_reassign_with_default, + reason = "generated requests can gain optional fields without requiring SDK changes" + )] async fn connect_handshake(&self) -> Result> { - let params = crate::generated::api_types::ConnectRequest { - token: self.inner.effective_connection_token.clone(), - enable_git_hub_telemetry_forwarding: self - .inner - .on_github_telemetry - .is_some() - .then_some(true), - }; + let mut params = crate::generated::api_types::ConnectRequest::default(); + params.token = self.inner.effective_connection_token.clone(); + params.enable_git_hub_telemetry_forwarding = + self.inner.on_github_telemetry.is_some().then_some(true); let value = self .call( crate::generated::api_types::rpc_methods::CONNECT,