Allow Remote sessions to outlive connection forms

This commit is contained in:
ak47 committed 2026-11-04 17:19:19 -05:00
1 parent b60cf5d5f2
commit b4896a2eb4
10 files changed
+233 -16

No files matched your search

+15 -1
View File
@@ -76,6 +76,20 @@ func (runtime *tentacleRuntime) interact(ctx context.Context, command tentacleCo
if runtime.output.gate.Locked() || runtime.output.gate.Suspended() {
return
}
if value.RemoteBackground != nil {
lease := *value.RemoteBackground
lifetime, leaseErr := command.session.RemoteLeaseContext(lease)
if leaseErr != nil || ctx.Err() != nil || lifetime.Err() != nil || approvedEpoch != 0 && approvedEpoch != lease.Epoch {
runtime.report("Remote connection ended. Start a fresh access review.")
return
}
// The reader lease already belongs to the policy/session owner.
// Returning to the desktop releases form and interaction capture,
// without extending consent or running the cancellation continuation.
approvedEpoch, cleanupNext = 0, ""
runtime.output.Present(runtime.output.Capture(command.session.OutputContext()), value.Presentation, "tentacle/"+command.id)
return
}
cleanupNext = value.Next
if value.Remote != nil {
action, approvalErr := runtime.authorize_remote_session(ctx, command.session, value.Remote.Role, value.Remote.Grants)
@@ -255,7 +269,7 @@ func (runtime *tentacleRuntime) waitTentacleForm(ctx context.Context, command te
return internalUIResult{Cancelled: true}, nil
}
status, err := tentaclewire.DecodeInteraction(raw)
if err != nil || status.Form != nil || status.Source != "" || status.Next != "" || status.Reader != nil || status.Remote != nil {
if err != nil || status.Form != nil || status.Source != "" || status.Next != "" || status.Reader != nil || status.Remote != nil || status.RemoteBackground != nil {
command.session.Disable()
return internalUIResult{Cancelled: true}, nil
}
@@ -222,8 +222,14 @@ func TestRemoteConnectionCommandCancelsInspectionOnHandoff(t *testing.T) {
}
func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
for _, role := range []string{"controller", "controlled"} {
t.Run(role, func(t *testing.T) {
for _, test := range []struct{ role, ending string }{
{"controller", "disconnect"}, {"controlled", "disconnect"},
{"controller", "lock"}, {"controlled", "lock"},
{"controller", "handoff"}, {"controlled", "handoff"},
{"controller", "disable"}, {"controlled", "disable"},
} {
role, ending := test.role, test.ending
t.Run(role+"/"+ending, func(t *testing.T) {
remoteText := " " + strings.Repeat("é\x00世界 ", 1500) + " "
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
@@ -299,7 +305,7 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
}
close(closed)
}()
runtime, forms, _ := remote_command_fixture(t)
runtime, forms, shortcut := remote_command_fixture(t)
if role == "controlled" {
worker := runtime.output.speech
worker.speaker = &fakeSpeaker{spoken: make(chan string, 16)}
@@ -356,6 +362,18 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
if status.form.Title != "Remote relay status" {
t.Fatal("missing connection status")
}
// Open has already succeeded before the relay can join. Return to
// ordinary desktop work without holding either runtime slot or form.
status.reply <- internalUIResult{Activated: "desktop"}
waitTentacle(t, func() bool { return len(runtime.resultSlots) == 0 && len(runtime.interactionSlots) == 0 })
if status.ctx.Err() == nil {
t.Fatal("background connection retained its form context")
}
select {
case <-closed:
t.Fatal("return to desktop disconnected the relay")
default:
}
var delivery tentacle.RemoteSpeechDelivery
if role == "controller" {
select {
@@ -393,6 +411,8 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
// Joining at the relay is earlier than the Tentacle admitting the
// peer. Poll status before testing actual output rather than
// depending on a scheduling delay or replaying pre-peer output.
runtime.Invoke(t.Context(), shortcut)
status = next_remote_form(t, forms)
deadline := time.Now().Add(5 * time.Second)
for {
encoded, _ := json.Marshal(status.form)
@@ -405,6 +425,8 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
status.reply <- internalUIResult{Activated: "refresh"}
status = next_remote_form(t, forms)
}
status.reply <- internalUIResult{Activated: "desktop"}
waitTentacle(t, func() bool { return len(runtime.resultSlots) == 0 })
worker := runtime.output.speech
for _, request := range []speechRequest{
{Text: "private form", Cause: "internal-ui"},
@@ -455,15 +477,40 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
t.Fatal("observation leaked private text or lost resolved output/boundaries")
}
}
status.reply <- internalUIResult{Activated: "cancel"}
// Reopening returns to this attempt, with no second consent or
// saved-key read. Lifecycle retirement must work with no form open.
switch ending {
case "disconnect":
runtime.Invoke(t.Context(), shortcut)
status = next_remote_form(t, forms)
if status.form.Title != "Remote relay status" {
t.Fatal("reopening lost the approved connection")
}
status.reply <- internalUIResult{Activated: "cancel"}
case "lock":
runtime.output.gate.SetLocked(true, nil, nil, nil, nil, &brailleWorker{})
runtime.output.gate.SetLocked(false, nil, nil, nil, nil, &brailleWorker{})
case "handoff":
runtime.output.gate.SetSuspended(true)
runtime.output.gate.SetSuspended(false)
case "disable":
runtime.Disable()
}
select {
case <-closed:
case <-time.After(5 * time.Second):
t.Fatal("Disconnect and close retained the relay")
t.Fatal("background connection retained the relay after " + ending)
}
waitTentacle(t, func() bool { return len(runtime.resultSlots) == 0 })
if role == "controller" && delivery.Context.Err() == nil {
t.Fatal("Disconnect retained remote delivery authority")
if role == "controller" {
// Socket cancellation precedes the final sequenced lease close.
// A reopened status interaction did not issue the original consent,
// so its result-slot completion is not the lease-close barrier.
select {
case <-delivery.Context.Done():
case <-time.After(3 * time.Second):
t.Fatal("Disconnect retained remote delivery authority")
}
}
})
}
+13
View File
@@ -144,6 +144,19 @@ func (session *Session) RemoteContext(lease tentaclewire.RemoteLease, grant stri
return access.ctx, nil
}
// RemoteLeaseContext checks process ownership and lifetime, without authorizing
// any desktop effect. Core uses it when returning from a connection form to the
// desktop; effect owners must still use RemoteContext with their specific grant.
func (session *Session) RemoteLeaseContext(lease tentaclewire.RemoteLease) (context.Context, error) {
session.mutex.Lock()
defer session.mutex.Unlock()
access := session.remote_lease_locked(lease)
if access == nil {
return nil, ErrPermission
}
return access.ctx, nil
}
func (session *Session) release_remote_owner() {
session.manager.remoteMutex.Lock()
if session.manager.remoteOwner == session {
@@ -0,0 +1,67 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package tentacle
import (
"strings"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
)
func TestRemoteBackgroundLeaseOwnershipAndLifetime(t *testing.T) {
for _, language := range []string{"go", "python"} {
t.Run(language, func(t *testing.T) {
manager := test_manager(t, DefaultLimits(), nil)
policy := NewRemotePolicy()
if err := manager.UseRemotePolicy(policy); err != nil {
t.Fatal(err)
}
session := startSessionSDK(t, manager, language, "speech")
lease := speech_fixture_lease(t, session, "controlled", []string{"output-observe"})
lifetime, err := session.RemoteLeaseContext(lease)
if err != nil || lifetime.Err() != nil {
t.Fatal("live owned lease unavailable", err)
}
for _, invalid := range []tentaclewire.RemoteLease{{}, {Lease: lease.Lease, Epoch: lease.Epoch + 1}, {Lease: strings.Repeat("b", 32), Epoch: lease.Epoch}} {
if _, err := session.RemoteLeaseContext(invalid); err == nil {
t.Fatal("invalid ownership accepted")
}
}
if _, err := session.RemoteContext(lease, ""); err == nil {
t.Fatal("ownership check bypassed desktop grant requirement")
}
other := startSessionSDK(t, manager, language, "speech-other")
if _, err := other.RemoteLeaseContext(lease); err == nil {
t.Fatal("another process session acquired lease ownership")
}
policy.SetAvailable(false)
policy.SetAvailable(true)
if lifetime.Err() == nil {
t.Fatal("lock/handoff round trip retained background lifetime")
}
if _, err := session.RemoteLeaseContext(lease); err == nil {
t.Fatal("background ownership survived policy retirement")
}
// A replacement remains distinct, even while old forms finish.
session.RevokeRemote()
invokeSessionSDK(t, session, "event", nil)
replacement := speech_fixture_lease(t, session, "controlled", []string{"output-observe"})
if _, err := session.RemoteLeaseContext(lease); err == nil {
t.Fatal("old controls followed replacement consent")
}
session.RevokeRemoteEpoch(lease.Epoch)
if _, err := session.RemoteLeaseContext(replacement); err != nil {
t.Fatal("old cleanup retired replacement", err)
}
session.mutex.Lock()
session.remote.deadline = time.Now().Add(-time.Second)
session.mutex.Unlock()
if _, err := session.RemoteLeaseContext(replacement); err == nil {
t.Fatal("expired lease accepted for background ownership")
}
})
}
}
+9 -1
View File
@@ -47,8 +47,16 @@ func (session *Session) EnableRemoteSpeech() <-chan RemoteSpeechDelivery {
}
func (session *Session) remote_access_locked(lease tentaclewire.RemoteLease, grant string) *remoteAccess {
access := session.remote_lease_locked(lease)
if access == nil || !slices.Contains(access.grants, grant) {
return nil
}
return access
}
func (session *Session) remote_lease_locked(lease tentaclewire.RemoteLease) *remoteAccess {
access := session.remote
if session.state != Active || access == nil || access.lease == "" || access.lease != lease.Lease || access.epoch != lease.Epoch || !time.Now().Before(access.deadline) || access.ctx.Err() != nil || !slices.Contains(access.grants, grant) {
if session.state != Active || access == nil || access.lease == "" || access.lease != lease.Lease || access.epoch != lease.Epoch || !time.Now().Before(access.deadline) || access.ctx.Err() != nil {
return nil
}
return access
+8 -1
View File
@@ -26,6 +26,9 @@ type Interaction struct {
Done bool `json:"done,omitempty"`
Reader *ReaderRequest `json:"reader,omitempty"`
Remote *RemoteRequest `json:"remote,omitempty"`
// RemoteBackground ends this interaction while retaining an already-open
// lease owned by this process session. It grants no new access or lifetime.
RemoteBackground *RemoteLease `json:"remote_background,omitempty"`
}
// RemoteRequest asks the reader to show its own local consent form. It never
@@ -80,7 +83,11 @@ func DecodeInteraction(data []byte) (Interaction, error) {
if value.Source != "" && value.Source != "focus" && value.Source != "clipboard" {
return value, ErrProtocol
}
if value.Remote != nil {
if value.RemoteBackground != nil {
if len(value.RemoteBackground.Lease) != 32 || value.RemoteBackground.Epoch == 0 || !value.Done || value.Next != "" || value.Poll != "" || value.Form != nil || value.Source != "" || value.Clipboard != nil || value.Reader != nil || value.Remote != nil || value.Uninterruptible {
return value, ErrProtocol
}
} else if value.Remote != nil {
if !ValidRoleGrants(value.Remote.Role, value.Remote.Grants) || value.Next == "" || value.Reader != nil || value.Form != nil || value.Source != "" || value.Poll != "" || value.Clipboard != nil || value.Done {
return value, ErrProtocol
}
@@ -4,6 +4,7 @@ package tentaclewire
import (
"encoding/json"
"strings"
"testing"
)
@@ -32,3 +33,29 @@ func TestRemoteConsentInteractionValidation(t *testing.T) {
}
}
}
func TestRemoteBackgroundInteractionValidation(t *testing.T) {
lease := RemoteLease{Lease: strings.Repeat("a", 32), Epoch: 1}
valid := Interaction{RemoteBackground: &lease, Done: true, Presentation: Presentation{Speech: "Connection controls closed"}}
data, _ := json.Marshal(valid)
if _, err := DecodeInteraction(data); err != nil {
t.Fatal(err)
}
for _, raw := range []string{
`{"remote_background":null,"done":true}`,
`{"remote_background":{"lease":"short","epoch":1},"done":true}`,
`{"remote_background":{"lease":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","epoch":0},"done":true}`,
`{"remote_background":{"lease":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","epoch":1,"session":"other"},"done":true}`,
`{"remote_background":{"lease":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","epoch":1}}`,
} {
if _, err := DecodeInteraction([]byte(raw)); err == nil {
t.Fatal("invalid background interaction accepted", raw)
}
}
for _, field := range []string{`"form":{"title":"Menu","elements":[]}`, `"next":"submit"`, `"poll":"poll"`, `"source":"focus"`, `"clipboard":"text"`, `"reader":{"snapshot":true}`, `"remote":{"role":"controller","grants":["remote-present"]}`, `"uninterruptible":true`} {
raw := `{"remote_background":{"lease":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","epoch":1},"done":true,"speech":"Closed",` + field + `}`
if _, err := DecodeInteraction([]byte(raw)); err == nil {
t.Fatal("background interaction mixed with another effect", field)
}
}
}