Skip to content

Commit ab55453

Browse files
ellismgCopilot
andcommitted
Use authoritative sandbox bypass schema
Remove the temporary schema augmentation now that CLI 1.0.83-4 ships allowBypass, update serialization coverage for the current generated shapes, and add a real-runtime bypass E2E test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 9fc5d7d commit ab55453

7 files changed

Lines changed: 116 additions & 48 deletions

File tree

‎java/sdk/src/test/java/com/github/copilot/generated/rpc/SandboxConfigSerializationTest.java‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,5 @@ void allowBypassRoundTripsAndIsOmittedWhenAbsent() throws Exception {
2929
""", SandboxConfig.class);
3030
var omittedJson = MAPPER.readTree(MAPPER.writeValueAsString(omitted));
3131
assertTrue(omittedJson.path("allowBypass").isMissingNode());
32-
33-
var legacyConstructor = new SandboxConfig(true, null, null, null, null);
34-
assertNull(legacyConstructor.allowBypass());
3532
}
3633
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
import { mkdir, writeFile } from "fs/promises";
6+
import { join } from "path";
7+
import { describe, expect, it } from "vitest";
8+
import type { PermissionRequest } from "../../src/index.js";
9+
import { createSdkTestContext } from "./harness/sdkTestContext.js";
10+
11+
const SEND_TIMEOUT_MS = 120_000;
12+
const TEST_TIMEOUT_MS = 180_000;
13+
14+
describe("Sandbox bypass", () => {
15+
// The Windows backend requires BaseContainer, which is unavailable on the SDK's Windows runners.
16+
it.skipIf(process.platform === "win32")(
17+
"approves a blocked search and executes it outside the sandbox",
18+
async () => {
19+
const { copilotClient: client, workDir } = await createSdkTestContext({
20+
copilotClientOptions: {
21+
env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "SANDBOX" },
22+
},
23+
});
24+
const vaultDir = join(workDir, "vault");
25+
await mkdir(vaultDir, { recursive: true });
26+
await writeFile(join(vaultDir, "notes.txt"), "OUTSIDE_MATCH_LINE bypass-approved\n");
27+
28+
const permissionRequests: PermissionRequest[] = [];
29+
let bypassedSearchCompleted = false;
30+
const session = await client.createSession({
31+
onPermissionRequest: (request) => {
32+
permissionRequests.push(request);
33+
return { kind: "approve-once" };
34+
},
35+
});
36+
const update = await session.rpc.options.update({
37+
sandboxConfig: {
38+
enabled: true,
39+
allowBypass: true,
40+
addCurrentWorkingDirectory: true,
41+
userPolicy: { filesystem: { deniedPaths: [vaultDir] } },
42+
},
43+
});
44+
expect(update.success).toBe(true);
45+
session.on((event) => {
46+
if (
47+
event.type === "tool.execution_complete" &&
48+
event.data.toolName === "grep" &&
49+
event.data.success &&
50+
event.data.sandboxed === false
51+
) {
52+
bypassedSearchCompleted = true;
53+
}
54+
});
55+
56+
const message = await session.sendAndWait(
57+
{
58+
prompt:
59+
"Search for OUTSIDE_MATCH_LINE in the vault directory. " +
60+
"After the search succeeds, reply with exactly SANDBOX_BYPASS_APPROVED.",
61+
},
62+
SEND_TIMEOUT_MS
63+
);
64+
65+
expect(message?.data.content).toContain("SANDBOX_BYPASS_APPROVED");
66+
expect(
67+
permissionRequests.some(
68+
(request) =>
69+
"requestSandboxBypass" in request && request.requestSandboxBypass === true
70+
)
71+
).toBe(true);
72+
expect(bypassedSearchCompleted).toBe(true);
73+
74+
await session.disconnect();
75+
},
76+
TEST_TIMEOUT_MS
77+
);
78+
});

‎python/test_rpc_generated.py‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ def test_sandbox_config_round_trips_allow_bypass_and_omits_when_absent():
3131
assert SandboxConfig.from_dict(configured.to_dict()).allow_bypass is True
3232
assert SandboxConfig(enabled=True).to_dict() == {"enabled": True}
3333

34-
legacy_positional = SandboxConfig(True, None, True)
35-
assert legacy_positional.to_dict() == {"enabled": True, "allowDevToolAccess": True}
36-
3734

3835
@pytest.mark.asyncio
3936
async def test_commands_invoke_deserializes_slash_command_result():

‎rust/tests/api_types_test.rs‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ fn permission_event_exposes_managed_approval_required() {
148148
}
149149

150150
#[test]
151-
<<<<<<< HEAD
152151
fn queue_pending_message_id_uses_camel_case_wire_name() {
153152
let item = QueuePendingItems {
154153
agent_mode: SendAgentMode::Interactive,
@@ -187,11 +186,9 @@ fn queue_pending_message_id_is_optional_for_older_hosts() {
187186

188187
#[test]
189188
fn sandbox_allow_bypass_round_trips_as_optional_camel_case() {
190-
let enabled = SandboxConfig {
191-
enabled: true,
192-
allow_bypass: Some(true),
193-
..Default::default()
194-
};
189+
let mut enabled = SandboxConfig::default();
190+
enabled.enabled = true;
191+
enabled.allow_bypass = Some(true);
195192
let value = serde_json::to_value(enabled).unwrap();
196193
assert_eq!(
197194
value,
@@ -203,11 +200,8 @@ fn sandbox_allow_bypass_round_trips_as_optional_camel_case() {
203200
let round_tripped: SandboxConfig = serde_json::from_value(value).unwrap();
204201
assert_eq!(round_tripped.allow_bypass, Some(true));
205202

206-
let omitted = SandboxConfig {
207-
enabled: true,
208-
allow_bypass: None,
209-
..Default::default()
210-
};
203+
let mut omitted = SandboxConfig::default();
204+
omitted.enabled = true;
211205
assert_eq!(
212206
serde_json::to_value(omitted).unwrap(),
213207
serde_json::json!({ "enabled": true })

‎scripts/codegen/rust.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { promisify } from "util";
1818
import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
1919
import {
2020
addManagedApprovalRequiredToPermissionRequests,
21-
addSandboxAllowBypass,
2221
type ApiSchema,
2322
type DefinitionCollections,
2423
EXCLUDED_EVENT_TYPES,
@@ -2233,9 +2232,7 @@ async function generate(): Promise<void> {
22332232
);
22342233
const apiSchema = propagateInternalVisibility(
22352234
postProcessSchema(
2236-
stripBooleanLiterals(
2237-
addSandboxAllowBypass(apiRaw as JSONSchema7),
2238-
) as JSONSchema7,
2235+
stripBooleanLiterals(apiRaw) as JSONSchema7,
22392236
),
22402237
) as unknown as ApiSchema;
22412238

‎scripts/codegen/utils.ts‎

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -484,33 +484,6 @@ export function addManagedApprovalRequiredToPermissionRequests<T extends JSONSch
484484
return cloned;
485485
}
486486

487-
/**
488-
* Add the sandbox bypass host capability until the pinned CLI schema includes the field.
489-
*/
490-
export function addSandboxAllowBypass<T extends JSONSchema7>(schema: T): T {
491-
const cloned = cloneSchemaForCodegen(schema);
492-
const property: JSONSchema7 = {
493-
description:
494-
"Host capability flag (not part of the sandbox policy): when set, exposes a per-command escape hatch so the model can request individual commands run outside the sandbox. Stripped from the effective spawn policy.",
495-
type: "boolean",
496-
};
497-
(property as Record<string, unknown>)["x-copilot-sdk-append-last"] = true;
498-
499-
for (const definitions of [cloned.definitions, cloned.$defs]) {
500-
if (!definitions) continue;
501-
const definition = definitions.SandboxConfig;
502-
if (!definition || typeof definition !== "object") continue;
503-
const objectDefinition = definition as JSONSchema7;
504-
objectDefinition.properties = {
505-
...objectDefinition.properties,
506-
allowBypass:
507-
objectDefinition.properties?.allowBypass ?? cloneSchemaForCodegen(property),
508-
};
509-
}
510-
511-
return cloned;
512-
}
513-
514487
export function getEnumValueDescriptions(schema: JSONSchema7 | null | undefined): EnumValueDescriptions | undefined {
515488
if (!schema || typeof schema !== "object") return undefined;
516489

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
models:
2+
- claude-sonnet-5
3+
conversations:
4+
- messages:
5+
- role: system
6+
content: ${system}
7+
- role: user
8+
content: Search for OUTSIDE_MATCH_LINE in the vault directory. After the search succeeds, reply with exactly SANDBOX_BYPASS_APPROVED.
9+
- role: assistant
10+
tool_calls:
11+
- id: toolcall_0
12+
type: function
13+
function:
14+
name: grep
15+
arguments: '{"pattern":"OUTSIDE_MATCH_LINE","path":"${workdir}/vault","output_mode":"content","-n":true}'
16+
- messages:
17+
- role: system
18+
content: ${system}
19+
- role: user
20+
content: Search for OUTSIDE_MATCH_LINE in the vault directory. After the search succeeds, reply with exactly SANDBOX_BYPASS_APPROVED.
21+
- role: assistant
22+
tool_calls:
23+
- id: toolcall_0
24+
type: function
25+
function:
26+
name: grep
27+
arguments: '{"pattern":"OUTSIDE_MATCH_LINE","path":"${workdir}/vault","output_mode":"content","-n":true}'
28+
- role: tool
29+
tool_call_id: toolcall_0
30+
content: '${workdir}/vault/notes.txt:1:OUTSIDE_MATCH_LINE bypass-approved'
31+
- role: assistant
32+
content: SANDBOX_BYPASS_APPROVED

0 commit comments

Comments
 (0)