1package permission
2
3import (
4 "context"
5 "errors"
6 "path/filepath"
7 "slices"
8 "sync"
9
10 "github.com/charmbracelet/crush/internal/csync"
11 "github.com/charmbracelet/crush/internal/pubsub"
12 "github.com/google/uuid"
13)
14
15var ErrorPermissionDenied = errors.New("permission denied")
16
17type CreatePermissionRequest struct {
18 SessionID string `json:"session_id"`
19 ToolCallID string `json:"tool_call_id"`
20 ToolName string `json:"tool_name"`
21 Description string `json:"description"`
22 Action string `json:"action"`
23 Params any `json:"params"`
24 Path string `json:"path"`
25}
26
27type PermissionNotification struct {
28 ToolCallID string `json:"tool_call_id"`
29 Granted bool `json:"granted"`
30 Denied bool `json:"denied"`
31}
32
33type PermissionRequest struct {
34 ID string `json:"id"`
35 SessionID string `json:"session_id"`
36 ToolCallID string `json:"tool_call_id"`
37 ToolName string `json:"tool_name"`
38 Description string `json:"description"`
39 Action string `json:"action"`
40 Params any `json:"params"`
41 Path string `json:"path"`
42}
43
44type Service interface {
45 pubsub.Suscriber[PermissionRequest]
46 GrantPersistent(permission PermissionRequest)
47 Grant(permission PermissionRequest)
48 Deny(permission PermissionRequest)
49 Request(opts CreatePermissionRequest) bool
50 AutoApproveSession(sessionID string)
51 SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[PermissionNotification]
52}
53
54type permissionService struct {
55 *pubsub.Broker[PermissionRequest]
56
57 notificationBroker *pubsub.Broker[PermissionNotification]
58 workingDir string
59 sessionPermissions []PermissionRequest
60 sessionPermissionsMu sync.RWMutex
61 pendingRequests *csync.Map[string, chan bool]
62 autoApproveSessions map[string]bool
63 autoApproveSessionsMu sync.RWMutex
64 skip bool
65 allowedTools []string
66
67 // used to make sure we only process one request at a time
68 requestMu sync.Mutex
69 activeRequest *PermissionRequest
70}
71
72func (s *permissionService) GrantPersistent(permission PermissionRequest) {
73 s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
74 ToolCallID: permission.ToolCallID,
75 Granted: true,
76 })
77 respCh, ok := s.pendingRequests.Get(permission.ID)
78 if ok {
79 respCh <- true
80 }
81
82 s.sessionPermissionsMu.Lock()
83 s.sessionPermissions = append(s.sessionPermissions, permission)
84 s.sessionPermissionsMu.Unlock()
85
86 if s.activeRequest != nil && s.activeRequest.ID == permission.ID {
87 s.activeRequest = nil
88 }
89}
90
91func (s *permissionService) Grant(permission PermissionRequest) {
92 s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
93 ToolCallID: permission.ToolCallID,
94 Granted: true,
95 })
96 respCh, ok := s.pendingRequests.Get(permission.ID)
97 if ok {
98 respCh <- true
99 }
100
101 if s.activeRequest != nil && s.activeRequest.ID == permission.ID {
102 s.activeRequest = nil
103 }
104}
105
106func (s *permissionService) Deny(permission PermissionRequest) {
107 s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
108 ToolCallID: permission.ToolCallID,
109 Granted: false,
110 Denied: true,
111 })
112 respCh, ok := s.pendingRequests.Get(permission.ID)
113 if ok {
114 respCh <- false
115 }
116
117 if s.activeRequest != nil && s.activeRequest.ID == permission.ID {
118 s.activeRequest = nil
119 }
120}
121
122func (s *permissionService) Request(opts CreatePermissionRequest) bool {
123 if s.skip {
124 return true
125 }
126
127 // tell the UI that a permission was requested
128 s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
129 ToolCallID: opts.ToolCallID,
130 })
131 s.requestMu.Lock()
132 defer s.requestMu.Unlock()
133
134 // Check if the tool/action combination is in the allowlist
135 commandKey := opts.ToolName + ":" + opts.Action
136 if slices.Contains(s.allowedTools, commandKey) || slices.Contains(s.allowedTools, opts.ToolName) {
137 return true
138 }
139
140 s.autoApproveSessionsMu.RLock()
141 autoApprove := s.autoApproveSessions[opts.SessionID]
142 s.autoApproveSessionsMu.RUnlock()
143
144 if autoApprove {
145 return true
146 }
147
148 dir := filepath.Dir(opts.Path)
149 if dir == "." {
150 dir = s.workingDir
151 }
152 permission := PermissionRequest{
153 ID: uuid.New().String(),
154 Path: dir,
155 SessionID: opts.SessionID,
156 ToolCallID: opts.ToolCallID,
157 ToolName: opts.ToolName,
158 Description: opts.Description,
159 Action: opts.Action,
160 Params: opts.Params,
161 }
162
163 s.sessionPermissionsMu.RLock()
164 for _, p := range s.sessionPermissions {
165 if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
166 s.sessionPermissionsMu.RUnlock()
167 return true
168 }
169 }
170 s.sessionPermissionsMu.RUnlock()
171
172 s.sessionPermissionsMu.RLock()
173 for _, p := range s.sessionPermissions {
174 if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
175 s.sessionPermissionsMu.RUnlock()
176 return true
177 }
178 }
179 s.sessionPermissionsMu.RUnlock()
180
181 s.activeRequest = &permission
182
183 respCh := make(chan bool, 1)
184 s.pendingRequests.Set(permission.ID, respCh)
185 defer s.pendingRequests.Del(permission.ID)
186
187 // Publish the request
188 s.Publish(pubsub.CreatedEvent, permission)
189
190 return <-respCh
191}
192
193func (s *permissionService) AutoApproveSession(sessionID string) {
194 s.autoApproveSessionsMu.Lock()
195 s.autoApproveSessions[sessionID] = true
196 s.autoApproveSessionsMu.Unlock()
197}
198
199func (s *permissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[PermissionNotification] {
200 return s.notificationBroker.Subscribe(ctx)
201}
202
203func NewPermissionService(workingDir string, skip bool, allowedTools []string) Service {
204 return &permissionService{
205 Broker: pubsub.NewBroker[PermissionRequest](),
206 notificationBroker: pubsub.NewBroker[PermissionNotification](),
207 workingDir: workingDir,
208 sessionPermissions: make([]PermissionRequest, 0),
209 autoApproveSessions: make(map[string]bool),
210 skip: skip,
211 allowedTools: allowedTools,
212 pendingRequests: csync.NewMap[string, chan bool](),
213 }
214}