@@ -79,12 +79,89 @@ func TestBashTool_CustomAutoBackgroundThreshold(t *testing.T) {
7979 require.NoError(t, bgManager.Kill(meta.ShellID))
8080}
8181
82+type recordingPermissionService struct {
83+ *pubsub.Broker[permission.PermissionRequest]
84+ requestCount int
85+ allow bool
86+}
87+
88+func (m *recordingPermissionService) Request(ctx context.Context, req permission.CreatePermissionRequest) (bool, error) {
89+ m.requestCount++
90+ return m.allow, nil
91+}
92+
93+func (m *recordingPermissionService) Grant(req permission.PermissionRequest) {}
94+
95+func (m *recordingPermissionService) Deny(req permission.PermissionRequest) {}
96+
97+func (m *recordingPermissionService) GrantPersistent(req permission.PermissionRequest) {}
98+
99+func (m *recordingPermissionService) AutoApproveSession(sessionID string) {}
100+
101+func (m *recordingPermissionService) SetSkipRequests(skip bool) {}
102+
103+func (m *recordingPermissionService) SkipRequests() bool {
104+ return false
105+}
106+
107+func (m *recordingPermissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[permission.PermissionNotification] {
108+ return make(<-chan pubsub.Event[permission.PermissionNotification])
109+}
110+
82111func newBashToolForTest(workingDir string) fantasy.AgentTool {
83112 permissions := &mockBashPermissionService{Broker: pubsub.NewBroker[permission.PermissionRequest]()}
84113 attribution := &config.Attribution{TrailerStyle: config.TrailerStyleNone}
85114 return NewBashTool(permissions, workingDir, attribution, "test-model")
86115}
87116
117+func newBashToolWithRecordingPerms(workingDir string, allow bool) (fantasy.AgentTool, *recordingPermissionService) {
118+ perms := &recordingPermissionService{
119+ Broker: pubsub.NewBroker[permission.PermissionRequest](),
120+ allow: allow,
121+ }
122+ attribution := &config.Attribution{TrailerStyle: config.TrailerStyleNone}
123+ return NewBashTool(perms, workingDir, attribution, "test-model"), perms
124+}
125+
126+func TestBashTool_ChainedCommandsRequirePermission(t *testing.T) {
127+ workingDir := t.TempDir()
128+ tool, perms := newBashToolWithRecordingPerms(workingDir, true)
129+ ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
130+
131+ // ls && echo should trigger permission check.
132+ resp := runBashTool(t, tool, ctx, BashParams{
133+ Description: "chained ls",
134+ Command: "ls && echo done",
135+ })
136+
137+ require.False(t, resp.IsError)
138+ require.Equal(t, 1, perms.requestCount, "chained command should trigger permission request")
139+
140+ // Plain ls should NOT trigger permission check.
141+ perms.requestCount = 0
142+ resp = runBashTool(t, tool, ctx, BashParams{
143+ Description: "plain ls",
144+ Command: "ls -la",
145+ })
146+
147+ require.False(t, resp.IsError)
148+ require.Equal(t, 0, perms.requestCount, "plain ls should not trigger permission request")
149+}
150+
151+func TestBashTool_ChainedCommandsDenied(t *testing.T) {
152+ workingDir := t.TempDir()
153+ tool, perms := newBashToolWithRecordingPerms(workingDir, false)
154+ ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
155+
156+ resp := runBashTool(t, tool, ctx, BashParams{
157+ Description: "chained ls denied",
158+ Command: "ls && rm -rf /",
159+ })
160+
161+ require.Equal(t, 1, perms.requestCount)
162+ require.Contains(t, resp.Content, "User denied permission")
163+}
164+
88165func runBashTool(t *testing.T, tool fantasy.AgentTool, ctx context.Context, params BashParams) fantasy.ToolResponse {
89166 t.Helper()
90167