diff options
| author | adamdottv <[email protected]> | 2025-05-12 08:43:34 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-05-12 08:43:34 -0500 |
| commit | ed9fba99c9e230094ed5d468c88f81469d60c911 (patch) | |
| tree | 0cebc5f7610e91c58c0289d3aa12d1f4c57eab8e /internal/permission | |
| parent | f1007771997bd0401516eda87a7e0ac92f269680 (diff) | |
| download | opencode-ed9fba99c9e230094ed5d468c88f81469d60c911.tar.gz opencode-ed9fba99c9e230094ed5d468c88f81469d60c911.zip | |
wip: refactoring
Diffstat (limited to 'internal/permission')
| -rw-r--r-- | internal/permission/permission.go | 211 |
1 files changed, 169 insertions, 42 deletions
diff --git a/internal/permission/permission.go b/internal/permission/permission.go index d6fdea664..8b924668e 100644 --- a/internal/permission/permission.go +++ b/internal/permission/permission.go @@ -1,11 +1,15 @@ package permission import ( + "context" "errors" + "fmt" "path/filepath" - "slices" + "strings" "sync" + "log/slog" + "github.com/google/uuid" "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/opencode/internal/pubsub" @@ -32,56 +36,141 @@ type PermissionRequest struct { Path string `json:"path"` } +type PermissionResponse struct { + Request PermissionRequest + Granted bool +} + +const ( + EventPermissionRequested pubsub.EventType = "permission_requested" + EventPermissionGranted pubsub.EventType = "permission_granted" + EventPermissionDenied pubsub.EventType = "permission_denied" + EventPermissionPersisted pubsub.EventType = "permission_persisted" +) + type Service interface { - pubsub.Suscriber[PermissionRequest] - GrantPersistant(permission PermissionRequest) - Grant(permission PermissionRequest) - Deny(permission PermissionRequest) - Request(opts CreatePermissionRequest) bool - AutoApproveSession(sessionID string) + pubsub.Subscriber[PermissionRequest] + SubscribeToResponseEvents(ctx context.Context) <-chan pubsub.Event[PermissionResponse] + + GrantPersistant(ctx context.Context, permission PermissionRequest) + Grant(ctx context.Context, permission PermissionRequest) + Deny(ctx context.Context, permission PermissionRequest) + Request(ctx context.Context, opts CreatePermissionRequest) bool + AutoApproveSession(ctx context.Context, sessionID string) + IsAutoApproved(ctx context.Context, sessionID string) bool } type permissionService struct { - *pubsub.Broker[PermissionRequest] + broker *pubsub.Broker[PermissionRequest] + responseBroker *pubsub.Broker[PermissionResponse] - sessionPermissions []PermissionRequest + sessionPermissions map[string][]PermissionRequest pendingRequests sync.Map - autoApproveSessions []string + autoApproveSessions map[string]bool + mu sync.RWMutex +} + +var globalPermissionService *permissionService + +func InitService() error { + if globalPermissionService != nil { + return fmt.Errorf("permission service already initialized") + } + globalPermissionService = &permissionService{ + broker: pubsub.NewBroker[PermissionRequest](), + responseBroker: pubsub.NewBroker[PermissionResponse](), + sessionPermissions: make(map[string][]PermissionRequest), + autoApproveSessions: make(map[string]bool), + } + return nil } -func (s *permissionService) GrantPersistant(permission PermissionRequest) { +func GetService() *permissionService { + if globalPermissionService == nil { + panic("permission service not initialized. Call permission.InitService() first.") + } + return globalPermissionService +} + +func (s *permissionService) GrantPersistant(ctx context.Context, permission PermissionRequest) { + s.mu.Lock() + s.sessionPermissions[permission.SessionID] = append(s.sessionPermissions[permission.SessionID], permission) + s.mu.Unlock() + respCh, ok := s.pendingRequests.Load(permission.ID) if ok { - respCh.(chan bool) <- true + select { + case respCh.(chan bool) <- true: + case <-ctx.Done(): + slog.Warn("Context cancelled while sending grant persistent response", "request_id", permission.ID) + } } - s.sessionPermissions = append(s.sessionPermissions, permission) + s.responseBroker.Publish(EventPermissionPersisted, PermissionResponse{Request: permission, Granted: true}) } -func (s *permissionService) Grant(permission PermissionRequest) { +func (s *permissionService) Grant(ctx context.Context, permission PermissionRequest) { respCh, ok := s.pendingRequests.Load(permission.ID) if ok { - respCh.(chan bool) <- true + select { + case respCh.(chan bool) <- true: + case <-ctx.Done(): + slog.Warn("Context cancelled while sending grant response", "request_id", permission.ID) + } } + s.responseBroker.Publish(EventPermissionGranted, PermissionResponse{Request: permission, Granted: true}) } -func (s *permissionService) Deny(permission PermissionRequest) { +func (s *permissionService) Deny(ctx context.Context, permission PermissionRequest) { respCh, ok := s.pendingRequests.Load(permission.ID) if ok { - respCh.(chan bool) <- false + select { + case respCh.(chan bool) <- false: + case <-ctx.Done(): + slog.Warn("Context cancelled while sending deny response", "request_id", permission.ID) + } } + s.responseBroker.Publish(EventPermissionDenied, PermissionResponse{Request: permission, Granted: false}) } -func (s *permissionService) Request(opts CreatePermissionRequest) bool { - if slices.Contains(s.autoApproveSessions, opts.SessionID) { +func (s *permissionService) Request(ctx context.Context, opts CreatePermissionRequest) bool { + s.mu.RLock() + if s.autoApproveSessions[opts.SessionID] { + s.mu.RUnlock() return true } - dir := filepath.Dir(opts.Path) - if dir == "." { - dir = config.WorkingDirectory() + + requestPath := opts.Path + if !filepath.IsAbs(requestPath) { + requestPath = filepath.Join(config.WorkingDirectory(), requestPath) } - permission := PermissionRequest{ + requestPath = filepath.Clean(requestPath) + + if permissions, ok := s.sessionPermissions[opts.SessionID]; ok { + for _, p := range permissions { + storedPath := p.Path + if !filepath.IsAbs(storedPath) { + storedPath = filepath.Join(config.WorkingDirectory(), storedPath) + } + storedPath = filepath.Clean(storedPath) + + if p.ToolName == opts.ToolName && p.Action == opts.Action && + (requestPath == storedPath || strings.HasPrefix(requestPath, storedPath+string(filepath.Separator))) { + s.mu.RUnlock() + return true + } + } + } + s.mu.RUnlock() + + normalizedPath := opts.Path + if !filepath.IsAbs(normalizedPath) { + normalizedPath = filepath.Join(config.WorkingDirectory(), normalizedPath) + } + normalizedPath = filepath.Clean(normalizedPath) + + permissionReq := PermissionRequest{ ID: uuid.New().String(), - Path: dir, + Path: normalizedPath, SessionID: opts.SessionID, ToolName: opts.ToolName, Description: opts.Description, @@ -89,31 +178,69 @@ func (s *permissionService) Request(opts CreatePermissionRequest) bool { Params: opts.Params, } - for _, p := range s.sessionPermissions { - if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path { - return true - } + respCh := make(chan bool, 1) + s.pendingRequests.Store(permissionReq.ID, respCh) + defer s.pendingRequests.Delete(permissionReq.ID) + + s.broker.Publish(EventPermissionRequested, permissionReq) + + select { + case resp := <-respCh: + return resp + case <-ctx.Done(): + slog.Warn("Permission request timed out or context cancelled", "request_id", permissionReq.ID, "tool", opts.ToolName) + return false } +} - respCh := make(chan bool, 1) +func (s *permissionService) AutoApproveSession(ctx context.Context, sessionID string) { + s.mu.Lock() + defer s.mu.Unlock() + s.autoApproveSessions[sessionID] = true +} - s.pendingRequests.Store(permission.ID, respCh) - defer s.pendingRequests.Delete(permission.ID) +func (s *permissionService) IsAutoApproved(ctx context.Context, sessionID string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.autoApproveSessions[sessionID] +} - s.Publish(pubsub.CreatedEvent, permission) +func (s *permissionService) Subscribe(ctx context.Context) <-chan pubsub.Event[PermissionRequest] { + return s.broker.Subscribe(ctx) +} - // Wait for the response with a timeout - resp := <-respCh - return resp +func (s *permissionService) SubscribeToResponseEvents(ctx context.Context) <-chan pubsub.Event[PermissionResponse] { + return s.responseBroker.Subscribe(ctx) } -func (s *permissionService) AutoApproveSession(sessionID string) { - s.autoApproveSessions = append(s.autoApproveSessions, sessionID) +func GrantPersistant(ctx context.Context, permission PermissionRequest) { + GetService().GrantPersistant(ctx, permission) } -func NewPermissionService() Service { - return &permissionService{ - Broker: pubsub.NewBroker[PermissionRequest](), - sessionPermissions: make([]PermissionRequest, 0), - } +func Grant(ctx context.Context, permission PermissionRequest) { + GetService().Grant(ctx, permission) +} + +func Deny(ctx context.Context, permission PermissionRequest) { + GetService().Deny(ctx, permission) +} + +func Request(ctx context.Context, opts CreatePermissionRequest) bool { + return GetService().Request(ctx, opts) +} + +func AutoApproveSession(ctx context.Context, sessionID string) { + GetService().AutoApproveSession(ctx, sessionID) +} + +func IsAutoApproved(ctx context.Context, sessionID string) bool { + return GetService().IsAutoApproved(ctx, sessionID) +} + +func SubscribeToRequests(ctx context.Context) <-chan pubsub.Event[PermissionRequest] { + return GetService().Subscribe(ctx) +} + +func SubscribeToResponses(ctx context.Context) <-chan pubsub.Event[PermissionResponse] { + return GetService().SubscribeToResponseEvents(ctx) } |
