// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package tentacle import ( "context" "slices" "sync" "git.stormux.org/storm/cthulhu/internal/tentaclewire" ) // RemotePolicy belongs to the reader's focus/lock owner. Its cancellation is // synchronous and does not acquire a session, command, output or registry lock. // Restoring availability creates a fresh lifetime; it never revives consent. type RemotePolicy struct { mutex sync.Mutex ctx context.Context cancel context.CancelFunc available bool } // RemoteApproval is a single-use core ticket captured BEFORE showing local // consent. It is not serializable and cannot be supplied by a Tentacle RPC. type RemoteApproval struct { policy *RemotePolicy ctx context.Context lifetime context.Context cancel context.CancelFunc session *Session role string grants []string } func NewRemotePolicy() *RemotePolicy { ctx, cancel := context.WithCancel(context.Background()) return &RemotePolicy{ctx: ctx, cancel: cancel, available: true} } func (policy *RemotePolicy) SetAvailable(available bool) { policy.mutex.Lock() defer policy.mutex.Unlock() if policy.available == available { return } policy.cancel() policy.available = available if available { policy.ctx, policy.cancel = context.WithCancel(context.Background()) } } // Context captures the current availability lifetime for sensitive forms. A // lock/handoff round trip cancels that lifetime even if native polling misses // the short transition; callers must never replace it with the resumed one. func (policy *RemotePolicy) Context() context.Context { policy.mutex.Lock() defer policy.mutex.Unlock() return policy.ctx } func (session *Session) BeginRemoteApproval(role string, grants []string) (*RemoteApproval, error) { session.mutex.Lock() defer session.mutex.Unlock() policy := session.manager.remotePolicy if policy == nil || session.state != Active || session.ctx.Err() != nil || !session.spec.SessionEvents || !tentaclewire.ValidRoleGrants(role, grants) { return nil, ErrPermission } for _, grant := range grants { if !slices.Contains(session.spec.RemoteGrants, grant) { return nil, ErrPermission } } policy.mutex.Lock() defer policy.mutex.Unlock() if !policy.available { return nil, ErrPermission } session.revoke_remote_locked("replaced", true) if session.state != Active { return nil, ErrUnavailable } ctx, cancel := context.WithCancel(policy.ctx) approval := &RemoteApproval{policy: policy, ctx: ctx, lifetime: policy.ctx, cancel: cancel, session: session, role: role, grants: slices.Clone(grants)} session.remoteApproval = approval return approval, nil } func (approval *RemoteApproval) Context() context.Context { return approval.ctx } // Cancel only retires this pending dialog, never an issued lease or a newer // dialog. Package changes and session shutdown also cancel pending approvals. func (approval *RemoteApproval) Cancel() { approval.session.mutex.Lock() defer approval.session.mutex.Unlock() if approval.session.remoteApproval == approval { approval.session.remoteApproval = nil } approval.cancel() } // UseRemotePolicy must run before starting any children. A production manager // with a policy rejects the unrestricted authorizer used by protocol fixtures. func (manager *Manager) UseRemotePolicy(policy *RemotePolicy) error { manager.mutex.Lock() defer manager.mutex.Unlock() if policy == nil || manager.remotePolicy != nil || len(manager.sessions) != 0 || manager.closed { return ErrUnavailable } manager.remotePolicy = policy return nil } func (session *Session) AuthorizeRemoteWithApproval(approval *RemoteApproval, role string, grants []string) (tentaclewire.SessionEvent, error) { session.mutex.Lock() defer session.mutex.Unlock() policy := session.manager.remotePolicy if policy == nil || approval == nil || approval.policy != policy || session.remoteApproval != approval || approval.session != session { return tentaclewire.SessionEvent{}, ErrPermission } policy.mutex.Lock() defer policy.mutex.Unlock() if !policy.available || approval.lifetime != policy.ctx || approval.ctx.Err() != nil || role != approval.role || !tentaclewire.ValidRoleGrants(role, grants) { return tentaclewire.SessionEvent{}, ErrPermission } for _, grant := range grants { if !slices.Contains(approval.grants, grant) { return tentaclewire.SessionEvent{}, ErrPermission } } session.remoteApproval = nil defer approval.cancel() return session.authorize_remote_locked(role, grants, approval.lifetime) }