1package permission
2
3import (
4 "context"
5 "errors"
6 "log/slog"
7 "path/filepath"
8 "slices"
9 "sync"
10
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 sync.Map
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.Load(permission.ID)
78 if ok {
79 respCh.(chan bool) <- 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.Load(permission.ID)
97 if ok {
98 respCh.(chan bool) <- 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.Load(permission.ID)
113 if ok {
114 respCh.(chan bool) <- 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 slog.Info("Requesting permission", "session_id", opts.SessionID, "tool_name", opts.ToolName, "action", opts.Action, "path", dir)
153 permission := PermissionRequest{
154 ID: uuid.New().String(),
155 Path: dir,
156 SessionID: opts.SessionID,
157 ToolCallID: opts.ToolCallID,
158 ToolName: opts.ToolName,
159 Description: opts.Description,
160 Action: opts.Action,
161 Params: opts.Params,
162 }
163
164 s.sessionPermissionsMu.RLock()
165 for _, p := range s.sessionPermissions {
166 if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
167 s.sessionPermissionsMu.RUnlock()
168 return true
169 }
170 }
171 s.sessionPermissionsMu.RUnlock()
172
173 s.sessionPermissionsMu.RLock()
174 for _, p := range s.sessionPermissions {
175 if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
176 s.sessionPermissionsMu.RUnlock()
177 return true
178 }
179 }
180 s.sessionPermissionsMu.RUnlock()
181
182 s.activeRequest = &permission
183
184 respCh := make(chan bool, 1)
185 s.pendingRequests.Store(permission.ID, respCh)
186 defer s.pendingRequests.Delete(permission.ID)
187
188 // Publish the request
189 s.Publish(pubsub.CreatedEvent, permission)
190
191 return <-respCh
192}
193
194func (s *permissionService) AutoApproveSession(sessionID string) {
195 s.autoApproveSessionsMu.Lock()
196 s.autoApproveSessions[sessionID] = true
197 s.autoApproveSessionsMu.Unlock()
198}
199
200func (s *permissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[PermissionNotification] {
201 return s.notificationBroker.Subscribe(ctx)
202}
203
204func NewPermissionService(workingDir string, skip bool, allowedTools []string) Service {
205 return &permissionService{
206 Broker: pubsub.NewBroker[PermissionRequest](),
207 notificationBroker: pubsub.NewBroker[PermissionNotification](),
208 workingDir: workingDir,
209 sessionPermissions: make([]PermissionRequest, 0),
210 autoApproveSessions: make(map[string]bool),
211 skip: skip,
212 allowedTools: allowedTools,
213 }
214}