1package permission
2
3import (
4 "errors"
5 "path/filepath"
6 "slices"
7 "sync"
8
9 "github.com/charmbracelet/crush/internal/pubsub"
10 "github.com/google/uuid"
11)
12
13var ErrorPermissionDenied = errors.New("permission denied")
14
15type CreatePermissionRequest struct {
16 SessionID string `json:"session_id"`
17 ToolName string `json:"tool_name"`
18 Description string `json:"description"`
19 Action string `json:"action"`
20 Params any `json:"params"`
21 Path string `json:"path"`
22}
23
24type PermissionRequest struct {
25 ID string `json:"id"`
26 SessionID string `json:"session_id"`
27 ToolName string `json:"tool_name"`
28 Description string `json:"description"`
29 Action string `json:"action"`
30 Params any `json:"params"`
31 Path string `json:"path"`
32}
33
34type Service interface {
35 pubsub.Suscriber[PermissionRequest]
36 GrantPersistent(permission PermissionRequest)
37 Grant(permission PermissionRequest)
38 Deny(permission PermissionRequest)
39 Request(opts CreatePermissionRequest) bool
40 AutoApproveSession(sessionID string)
41}
42
43type permissionService struct {
44 *pubsub.Broker[PermissionRequest]
45
46 workingDir string
47 sessionPermissions []PermissionRequest
48 sessionPermissionsMu sync.RWMutex
49 pendingRequests sync.Map
50 autoApproveSessions []string
51 autoApproveSessionsMu sync.RWMutex
52 skip bool
53 allowedTools []string
54}
55
56func (s *permissionService) GrantPersistent(permission PermissionRequest) {
57 respCh, ok := s.pendingRequests.Load(permission.ID)
58 if ok {
59 respCh.(chan bool) <- true
60 }
61
62 s.sessionPermissionsMu.Lock()
63 s.sessionPermissions = append(s.sessionPermissions, permission)
64 s.sessionPermissionsMu.Unlock()
65}
66
67func (s *permissionService) Grant(permission PermissionRequest) {
68 respCh, ok := s.pendingRequests.Load(permission.ID)
69 if ok {
70 respCh.(chan bool) <- true
71 }
72}
73
74func (s *permissionService) Deny(permission PermissionRequest) {
75 respCh, ok := s.pendingRequests.Load(permission.ID)
76 if ok {
77 respCh.(chan bool) <- false
78 }
79}
80
81func (s *permissionService) Request(opts CreatePermissionRequest) bool {
82 if s.skip {
83 return true
84 }
85
86 // Check if the tool/action combination is in the allowlist
87 commandKey := opts.ToolName + ":" + opts.Action
88 if slices.Contains(s.allowedTools, commandKey) || slices.Contains(s.allowedTools, opts.ToolName) {
89 return true
90 }
91
92 s.autoApproveSessionsMu.RLock()
93 autoApprove := slices.Contains(s.autoApproveSessions, opts.SessionID)
94 s.autoApproveSessionsMu.RUnlock()
95
96 if autoApprove {
97 return true
98 }
99
100 dir := filepath.Dir(opts.Path)
101 if dir == "." {
102 dir = s.workingDir
103 }
104 permission := PermissionRequest{
105 ID: uuid.New().String(),
106 Path: dir,
107 SessionID: opts.SessionID,
108 ToolName: opts.ToolName,
109 Description: opts.Description,
110 Action: opts.Action,
111 Params: opts.Params,
112 }
113
114 s.sessionPermissionsMu.RLock()
115 for _, p := range s.sessionPermissions {
116 if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
117 s.sessionPermissionsMu.RUnlock()
118 return true
119 }
120 }
121 s.sessionPermissionsMu.RUnlock()
122
123 respCh := make(chan bool, 1)
124
125 s.pendingRequests.Store(permission.ID, respCh)
126 defer s.pendingRequests.Delete(permission.ID)
127
128 s.Publish(pubsub.CreatedEvent, permission)
129
130 // Wait for the response indefinitely
131 return <-respCh
132}
133
134func (s *permissionService) AutoApproveSession(sessionID string) {
135 s.autoApproveSessionsMu.Lock()
136 s.autoApproveSessions = append(s.autoApproveSessions, sessionID)
137 s.autoApproveSessionsMu.Unlock()
138}
139
140func NewPermissionService(workingDir string, skip bool, allowedTools []string) Service {
141 return &permissionService{
142 Broker: pubsub.NewBroker[PermissionRequest](),
143 workingDir: workingDir,
144 sessionPermissions: make([]PermissionRequest, 0),
145 skip: skip,
146 allowedTools: allowedTools,
147 }
148}