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:
1 parent
477d860fb8
commit
2651b5e9bd
10 files changed
+476
-3
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
Reference in new issue
Block a user