on main help sign in
add command whitelist
bc6fadcTai Groot
internal/app/app.go+5−1
@@ -60,12 +60,16 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
60 messages := message.NewService(q)
60 messages := message.NewService(q)
61 files := history.NewService(q, conn)
61 files := history.NewService(q, conn)
62 skipPermissionsRequests := cfg.Options != nil && cfg.Options.SkipPermissionsRequests
62 skipPermissionsRequests := cfg.Options != nil && cfg.Options.SkipPermissionsRequests
63+ allowedCommands := []string{}
64+ if cfg.Options != nil && cfg.Options.AllowedCommands != nil {
65+ allowedCommands = cfg.Options.AllowedCommands
66+ }
63
67
64 app := &App{
68 app := &App{
65 Sessions: sessions,
69 Sessions: sessions,
66 Messages: messages,
70 Messages: messages,
67 History: files,
71 History: files,
68− Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests),
72+ Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedCommands),
69 LSPClients: make(map[string]*lsp.Client),
73 LSPClients: make(map[string]*lsp.Client),
70
74
71 globalCtx: ctx,
75 globalCtx: ctx,
internal/config/config.go+3−2
@@ -126,8 +126,9 @@ type Options struct {
126 Debug bool `json:"debug,omitempty"`
126 Debug bool `json:"debug,omitempty"`
127 DebugLSP bool `json:"debug_lsp,omitempty"`
127 DebugLSP bool `json:"debug_lsp,omitempty"`
128 DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty"`
128 DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty"`
129− DataDirectory string `json:"data_directory,omitempty"` // Relative to the cwd
129+ DataDirectory string `json:"data_directory,omitempty"` // Relative to the cwd
130− SkipPermissionsRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode)
130+ SkipPermissionsRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode)
131+ AllowedCommands []string `json:"allowed_commands,omitempty"` // Commands that don't require permission prompts
131}
132}
132
133
133type MCPs map[string]MCPConfig
134type MCPs map[string]MCPConfig
internal/permission/permission.go+9−1
@@ -50,6 +50,7 @@ type permissionService struct {
50 autoApproveSessions []string
50 autoApproveSessions []string
51 autoApproveSessionsMu sync.RWMutex
51 autoApproveSessionsMu sync.RWMutex
52 skip bool
52 skip bool
53+ allowedCommands []string
53}
54}
54
55
55func (s *permissionService) GrantPersistent(permission PermissionRequest) {
56func (s *permissionService) GrantPersistent(permission PermissionRequest) {
@@ -82,6 +83,12 @@ func (s *permissionService) Request(opts CreatePermissionRequest) bool {
82 return true
83 return true
83 }
84 }
84
85
86+ // Check if the tool/action combination is in the allowlist
87+ commandKey := opts.ToolName + ":" + opts.Action
88+ if slices.Contains(s.allowedCommands, commandKey) || slices.Contains(s.allowedCommands, opts.ToolName) {
89+ return true
90+ }
91+
85 s.autoApproveSessionsMu.RLock()
92 s.autoApproveSessionsMu.RLock()
86 autoApprove := slices.Contains(s.autoApproveSessions, opts.SessionID)
93 autoApprove := slices.Contains(s.autoApproveSessions, opts.SessionID)
87 s.autoApproveSessionsMu.RUnlock()
94 s.autoApproveSessionsMu.RUnlock()
@@ -130,11 +137,12 @@ func (s *permissionService) AutoApproveSession(sessionID string) {
130 s.autoApproveSessionsMu.Unlock()
137 s.autoApproveSessionsMu.Unlock()
131}
138}
132
139
133−func NewPermissionService(workingDir string, skip bool) Service {
140+func NewPermissionService(workingDir string, skip bool, allowedCommands []string) Service {
134 return &permissionService{
141 return &permissionService{
135 Broker: pubsub.NewBroker[PermissionRequest](),
142 Broker: pubsub.NewBroker[PermissionRequest](),
136 workingDir: workingDir,
143 workingDir: workingDir,
137 sessionPermissions: make([]PermissionRequest, 0),
144 sessionPermissions: make([]PermissionRequest, 0),
138 skip: skip,
145 skip: skip,
146+ allowedCommands: allowedCommands,
139 }
147 }
140}
148}
internal/permission/permission_test.gonew file+92
@@ -0,0 +1,92 @@
1+package permission
2+
3+import (
4+ "testing"
5+)
6+
7+func TestPermissionService_AllowedCommands(t *testing.T) {
8+ tests := []struct {
9+ name string
10+ allowedCommands []string
11+ toolName string
12+ action string
13+ expected bool
14+ }{
15+ {
16+ name: "tool in allowlist",
17+ allowedCommands: []string{"bash", "view"},
18+ toolName: "bash",
19+ action: "execute",
20+ expected: true,
21+ },
22+ {
23+ name: "tool:action in allowlist",
24+ allowedCommands: []string{"bash:execute", "edit:create"},
25+ toolName: "bash",
26+ action: "execute",
27+ expected: true,
28+ },
29+ {
30+ name: "tool not in allowlist",
31+ allowedCommands: []string{"view", "ls"},
32+ toolName: "bash",
33+ action: "execute",
34+ expected: false,
35+ },
36+ {
37+ name: "tool:action not in allowlist",
38+ allowedCommands: []string{"bash:read", "edit:create"},
39+ toolName: "bash",
40+ action: "execute",
41+ expected: false,
42+ },
43+ {
44+ name: "empty allowlist",
45+ allowedCommands: []string{},
46+ toolName: "bash",
47+ action: "execute",
48+ expected: false,
49+ },
50+ }
51+
52+ for _, tt := range tests {
53+ t.Run(tt.name, func(t *testing.T) {
54+ service := NewPermissionService("/tmp", false, tt.allowedCommands)
55+
56+ // Create a channel to capture the permission request
57+ // Since we're testing the allowlist logic, we need to simulate the request
58+ ps := service.(*permissionService)
59+
60+ // Test the allowlist logic directly
61+ commandKey := tt.toolName + ":" + tt.action
62+ allowed := false
63+ for _, cmd := range ps.allowedCommands {
64+ if cmd == commandKey || cmd == tt.toolName {
65+ allowed = true
66+ break
67+ }
68+ }
69+
70+ if allowed != tt.expected {
71+ t.Errorf("expected %v, got %v for tool %s action %s with allowlist %v",
72+ tt.expected, allowed, tt.toolName, tt.action, tt.allowedCommands)
73+ }
74+ })
75+ }
76+}
77+
78+func TestPermissionService_SkipMode(t *testing.T) {
79+ service := NewPermissionService("/tmp", true, []string{})
80+
81+ result := service.Request(CreatePermissionRequest{
82+ SessionID: "test-session",
83+ ToolName: "bash",
84+ Action: "execute",
85+ Description: "test command",
86+ Path: "/tmp",
87+ })
88+
89+ if !result {
90+ t.Error("expected permission to be granted in skip mode")
91+ }
92+}