summaryrefslogtreecommitdiffhomepage
path: root/internal/permission
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-05-12 08:43:34 -0500
committeradamdottv <[email protected]>2025-05-12 08:43:34 -0500
commited9fba99c9e230094ed5d468c88f81469d60c911 (patch)
tree0cebc5f7610e91c58c0289d3aa12d1f4c57eab8e /internal/permission
parentf1007771997bd0401516eda87a7e0ac92f269680 (diff)
downloadopencode-ed9fba99c9e230094ed5d468c88f81469d60c911.tar.gz
opencode-ed9fba99c9e230094ed5d468c88f81469d60c911.zip
wip: refactoring
Diffstat (limited to 'internal/permission')
-rw-r--r--internal/permission/permission.go211
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)
}