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