1package permission
2
3import (
4 "context"
5 "errors"
6 "path/filepath"
7 "slices"
8 "sync"
9 "time"
10
11 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/pubsub"
13 "github.com/google/uuid"
14)
15
16var ErrorPermissionDenied = errors.New("permission denied")
17
18type CreatePermissionRequest struct {
19 SessionID string `json:"session_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 PermissionRequest struct {
28 ID string `json:"id"`
29 SessionID string `json:"session_id"`
30 ToolName string `json:"tool_name"`
31 Description string `json:"description"`
32 Action string `json:"action"`
33 Params any `json:"params"`
34 Path string `json:"path"`
35}
36
37type Service interface {
38 pubsub.Suscriber[PermissionRequest]
39 GrantPersistent(permission PermissionRequest)
40 Grant(permission PermissionRequest)
41 Deny(permission PermissionRequest)
42 Request(opts CreatePermissionRequest) bool
43 AutoApproveSession(sessionID string)
44}
45
46type permissionService struct {
47 *pubsub.Broker[PermissionRequest]
48
49 sessionPermissions []PermissionRequest
50 sessionPermissionsMu sync.RWMutex
51 pendingRequests sync.Map
52 autoApproveSessions []string
53 autoApproveSessionsMu sync.RWMutex
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 s.autoApproveSessionsMu.RLock()
83 autoApprove := slices.Contains(s.autoApproveSessions, opts.SessionID)
84 s.autoApproveSessionsMu.RUnlock()
85
86 if autoApprove {
87 return true
88 }
89
90 dir := filepath.Dir(opts.Path)
91 if dir == "." {
92 dir = config.WorkingDirectory()
93 }
94 permission := PermissionRequest{
95 ID: uuid.New().String(),
96 Path: dir,
97 SessionID: opts.SessionID,
98 ToolName: opts.ToolName,
99 Description: opts.Description,
100 Action: opts.Action,
101 Params: opts.Params,
102 }
103
104 s.sessionPermissionsMu.RLock()
105 for _, p := range s.sessionPermissions {
106 if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
107 s.sessionPermissionsMu.RUnlock()
108 return true
109 }
110 }
111 s.sessionPermissionsMu.RUnlock()
112
113 respCh := make(chan bool, 1)
114
115 s.pendingRequests.Store(permission.ID, respCh)
116 defer s.pendingRequests.Delete(permission.ID)
117
118 s.Publish(pubsub.CreatedEvent, permission)
119
120 // Wait for the response with a timeout to prevent indefinite blocking
121 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
122 defer cancel()
123
124 select {
125 case resp := <-respCh:
126 return resp
127 case <-ctx.Done():
128 return false // Timeout - deny by default
129 }
130}
131
132func (s *permissionService) AutoApproveSession(sessionID string) {
133 s.autoApproveSessionsMu.Lock()
134 s.autoApproveSessions = append(s.autoApproveSessions, sessionID)
135 s.autoApproveSessionsMu.Unlock()
136}
137
138func NewPermissionService() Service {
139 return &permissionService{
140 Broker: pubsub.NewBroker[PermissionRequest](),
141 sessionPermissions: make([]PermissionRequest, 0),
142 }
143}