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