Skip to content

Commit 2a386ac

Browse files
committed
Align managed bypass modes in Node and Rust
Broaden the Node API to forward future fail-closed values and add Rust support for the allow-auto-only policy. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e98a14a6-7ad4-4cb6-b808-e56547701c19
1 parent 67ef658 commit 2a386ac

5 files changed

Lines changed: 58 additions & 23 deletions

File tree

nodejs/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010

1111
export { CopilotClient } from "./client.js";
12-
export { RuntimeConnection } from "./types.js";
12+
export { DisableBypassPermissionsModes, RuntimeConnection } from "./types.js";
1313
export { BuiltInTools, ToolSet } from "./toolSet.js";
1414
export { CopilotSession, type AssistantMessageEvent } from "./session.js";
1515
export { defineFactory, FactoryResumeError, isFactoryRunTerminal } from "./factory.js";

nodejs/src/types.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,6 +2167,14 @@ export interface GitHubMcpToolConfig {
21672167
disableFormDeferral?: boolean;
21682168
}
21692169

2170+
/** Well-known managed bypass-permissions policies. */
2171+
export const DisableBypassPermissionsModes = {
2172+
/** Turn off bypass-permissions mode entirely. */
2173+
Disable: "disable",
2174+
/** Permit automatic bypass but block full allow-all. */
2175+
AllowAutoOnly: "allow-auto-only",
2176+
} as const;
2177+
21702178
/**
21712179
* Permissions-only managed policy injected by the host via
21722180
* {@link SessionConfigBase.managedSettings}.
@@ -2177,11 +2185,11 @@ export interface GitHubMcpToolConfig {
21772185
*/
21782186
export interface ManagedSettingsPermissions {
21792187
/**
2180-
* When set to `"disable"`, bypass-permissions ("yolo") mode is turned off
2181-
* for the session. This is deny-wins: it cannot be re-enabled by any other
2182-
* layer.
2188+
* Restricts bypass-permissions mode for the session. See
2189+
* {@link DisableBypassPermissionsModes} for well-known values. Unknown
2190+
* values are forwarded so newer runtime policies fail closed.
21832191
*/
2184-
disableBypassPermissionsMode?: "disable";
2192+
disableBypassPermissionsMode?: string;
21852193
/** Operations that must always be denied. Unioned across managed layers. */
21862194
deny?: string[];
21872195
/**
@@ -2721,8 +2729,8 @@ export interface SessionConfigBase {
27212729
* with the same managed-permission parser it uses for fetched policy and
27222730
* composes it restrictively with any self-fetched (server) and
27232731
* device-managed (MDM) layers: `deny`/`ask` rules are unioned, every
2724-
* declared `allow` list must admit an operation, and
2725-
* `disableBypassPermissionsMode: "disable"` is deny-wins.
2732+
* declared `allow` list must admit an operation, and bypass-mode
2733+
* restrictions are composed fail-closed.
27262734
*
27272735
* This is startup-only. It is **not** persisted: it must be re-supplied on
27282736
* {@link CopilotClient.resumeSession | resume}, where it replaces the prior

nodejs/test/client.test.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {
1010
createAttributedPermissionResult,
1111
CopilotClient,
1212
createCanvas,
13+
DisableBypassPermissionsModes,
1314
RuntimeConnection,
1415
type GitHubTelemetryNotification,
16+
type ManagedSettings,
1517
type ModelInfo,
1618
} from "../src/index.js";
1719
import { CopilotSession } from "../src/session.js";
@@ -3905,26 +3907,38 @@ describe("managedSettings serialization", () => {
39053907
}
39063908

39073909
it("forwards the full permissions object on session.create", async () => {
3908-
const params = await captureCreateParams({
3909-
managedSettings: {
3910-
permissions: {
3911-
disableBypassPermissionsMode: "disable",
3912-
deny: ["Shell(git push)"],
3913-
ask: ["Domain(publish.example)"],
3914-
allow: ["Read(**)"],
3915-
},
3910+
const managedSettings = {
3911+
permissions: {
3912+
disableBypassPermissionsMode: DisableBypassPermissionsModes.AllowAutoOnly,
3913+
deny: ["Shell(git push)"],
3914+
ask: ["Domain(publish.example)"],
3915+
allow: ["Read(**)"],
39163916
},
3917+
} satisfies ManagedSettings;
3918+
const params = await captureCreateParams({
3919+
managedSettings,
39173920
});
39183921
expect(params.managedSettings).toEqual({
39193922
permissions: {
3920-
disableBypassPermissionsMode: "disable",
3923+
disableBypassPermissionsMode: "allow-auto-only",
39213924
deny: ["Shell(git push)"],
39223925
ask: ["Domain(publish.example)"],
39233926
allow: ["Read(**)"],
39243927
},
39253928
});
39263929
});
39273930

3931+
it("forwards unknown bypass-permissions modes", async () => {
3932+
const managedSettings = {
3933+
permissions: {
3934+
disableBypassPermissionsMode: "future-fail-closed-mode",
3935+
},
3936+
} satisfies ManagedSettings;
3937+
const params = await captureCreateParams({ managedSettings });
3938+
3939+
expect(params.managedSettings).toEqual(managedSettings);
3940+
});
3941+
39283942
it("marks directly injected sessions as managed", async () => {
39293943
const client = new CopilotClient();
39303944
await client.start();

rust/src/types.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,6 +1767,9 @@ pub struct CopilotExpAssignmentResponse {
17671767
pub enum DisableBypassPermissionsMode {
17681768
/// Turn off bypass-permissions mode.
17691769
Disable,
1770+
/// Permit automatic bypass but block full allow-all.
1771+
#[serde(rename = "allow-auto-only")]
1772+
AllowAutoOnly,
17701773
}
17711774

17721775
/// Permission rules injected as a managed-settings layer at session bootstrap.
@@ -1775,15 +1778,13 @@ pub enum DisableBypassPermissionsMode {
17751778
/// layer. This layer composes restrictively with any server- or device-level
17761779
/// managed settings: [`deny`](Self::deny) and [`ask`](Self::ask) rules are
17771780
/// unioned across layers, every present [`allow`](Self::allow) list must admit a
1778-
/// tool for it to be allowed, and
1779-
/// [`disable_bypass_permissions_mode`](Self::disable_bypass_permissions_mode) is
1780-
/// honored if any layer sets it (deny-wins).
1781+
/// tool for it to be allowed, and bypass-mode restrictions compose to the most
1782+
/// restrictive setting.
17811783
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
17821784
#[serde(rename_all = "camelCase")]
17831785
#[non_exhaustive]
17841786
pub struct ManagedSettingsPermissions {
1785-
/// When set to `"disable"`, bypass-permissions mode is turned off for the
1786-
/// session regardless of other layers. Serialized as
1787+
/// Restricts bypass-permissions mode for the session. Serialized as
17871788
/// `disableBypassPermissionsMode`.
17881789
#[serde(default, skip_serializing_if = "Option::is_none")]
17891790
pub disable_bypass_permissions_mode: Option<DisableBypassPermissionsMode>,

rust/tests/session_test.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,18 @@ async fn create_session_sends_canvas_wire_fields() {
788788
timeout(TIMEOUT, create_handle).await.unwrap().unwrap();
789789
}
790790

791+
#[test]
792+
fn managed_bypass_permissions_modes_use_wire_values() {
793+
assert_eq!(
794+
serde_json::to_value(DisableBypassPermissionsMode::Disable).unwrap(),
795+
serde_json::json!("disable")
796+
);
797+
assert_eq!(
798+
serde_json::to_value(DisableBypassPermissionsMode::AllowAutoOnly).unwrap(),
799+
serde_json::json!("allow-auto-only")
800+
);
801+
}
802+
791803
#[tokio::test]
792804
async fn create_and_resume_send_managed_settings_permissions() {
793805
use github_copilot_sdk::types::ResumeSessionConfig;
@@ -796,7 +808,7 @@ async fn create_and_resume_send_managed_settings_permissions() {
796808

797809
let managed = ManagedSettings::default().with_permissions(
798810
ManagedSettingsPermissions::default()
799-
.with_disable_bypass_permissions_mode(DisableBypassPermissionsMode::Disable)
811+
.with_disable_bypass_permissions_mode(DisableBypassPermissionsMode::AllowAutoOnly)
800812
.with_deny(vec!["shell(rm*)".to_string()])
801813
.with_ask(vec!["write".to_string()])
802814
.with_allow(vec![]),
@@ -821,7 +833,7 @@ async fn create_and_resume_send_managed_settings_permissions() {
821833
assert_eq!(request["method"], "session.create");
822834
assert_eq!(request["params"]["enableManagedSettings"], true);
823835
let perms = &request["params"]["managedSettings"]["permissions"];
824-
assert_eq!(perms["disableBypassPermissionsMode"], "disable");
836+
assert_eq!(perms["disableBypassPermissionsMode"], "allow-auto-only");
825837
assert_eq!(perms["deny"][0], "shell(rm*)");
826838
assert_eq!(perms["ask"][0], "write");
827839
assert_eq!(perms["allow"], serde_json::json!([]));

0 commit comments

Comments
 (0)