@@ -79,12 +79,89 @@ func TestBashTool_CustomAutoBackgroundThreshold(t *testing.T) {
79 require.NoError(t, bgManager.Kill(meta.ShellID))
79 require.NoError(t, bgManager.Kill(meta.ShellID))
82+type recordingPermissionService struct {
83+ *pubsub.Broker[permission.PermissionRequest]
88+func (m *recordingPermissionService) Request(ctx context.Context, req permission.CreatePermissionRequest) (bool, error) {
93+func (m *recordingPermissionService) Grant(req permission.PermissionRequest) {}
95+func (m *recordingPermissionService) Deny(req permission.PermissionRequest) {}
97+func (m *recordingPermissionService) GrantPersistent(req permission.PermissionRequest) {}
99+func (m *recordingPermissionService) AutoApproveSession(sessionID string) {}
101+func (m *recordingPermissionService) SetSkipRequests(skip bool) {}
103+func (m *recordingPermissionService) SkipRequests() bool {
107+func (m *recordingPermissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[permission.PermissionNotification] {
108+ return make(<-chan pubsub.Event[permission.PermissionNotification])
82func newBashToolForTest(workingDir string) fantasy.AgentTool {
111func newBashToolForTest(workingDir string) fantasy.AgentTool {
83 permissions := &mockBashPermissionService{Broker: pubsub.NewBroker[permission.PermissionRequest]()}
112 permissions := &mockBashPermissionService{Broker: pubsub.NewBroker[permission.PermissionRequest]()}
84 attribution := &config.Attribution{TrailerStyle: config.TrailerStyleNone}
113 attribution := &config.Attribution{TrailerStyle: config.TrailerStyleNone}
85 return NewBashTool(permissions, workingDir, attribution, "test-model")
114 return NewBashTool(permissions, workingDir, attribution, "test-model")
117+func newBashToolWithRecordingPerms(workingDir string, allow bool) (fantasy.AgentTool, *recordingPermissionService) {
118+ perms := &recordingPermissionService{
119+ Broker: pubsub.NewBroker[permission.PermissionRequest](),
122+ attribution := &config.Attribution{TrailerStyle: config.TrailerStyleNone}
123+ return NewBashTool(perms, workingDir, attribution, "test-model"), perms
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")
131+ // ls && echo should trigger permission check.
132+ resp := runBashTool(t, tool, ctx, BashParams{
133+ Description: "chained ls",
134+ Command: "ls && echo done",
137+ require.False(t, resp.IsError)
138+ require.Equal(t, 1, perms.requestCount, "chained command should trigger permission request")
140+ // Plain ls should NOT trigger permission check.
141+ perms.requestCount = 0
142+ resp = runBashTool(t, tool, ctx, BashParams{
143+ Description: "plain ls",
147+ require.False(t, resp.IsError)
148+ require.Equal(t, 0, perms.requestCount, "plain ls should not trigger permission request")
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")
156+ resp := runBashTool(t, tool, ctx, BashParams{
157+ Description: "chained ls denied",
158+ Command: "ls && rm -rf /",
161+ require.Equal(t, 1, perms.requestCount)
162+ require.Contains(t, resp.Content, "User denied permission")
88func runBashTool(t *testing.T, tool fantasy.AgentTool, ctx context.Context, params BashParams) fantasy.ToolResponse {
165func runBashTool(t *testing.T, tool fantasy.AgentTool, ctx context.Context, params BashParams) fantasy.ToolResponse {
89 t.Helper()
166 t.Helper()