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}
54
55func (s *permissionService) GrantPersistent(permission PermissionRequest) {
56 respCh, ok := s.pendingRequests.Load(permission.ID)
57 if ok {
58 respCh.(chan bool) <- true
59 }
60
61 s.sessionPermissionsMu.Lock()
62 s.sessionPermissions = append(s.sessionPermissions, permission)
63 s.sessionPermissionsMu.Unlock()
64}
65
66func (s *permissionService) Grant(permission PermissionRequest) {
67 respCh, ok := s.pendingRequests.Load(permission.ID)
68 if ok {
69 respCh.(chan bool) <- true
70 }
71}
72
73func (s *permissionService) Deny(permission PermissionRequest) {
74 respCh, ok := s.pendingRequests.Load(permission.ID)
75 if ok {
76 respCh.(chan bool) <- false
77 }
78}
79
80func (s *permissionService) Request(opts CreatePermissionRequest) bool {
81 if s.skip {
82 return true
83 }
84
85 s.autoApproveSessionsMu.RLock()
86 autoApprove := slices.Contains(s.autoApproveSessions, opts.SessionID)
87 s.autoApproveSessionsMu.RUnlock()
88
89 if autoApprove {
90 return true
91 }
92
93 dir := filepath.Dir(opts.Path)
94 if dir == "." {
95 dir = s.workingDir
96 }
97 permission := PermissionRequest{
98 ID: uuid.New().String(),
99 Path: dir,
100 SessionID: opts.SessionID,
101 ToolName: opts.ToolName,
102 Description: opts.Description,
103 Action: opts.Action,
104 Params: opts.Params,
105 }
106
107 s.sessionPermissionsMu.RLock()
108 for _, p := range s.sessionPermissions {
109 if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
110 s.sessionPermissionsMu.RUnlock()
111 return true
112 }
113 }
114 s.sessionPermissionsMu.RUnlock()
115
116 respCh := make(chan bool, 1)
117
118 s.pendingRequests.Store(permission.ID, respCh)
119 defer s.pendingRequests.Delete(permission.ID)
120
121 s.Publish(pubsub.CreatedEvent, permission)
122
123 // Wait for the response indefinitely
124 return <-respCh
125}
126
127func (s *permissionService) AutoApproveSession(sessionID string) {
128 s.autoApproveSessionsMu.Lock()
129 s.autoApproveSessions = append(s.autoApproveSessions, sessionID)
130 s.autoApproveSessionsMu.Unlock()
131}
132
133func NewPermissionService(workingDir string, skip bool) Service {
134 return &permissionService{
135 Broker: pubsub.NewBroker[PermissionRequest](),
136 workingDir: workingDir,
137 sessionPermissions: make([]PermissionRequest, 0),
138 skip: skip,
139 }
140}