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