Add Remote Braille IPC to the host and both SDKs

remote.braille requests and events carry a typed op under a
braille-stream lease: the controller's width and gestures for a
controlled reader, rendered cells back to its child, and cells, local
geometry and display gestures for a controller. Direction is validated
per role; a full mailbox revokes the lease. No display owner is
attached yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chase CichorzandClaude Opus 5.5 committed 2026-11-06 16:06:38 -05:00
1 parent 477d860fb8
commit 2651b5e9bd
10 files changed
+476 -3

No files matched your search

+12
View File
@@ -191,6 +191,16 @@ func (session *Session) revoke_remote_locked(reason string, notify bool) {
}
}
}
if session.remoteBraille != nil {
drainBraille:
for {
select {
case <-session.remoteBraille:
default:
break drainBraille
}
}
}
if session.remoteCapture != nil {
drainCapture:
for {
@@ -292,6 +302,8 @@ func (session *Session) remote_call_locked(method string, params json.RawMessage
return session.remote_key_locked(params)
case "remote.focus":
return session.remote_focus_locked(params)
case "remote.braille":
return session.remote_braille_locked(params)
case "remote.open":
var request tentaclewire.RemoteOpen
if decode_strict(params, &request) != nil || len(request.Action) != 32 || request.Epoch == 0 || !tentaclewire.ValidRoleGrants(request.Role, request.Grants) {
+79
View File
@@ -0,0 +1,79 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package tentacle
import (
"context"
"encoding/json"
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
)
// RemoteBrailleDelivery is admitted Braille work for the reader's owner.
// The owner must recheck Context and RemoteContext(braille-stream) and its own
// display policy before any effect. Role is the lease's core-assigned role.
type RemoteBrailleDelivery struct {
Context context.Context
Lease tentaclewire.RemoteLease
Role string
Sequence uint64
Braille tentaclewire.RemoteBraille
}
// EnableRemoteBraille attaches one reader-owned Braille mailbox. It performs
// no display setup and grants no authority.
func (session *Session) EnableRemoteBraille() <-chan RemoteBrailleDelivery {
session.mutex.Lock()
defer session.mutex.Unlock()
if session.remoteBraille == nil {
session.remoteBraille = make(chan RemoteBrailleDelivery, tentaclewire.RemoteBrailleQueueSize)
}
return session.remoteBraille
}
func (session *Session) remote_braille_locked(params json.RawMessage) (any, *RPCError) {
var request tentaclewire.RemoteBrailleRequest
if decode_strict(params, &request) != nil {
return nil, &RPCError{Code: -32602, Message: "Invalid session parameters"}
}
lease := tentaclewire.RemoteLease{Lease: request.Lease, Epoch: request.Epoch}
access := session.remote_access_locked(lease, "braille-stream")
if access == nil || request.Sequence <= access.sequence || !tentaclewire.ValidRemoteBrailleFor(access.role, true, request.Braille) {
return nil, &RPCError{Code: -32001, Message: "Session permission denied"}
}
if session.remoteBraille == nil {
return nil, &RPCError{Code: -32601, Message: "Remote Braille owner unavailable"}
}
access.sequence = request.Sequence
select {
case session.remoteBraille <- RemoteBrailleDelivery{Context: access.ctx, Lease: lease, Role: access.role, Sequence: request.Sequence, Braille: request.Braille}:
return struct{}{}, nil
default:
session.revoke_remote_locked("local", true)
return nil, &RPCError{Code: -32002, Message: "Remote Braille queue full"}
}
}
// PublishRemoteBraille sends one Braille operation to the child. The owner
// supplies only rendered cells (controlled), local geometry or local display
// gestures (controller); never text, routing identities or native handles.
func (session *Session) PublishRemoteBraille(lease tentaclewire.RemoteLease, value tentaclewire.RemoteBraille) error {
session.mutex.Lock()
defer session.mutex.Unlock()
access := session.remote_access_locked(lease, "braille-stream")
if access == nil || !tentaclewire.ValidRemoteBrailleFor(access.role, false, value) {
return ErrPermission
}
value.Cells = append([]int(nil), value.Cells...)
if len(value.Cells) == 0 {
value.Cells = nil
}
event := tentaclewire.SessionEvent{Kind: "remote.braille", Lease: lease.Lease, Epoch: lease.Epoch, Braille: &value}
if !session.remote_event_locked(&event) {
session.fail_locked(ErrLimit)
return ErrLimit
}
return nil
}
+119
View File
@@ -0,0 +1,119 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package tentacle
import (
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
)
func brailleRequest(lease tentaclewire.RemoteLease, sequence uint64, value tentaclewire.RemoteBraille) tentaclewire.RemoteBrailleRequest {
return tentaclewire.RemoteBrailleRequest{RemoteOperation: tentaclewire.RemoteOperation{Lease: lease.Lease, Epoch: lease.Epoch, Sequence: sequence}, Braille: value}
}
func TestRemoteBrailleSDKDirectionsAndEvents(t *testing.T) {
for _, language := range []string{"go", "python"} {
t.Run(language, func(t *testing.T) {
manager := test_manager(t, DefaultLimits(), nil)
session := startSessionSDK(t, manager, language, "braille", "braille-stream")
lease := speech_fixture_lease(t, session, "controlled", []string{"output-observe", "braille-stream"})
size := tentaclewire.RemoteBraille{Op: "size", Width: 40}
if _, code := remoteCall(t, session, "remote.braille", brailleRequest(lease, 1, size)); code != -32601 {
t.Fatal("Braille admitted without owner", code)
}
deliveries := session.EnableRemoteBraille()
if _, code := remoteCall(t, session, "remote.braille", brailleRequest(lease, 1, size)); code != 0 {
t.Fatal("size refused", code)
}
if delivery := <-deliveries; delivery.Role != "controlled" || !reflect.DeepEqual(delivery.Braille, size) || delivery.Lease != lease {
t.Fatal("size delivery changed", delivery)
}
route := tentaclewire.RemoteBraille{Op: "gesture", Gesture: "route", Index: 7}
if _, code := remoteCall(t, session, "remote.braille", brailleRequest(lease, 2, route)); code != 0 {
t.Fatal("gesture refused", code)
}
<-deliveries
cells := tentaclewire.RemoteBraille{Op: "cells", Cells: []int{1, 255, 0}}
if _, code := remoteCall(t, session, "remote.braille", brailleRequest(lease, 3, cells)); code != -32001 {
t.Fatal("controlled child sent display cells to its reader", code)
}
if err := session.PublishRemoteBraille(lease, cells); err != nil {
t.Fatal(err)
}
if event := nextSDKEvent(t, session); event.Kind != "remote.braille" || event.Braille == nil || !reflect.DeepEqual(*event.Braille, cells) {
t.Fatal("cells event changed", event)
}
if !errors.Is(session.PublishRemoteBraille(lease, route), ErrPermission) {
t.Fatal("controlled reader published a gesture")
}
session.RevokeRemote()
nextSDKEvent(t, session)
lease = speech_fixture_lease(t, session, "controller", []string{"remote-present", "braille-stream"})
if _, code := remoteCall(t, session, "remote.braille", brailleRequest(lease, 1, cells)); code != 0 {
t.Fatal("controller cells refused", code)
}
if delivery := <-deliveries; delivery.Role != "controller" || !reflect.DeepEqual(delivery.Braille.Cells, cells.Cells) {
t.Fatal("controller cells changed", delivery)
}
if _, code := remoteCall(t, session, "remote.braille", brailleRequest(lease, 2, size)); code != -32001 {
t.Fatal("controller sent a size request", code)
}
for _, value := range []tentaclewire.RemoteBraille{{Op: "geometry", Width: 32}, {Op: "gesture", Gesture: "pan-right"}} {
if err := session.PublishRemoteBraille(lease, value); err != nil {
t.Fatal(err)
}
if event := nextSDKEvent(t, session); event.Braille == nil || !reflect.DeepEqual(*event.Braille, value) {
t.Fatal("controller event changed", event)
}
}
data, _ := json.Marshal(brailleRequest(lease, 3, cells))
for _, raw := range []string{
strings.Replace(string(data), `[1,255,0]`, `[1,256,0]`, 1),
strings.Replace(string(data), `"op":"cells"`, `"op":"text"`, 1),
strings.Replace(string(data), `"op":"cells"`, `"op":"cells","width":3`, 1),
strings.Replace(string(data), `"sequence":3`, `"sequence":3,"origin":"local"`, 1),
} {
if _, code := remoteCall(t, session, "remote.braille", json.RawMessage(raw)); code != -32602 {
t.Fatal("invalid Braille admitted", code, raw)
}
}
session.RevokeRemote()
nextSDKEvent(t, session)
if !errors.Is(session.PublishRemoteBraille(lease, cells), ErrPermission) {
t.Fatal("retired lease published Braille")
}
})
}
}
func TestRemoteBrailleSDKGrantAndSaturation(t *testing.T) {
manager := test_manager(t, DefaultLimits(), nil)
session := startSessionSDK(t, manager, "go", "braille", "braille-stream")
deliveries := session.EnableRemoteBraille()
lease := speech_fixture_lease(t, session, "controlled", []string{"output-observe"})
size := tentaclewire.RemoteBraille{Op: "size", Width: 40}
if _, code := remoteCall(t, session, "remote.braille", brailleRequest(lease, 1, size)); code != -32001 {
t.Fatal("Braille admitted without grant", code)
}
session.RevokeRemote()
invokeSessionSDK(t, session, "event", nil)
lease = speech_fixture_lease(t, session, "controlled", []string{"braille-stream"})
for sequence := uint64(1); sequence <= tentaclewire.RemoteBrailleQueueSize; sequence++ {
if _, code := remoteCall(t, session, "remote.braille", brailleRequest(lease, sequence, size)); code != 0 {
t.Fatal("refused before saturation", code)
}
}
if _, code := remoteCall(t, session, "remote.braille", brailleRequest(lease, 99, size)); code != -32002 {
t.Fatal("saturation did not fail", code)
}
if _, err := session.RemoteContext(lease, "braille-stream"); err == nil || len(deliveries) != 0 {
t.Fatal("saturation left the lease or queue")
}
}
+1
View File
@@ -79,6 +79,7 @@ type Session struct {
remoteSpeech chan RemoteSpeechDelivery
remoteKeys chan RemoteKeyDelivery
remoteCapture chan RemoteFocusDelivery
remoteBraille chan RemoteBrailleDelivery
}
func (session *Session) Ready() <-chan struct{} { return session.ready }