// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "slices" "time" "git.stormux.org/storm/cthulhu/internal/interaction" "git.stormux.org/storm/cthulhu/internal/tentacle" "git.stormux.org/storm/cthulhu/internal/tentaclewire" ) type localTentacleForms interface { OpenLocal(context.Context, interaction.Form) (<-chan internalUIResult, error) } // authorize_remote reserves a standalone core dialog. Interactive commands use // authorize_remote_session with their already-reserved slot and exact session. func (runtime *tentacleRuntime) authorize_remote(ctx context.Context, id, role string, grants []string) (tentaclewire.SessionEvent, error) { if runtime == nil || runtime.remotePolicy == nil || !tentaclewire.ValidRoleGrants(role, grants) { return tentaclewire.SessionEvent{}, tentacle.ErrPermission } runtime.mutex.Lock() var target *tentacle.Session if !runtime.disabled { for _, session := range runtime.sessions { if session.TentacleID() == id { target = session break } } } runtime.mutex.Unlock() if target == nil { return tentaclewire.SessionEvent{}, tentacle.ErrUnavailable } // Reserve the interaction before creating a ticket, so a second request // rejected as busy cannot invalidate the dialog already on screen. select { case runtime.interactionSlots <- struct{}{}: default: return tentaclewire.SessionEvent{}, tentacle.ErrBusy } defer func() { <-runtime.interactionSlots }() return runtime.authorize_remote_session(ctx, target, role, grants) } // Caller owns interactionSlots. A child request may open a dialog, but only // local capture and the core-issued ticket can authorize the requesting session. func (runtime *tentacleRuntime) authorize_remote_session(ctx context.Context, target *tentacle.Session, role string, grants []string) (tentaclewire.SessionEvent, error) { if runtime == nil || runtime.remotePolicy == nil || target == nil || !tentaclewire.ValidRoleGrants(role, grants) { return tentaclewire.SessionEvent{}, tentacle.ErrPermission } forms, ok := runtime.forms.(localTentacleForms) if !ok { return tentaclewire.SessionEvent{}, tentacle.ErrUnavailable } runtime.mutex.Lock() current := !runtime.disabled && slices.Contains(runtime.sessions, target) runtime.mutex.Unlock() if !current { return tentaclewire.SessionEvent{}, tentacle.ErrUnavailable } grants = slices.Clone(grants) installed := target.RemoteGrants() for _, grant := range grants { if !slices.Contains(installed, grant) { return tentaclewire.SessionEvent{}, tentacle.ErrPermission } } approval, err := target.BeginRemoteApproval(role, grants) if err != nil { return tentaclewire.SessionEvent{}, err } defer approval.Cancel() ctx, cancel := context.WithTimeout(ctx, 2*time.Minute) defer cancel() stopPolicy := context.AfterFunc(approval.Context(), cancel) defer stopPolicy() stopSession := context.AfterFunc(target.OutputContext(), cancel) defer stopSession() form := remote_consent_form(target.TentacleID(), role, grants) replies, err := forms.OpenLocal(ctx, form) if err != nil { return tentaclewire.SessionEvent{}, err } select { case reply, open := <-replies: if !open || ctx.Err() != nil || reply.Cancelled || reply.Err != nil || reply.Activated != "approve" { return tentaclewire.SessionEvent{}, tentacle.ErrPermission } var selected []string for _, grant := range grants { if reply.Values[grant].Checked { selected = append(selected, grant) } } if len(selected) == 0 { return tentaclewire.SessionEvent{}, tentacle.ErrPermission } return target.AuthorizeRemoteWithApproval(approval, role, selected) case <-ctx.Done(): return tentaclewire.SessionEvent{}, tentacle.ErrPermission } } func remote_consent_form(id, role string, grants []string) interaction.Form { roleText := "control another computer" if role == "controlled" { roleText = "allow a helper to access this computer" } form := interaction.Form{Title: "Remote access for " + id + ": " + roleText + ". The relay can read session contents. Select access for this session.", Elements: []interaction.Element{{ID: "cancel", Label: "Cancel", Kind: interaction.Button}}} labels := map[string]string{ "output-observe": "Share speech with the helper", "remote-present": "Hear the other computer", "keyboard-capture": "Keyboard control of the other computer. F11 returns here", "keyboard-inject": "Allow the helper to type here", "clipboard-exchange": "Clipboard text, transferred on request", "braille-stream": "Braille display and routing", } for _, grant := range grants { form.Elements = append(form.Elements, interaction.Element{ID: grant, Label: labels[grant], Kind: interaction.CheckBox}) } form.Elements = append(form.Elements, interaction.Element{ID: "approve", Label: "Allow selected access and connect", Kind: interaction.Button}) return form } func (runtime *tentacleRuntime) authorize_remote_request(ctx context.Context, target *tentacle.Session, request tentaclewire.RemoteRequest) (tentaclewire.SessionEvent, error) { if target == nil || !tentaclewire.ValidRemoteRequest(request) { return tentaclewire.SessionEvent{}, tentacle.ErrPermission } grants := slices.Clone(request.Grants) installed := target.RemoteGrants() for _, grant := range request.OptionalGrants { if slices.Contains(installed, grant) { grants = append(grants, grant) } } return runtime.authorize_remote_session(ctx, target, request.Role, grants) }