on main help sign in
fix(permission): fix publish-before-lock race and use O(1) session permission lookups

Move notification publish inside requestMu lock to prevent inconsistent UI state, and replace the sessionPermissions slice with csync.Map for constant-time persistent permission lookups. ๐Ÿ’˜ Generated with Crush Assisted-by: DeepSeek V4 Pro via Crush <crush@charm.land>

commit02b783a380c074c934f1534226c989d03e1642a8 authorKieran Klukas <kieran@dunkirk.sh> dateFri 8 May 2026, 18:09 (5 months ago) parent0485adc
1 file changed+30โˆ’19
internal/permission/permission.go+30โˆ’19
internal/permission/permission.go+30โˆ’19
@@ -73,13 +73,20 @@ type Service interface {
7373 SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[PermissionNotification]
7474}
7575
76+// PermissionKey is a composite key for session permission lookups.
77+type PermissionKey struct {
78+ SessionID string
79+ ToolName string
80+ Action string
81+ Path string
82+}
83+
7684type permissionService struct {
7785 *pubsub.Broker[PermissionRequest]
7886
7987 notificationBroker *pubsub.Broker[PermissionNotification]
8088 workingDir string
81โˆ’ sessionPermissions []PermissionRequest
82โˆ’ sessionPermissionsMu sync.RWMutex
89+ sessionPermissions *csync.Map[PermissionKey, bool]
8390 pendingRequests *csync.Map[string, chan bool]
8491 autoApproveSessions map[string]bool
8592 autoApproveSessionsMu sync.RWMutex
@@ -102,9 +109,12 @@ func (s *permissionService) GrantPersistent(permission PermissionRequest) {
102109 respCh <- true
103110 }
104111
105โˆ’ s.sessionPermissionsMu.Lock()
106โˆ’ s.sessionPermissions = append(s.sessionPermissions, permission)
107โˆ’ s.sessionPermissionsMu.Unlock()
112+ s.sessionPermissions.Set(PermissionKey{
113+ SessionID: permission.SessionID,
114+ ToolName: permission.ToolName,
115+ Action: permission.Action,
116+ Path: permission.Path,
117+ }, true)
108118
109119 s.activeRequestMu.Lock()
110120 if s.activeRequest != nil && s.activeRequest.ID == permission.ID {
@@ -171,12 +181,13 @@ func (s *permissionService) Request(ctx context.Context, opts CreatePermissionRe
171181 return true, nil
172182 }
173183
184+ s.requestMu.Lock()
185+ defer s.requestMu.Unlock()
186+
174187 // tell the UI that a permission was requested
175188 s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
176189 ToolCallID: opts.ToolCallID,
177190 })
178โˆ’ s.requestMu.Lock()
179โˆ’ defer s.requestMu.Unlock()
180191
181192 s.autoApproveSessionsMu.RLock()
182193 autoApprove := s.autoApproveSessions[opts.SessionID]
@@ -214,18 +225,18 @@ func (s *permissionService) Request(ctx context.Context, opts CreatePermissionRe
214225 Params: opts.Params,
215226 }
216227
217โˆ’ s.sessionPermissionsMu.RLock()
218โˆ’ for _, p := range s.sessionPermissions {
219โˆ’ if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
220โˆ’ s.sessionPermissionsMu.RUnlock()
221โˆ’ s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
222โˆ’ ToolCallID: opts.ToolCallID,
223โˆ’ Granted: true,
224โˆ’ })
225โˆ’ return true, nil
226โˆ’ }
228+ if _, ok := s.sessionPermissions.Get(PermissionKey{
229+ SessionID: permission.SessionID,
230+ ToolName: permission.ToolName,
231+ Action: permission.Action,
232+ Path: permission.Path,
233+ }); ok {
234+ s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
235+ ToolCallID: opts.ToolCallID,
236+ Granted: true,
237+ })
238+ return true, nil
227239 }
228โˆ’ s.sessionPermissionsMu.RUnlock()
229240
230241 s.activeRequestMu.Lock()
231242 s.activeRequest = &permission
@@ -269,7 +280,7 @@ func NewPermissionService(workingDir string, skip bool, allowedTools []string) S
269280 Broker: pubsub.NewBroker[PermissionRequest](),
270281 notificationBroker: pubsub.NewBroker[PermissionNotification](),
271282 workingDir: workingDir,
272โˆ’ sessionPermissions: make([]PermissionRequest, 0),
283+ sessionPermissions: csync.NewMap[PermissionKey, bool](),
273284 autoApproveSessions: make(map[string]bool),
274285 skip: skip,
275286 allowedTools: allowedTools,