Files
2026-11-03 23:48:31 -05:00

136 lines
4.5 KiB
Go

// 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)
}