Skip to content

Commit 9ca4554

Browse files
fix: move requestPermission outside config nil-guard in Go; add resume deny-by-default e2e tests
- Go CreateSession/ResumeSession: requestPermission=true was inside the 'if config != nil' block, so nil config skipped it entirely. Move it unconditionally after the block (matching Node.js/.NET/Python). - Add 'deny by default after resume' e2e test to all 4 languages, verifying that resumeSession with no handler also denies tool calls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6091fd7 commit 9ca4554

File tree

6 files changed

+202
-3
lines changed

6 files changed

+202
-3
lines changed

dotnet/test/PermissionTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,37 @@ await session.SendAsync(new MessageOptions
185185
Assert.Matches("fail|cannot|unable|permission", message?.Data.Content?.ToLowerInvariant() ?? string.Empty);
186186
}
187187

188+
[Fact]
189+
public async Task Should_Deny_Tool_Operations_By_Default_When_No_Handler_Is_Provided_After_Resume()
190+
{
191+
var session1 = await Client.CreateSessionAsync(new SessionConfig
192+
{
193+
OnPermissionRequest = PermissionHandler.ApproveAll
194+
});
195+
var sessionId = session1.SessionId;
196+
await session1.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" });
197+
198+
var session2 = await Client.ResumeSessionAsync(sessionId);
199+
var permissionDenied = false;
200+
201+
session2.On(evt =>
202+
{
203+
if (evt is ToolExecutionCompleteEvent toolEvt &&
204+
!toolEvt.Data.Success &&
205+
toolEvt.Data.Error?.Message.Contains("Permission denied") == true)
206+
{
207+
permissionDenied = true;
208+
}
209+
});
210+
211+
await session2.SendAndWaitAsync(new MessageOptions
212+
{
213+
Prompt = "Run 'node --version'"
214+
});
215+
216+
Assert.True(permissionDenied, "Expected a tool.execution_complete event with Permission denied result");
217+
}
218+
188219
[Fact]
189220
public async Task Should_Receive_ToolCallId_In_Permission_Requests()
190221
{

go/client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,6 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
473473
if config.Streaming {
474474
req.Streaming = Bool(true)
475475
}
476-
req.RequestPermission = Bool(true)
477476
if config.OnUserInputRequest != nil {
478477
req.RequestUserInput = Bool(true)
479478
}
@@ -486,6 +485,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
486485
req.Hooks = Bool(true)
487486
}
488487
}
488+
req.RequestPermission = Bool(true)
489489

490490
result, err := c.client.Request("session.create", req)
491491
if err != nil {
@@ -560,7 +560,6 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
560560
if config.Streaming {
561561
req.Streaming = Bool(true)
562562
}
563-
req.RequestPermission = Bool(true)
564563
if config.OnUserInputRequest != nil {
565564
req.RequestUserInput = Bool(true)
566565
}
@@ -584,6 +583,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
584583
req.DisabledSkills = config.DisabledSkills
585584
req.InfiniteSessions = config.InfiniteSessions
586585
}
586+
req.RequestPermission = Bool(true)
587587

588588
result, err := c.client.Request("session.resume", req)
589589
if err != nil {

go/internal/e2e/permissions_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,52 @@ func TestPermissions(t *testing.T) {
192192
}
193193
})
194194

195+
t.Run("should deny tool operations by default when no handler is provided after resume", func(t *testing.T) {
196+
ctx.ConfigureForTest(t)
197+
198+
session1, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
199+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
200+
})
201+
if err != nil {
202+
t.Fatalf("Failed to create session: %v", err)
203+
}
204+
sessionID := session1.SessionID
205+
if _, err = session1.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "What is 1+1?"}); err != nil {
206+
t.Fatalf("Failed to send message: %v", err)
207+
}
208+
209+
session2, err := client.ResumeSession(t.Context(), sessionID)
210+
if err != nil {
211+
t.Fatalf("Failed to resume session: %v", err)
212+
}
213+
214+
var mu sync.Mutex
215+
permissionDenied := false
216+
217+
session2.On(func(event copilot.SessionEvent) {
218+
if event.Type == copilot.ToolExecutionComplete &&
219+
event.Data.Success != nil && !*event.Data.Success &&
220+
event.Data.Error != nil && event.Data.Error.ErrorClass != nil &&
221+
strings.Contains(event.Data.Error.ErrorClass.Message, "Permission denied") {
222+
mu.Lock()
223+
permissionDenied = true
224+
mu.Unlock()
225+
}
226+
})
227+
228+
if _, err = session2.SendAndWait(t.Context(), copilot.MessageOptions{
229+
Prompt: "Run 'node --version'",
230+
}); err != nil {
231+
t.Fatalf("Failed to send message: %v", err)
232+
}
233+
234+
mu.Lock()
235+
defer mu.Unlock()
236+
if !permissionDenied {
237+
t.Error("Expected a tool.execution_complete event with Permission denied result")
238+
}
239+
})
240+
195241
t.Run("without permission handler", func(t *testing.T) {
196242
ctx.ConfigureForTest(t)
197243

nodejs/test/e2e/permissions.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { readFile, writeFile } from "fs/promises";
66
import { join } from "path";
77
import { describe, expect, it } from "vitest";
88
import type { PermissionRequest, PermissionRequestResult } from "../../src/index.js";
9+
import { approveAll } from "../../src/index.js";
910
import { createSdkTestContext } from "./harness/sdkTestContext.js";
1011

1112
describe("Permission callbacks", async () => {
@@ -84,6 +85,30 @@ describe("Permission callbacks", async () => {
8485
await session.destroy();
8586
});
8687

88+
it("should deny tool operations by default when no handler is provided after resume", async () => {
89+
const session1 = await client.createSession({ onPermissionRequest: approveAll });
90+
const sessionId = session1.sessionId;
91+
await session1.sendAndWait({ prompt: "What is 1+1?" });
92+
93+
const session2 = await client.resumeSession(sessionId);
94+
let permissionDenied = false;
95+
session2.on((event) => {
96+
if (
97+
event.type === "tool.execution_complete" &&
98+
!event.data.success &&
99+
event.data.error?.message.includes("Permission denied")
100+
) {
101+
permissionDenied = true;
102+
}
103+
});
104+
105+
await session2.sendAndWait({ prompt: "Run 'node --version'" });
106+
107+
expect(permissionDenied).toBe(true);
108+
109+
await session2.destroy();
110+
});
111+
87112
it("should work without permission handler (default behavior)", async () => {
88113
// Create session without onPermissionRequest handler
89114
const session = await client.createSession();

python/e2e/test_permissions.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ async def test_should_deny_tool_operations_by_default_when_no_handler_is_provide
8181
def on_event(event):
8282
if event.type.value == "tool.execution_complete" and event.data.success is False:
8383
error = event.data.error
84-
msg = error if isinstance(error, str) else (getattr(error, "message", None) if error is not None else None)
84+
msg = (
85+
error
86+
if isinstance(error, str)
87+
else (getattr(error, "message", None) if error is not None else None)
88+
)
8589
if msg and "Permission denied" in msg:
8690
denied_events.append(event)
8791
elif event.type.value == "session.idle":
@@ -96,6 +100,44 @@ def on_event(event):
96100

97101
await session.destroy()
98102

103+
async def test_should_deny_tool_operations_by_default_when_no_handler_is_provided_after_resume(
104+
self, ctx: E2ETestContext
105+
):
106+
import asyncio
107+
108+
session1 = await ctx.client.create_session(
109+
{"on_permission_request": PermissionHandler.approve_all}
110+
)
111+
session_id = session1.session_id
112+
await session1.send_and_wait({"prompt": "What is 1+1?"})
113+
114+
session2 = await ctx.client.resume_session(session_id)
115+
116+
denied_events = []
117+
done_event = asyncio.Event()
118+
119+
def on_event(event):
120+
if event.type.value == "tool.execution_complete" and event.data.success is False:
121+
error = event.data.error
122+
msg = (
123+
error
124+
if isinstance(error, str)
125+
else (getattr(error, "message", None) if error is not None else None)
126+
)
127+
if msg and "Permission denied" in msg:
128+
denied_events.append(event)
129+
elif event.type.value == "session.idle":
130+
done_event.set()
131+
132+
session2.on(on_event)
133+
134+
await session2.send({"prompt": "Run 'node --version'"})
135+
await asyncio.wait_for(done_event.wait(), timeout=60)
136+
137+
assert len(denied_events) > 0
138+
139+
await session2.destroy()
140+
99141
async def test_should_work_without_permission_handler__default_behavior_(
100142
self, ctx: E2ETestContext
101143
):
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
models:
2+
- claude-sonnet-4.5
3+
conversations:
4+
- messages:
5+
- role: system
6+
content: ${system}
7+
- role: user
8+
content: What is 1+1?
9+
- role: assistant
10+
content: 1+1 = 2
11+
- role: user
12+
content: Run 'node --version'
13+
- role: assistant
14+
tool_calls:
15+
- id: toolcall_0
16+
type: function
17+
function:
18+
name: report_intent
19+
arguments: '{"intent":"Checking Node.js version"}'
20+
- role: assistant
21+
tool_calls:
22+
- id: toolcall_1
23+
type: function
24+
function:
25+
name: ${shell}
26+
arguments: '{"command":"node --version","description":"Check Node.js version"}'
27+
- messages:
28+
- role: system
29+
content: ${system}
30+
- role: user
31+
content: What is 1+1?
32+
- role: assistant
33+
content: 1+1 = 2
34+
- role: user
35+
content: Run 'node --version'
36+
- role: assistant
37+
tool_calls:
38+
- id: toolcall_0
39+
type: function
40+
function:
41+
name: report_intent
42+
arguments: '{"intent":"Checking Node.js version"}'
43+
- id: toolcall_1
44+
type: function
45+
function:
46+
name: ${shell}
47+
arguments: '{"command":"node --version","description":"Check Node.js version"}'
48+
- role: tool
49+
tool_call_id: toolcall_0
50+
content: Intent logged
51+
- role: tool
52+
tool_call_id: toolcall_1
53+
content: Permission denied and could not request permission from user
54+
- role: assistant
55+
content: Permission was denied to run the command. I don't have access to execute shell commands in this environment.

0 commit comments

Comments
 (0)