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
@@ -0,0 +1,24 @@
|
||||
# Remote Braille IPC
|
||||
|
||||
Protocol 1.1 carries Remote Braille through one child-to-host method,
|
||||
`remote.braille`, and one host-to-child event kind of the same name. Both need
|
||||
the current process session's live lease with the separately approved
|
||||
`braille-stream` grant; requests share the operation sequence. Every message
|
||||
carries one `braille` object whose `op` selects its form:
|
||||
|
||||
| `op` | Direction and role | Fields |
|
||||
| --- | --- | --- |
|
||||
| `size` | controlled child to host | `width` 0–4,096: the controller's display; 0 stops export |
|
||||
| `gesture` | controlled child to host; host to controller child | `gesture` `pan-left`, `pan-right` or `route`, with `index` for route |
|
||||
| `cells` | host to controlled child; controller child to host | `cells`: 1–4,096 final dot values 0–255, never text |
|
||||
| `geometry` | controller child to host (no width); host reply event with `width` | 0 means no local display |
|
||||
|
||||
The request reply acknowledges admission into a 16-item reader mailbox; a full
|
||||
mailbox revokes the lease. Without an attached reader owner the method returns
|
||||
owner-unavailable. No AT-SPI identity, object path, routing map or native
|
||||
handle crosses this boundary: routes are cell indexes into the latest exported
|
||||
frame only. `Session.EnableRemoteBraille`, `PublishRemoteBraille`, the Go
|
||||
`RemoteBraille` types and validators and the Python `valid_remote_braille` and
|
||||
`valid_remote_braille_request` validators implement the contract. Go/Python
|
||||
SDK-process tests cover each direction, role denial, invalid values,
|
||||
saturation and revocation.
|
||||
@@ -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 }
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package tentaclewire
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
RemoteBrailleMaxCells = 4096
|
||||
RemoteBrailleQueueSize = 16
|
||||
)
|
||||
|
||||
// RemoteBraille carries one Braille operation. Exactly one form is valid per
|
||||
// direction and role:
|
||||
//
|
||||
// - "size" (controlled child to host): Width is the controller's display
|
||||
// width; zero stops export.
|
||||
// - "gesture" (controlled child to host, or host to controller child):
|
||||
// Gesture is "pan-left", "pan-right" or "route"; Index is the route cell.
|
||||
// - "cells" (controller child to host, or host to controlled child): final
|
||||
// dot cells, 1 to 4,096 values of 0 to 255, never text to translate.
|
||||
// - "geometry" (controller child to host asks; host to controller child
|
||||
// answers with Width, zero meaning no local display).
|
||||
type RemoteBraille struct {
|
||||
Op string `json:"op"`
|
||||
Width int `json:"width,omitempty"`
|
||||
Gesture string `json:"gesture,omitempty"`
|
||||
Index int `json:"index,omitempty"`
|
||||
Cells []int `json:"cells,omitempty"`
|
||||
}
|
||||
|
||||
type RemoteBrailleRequest struct {
|
||||
RemoteOperation
|
||||
Braille RemoteBraille `json:"braille"`
|
||||
}
|
||||
|
||||
func validCells(cells []int) bool {
|
||||
if len(cells) == 0 || len(cells) > RemoteBrailleMaxCells {
|
||||
return false
|
||||
}
|
||||
for _, cell := range cells {
|
||||
if cell < 0 || cell > 255 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ValidRemoteBraille checks representation only. Role and direction are
|
||||
// checked by ValidRemoteBrailleFor.
|
||||
func ValidRemoteBraille(value RemoteBraille) bool {
|
||||
switch value.Op {
|
||||
case "size":
|
||||
return value.Width >= 0 && value.Width <= RemoteBrailleMaxCells && value.Gesture == "" && value.Index == 0 && value.Cells == nil
|
||||
case "geometry":
|
||||
return value.Width >= 0 && value.Width <= RemoteBrailleMaxCells && value.Gesture == "" && value.Index == 0 && value.Cells == nil
|
||||
case "gesture":
|
||||
switch value.Gesture {
|
||||
case "pan-left", "pan-right":
|
||||
return value.Index == 0 && value.Width == 0 && value.Cells == nil
|
||||
case "route":
|
||||
return value.Index >= 0 && value.Index < RemoteBrailleMaxCells && value.Width == 0 && value.Cells == nil
|
||||
}
|
||||
return false
|
||||
case "cells":
|
||||
return validCells(value.Cells) && value.Width == 0 && value.Gesture == "" && value.Index == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidRemoteBrailleFor checks direction: request is true for child-to-host.
|
||||
func ValidRemoteBrailleFor(role string, request bool, value RemoteBraille) bool {
|
||||
if !ValidRemoteBraille(value) {
|
||||
return false
|
||||
}
|
||||
switch {
|
||||
case role == "controlled" && request:
|
||||
return value.Op == "size" || value.Op == "gesture"
|
||||
case role == "controlled":
|
||||
return value.Op == "cells"
|
||||
case role == "controller" && request:
|
||||
return value.Op == "cells" || value.Op == "geometry" && value.Width == 0
|
||||
case role == "controller":
|
||||
return value.Op == "gesture" || value.Op == "geometry"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ValidRemoteBrailleRequest(request RemoteBrailleRequest) bool {
|
||||
return len(request.Lease) == 32 && request.Epoch != 0 && request.Sequence != 0 && ValidRemoteBraille(request.Braille)
|
||||
}
|
||||
|
||||
func (value *RemoteBraille) UnmarshalJSON(data []byte) error {
|
||||
var fields map[string]json.RawMessage
|
||||
if DecodeStrict(data, &fields) != nil || fields["op"] == nil {
|
||||
return ErrProtocol
|
||||
}
|
||||
type plain RemoteBraille
|
||||
var decoded plain
|
||||
if DecodeStrict(data, &decoded) != nil || !ValidRemoteBraille(RemoteBraille(decoded)) {
|
||||
return ErrProtocol
|
||||
}
|
||||
*value = RemoteBraille(decoded)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (request *RemoteBrailleRequest) UnmarshalJSON(data []byte) error {
|
||||
var fields map[string]json.RawMessage
|
||||
if DecodeStrict(data, &fields) != nil || len(fields) != 4 || fields["lease"] == nil || fields["epoch"] == nil ||
|
||||
fields["sequence"] == nil || fields["braille"] == nil {
|
||||
return ErrProtocol
|
||||
}
|
||||
type plain RemoteBrailleRequest
|
||||
var decoded plain
|
||||
if DecodeStrict(data, &decoded) != nil || !ValidRemoteBrailleRequest(RemoteBrailleRequest(decoded)) {
|
||||
return ErrProtocol
|
||||
}
|
||||
*request = RemoteBrailleRequest(decoded)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package tentaclewire
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRemoteBrailleDirections(t *testing.T) {
|
||||
size := RemoteBraille{Op: "size", Width: 40}
|
||||
route := RemoteBraille{Op: "gesture", Gesture: "route", Index: 3}
|
||||
cells := RemoteBraille{Op: "cells", Cells: []int{0, 255}}
|
||||
query := RemoteBraille{Op: "geometry"}
|
||||
answer := RemoteBraille{Op: "geometry", Width: 32}
|
||||
for _, test := range []struct {
|
||||
role string
|
||||
request bool
|
||||
value RemoteBraille
|
||||
want bool
|
||||
}{
|
||||
{"controlled", true, size, true}, {"controlled", true, route, true}, {"controlled", true, cells, false},
|
||||
{"controlled", false, cells, true}, {"controlled", false, route, false},
|
||||
{"controller", true, cells, true}, {"controller", true, query, true}, {"controller", true, answer, false},
|
||||
{"controller", true, size, false}, {"controller", false, route, true}, {"controller", false, answer, true},
|
||||
{"controller", false, cells, false}, {"observer", true, size, false},
|
||||
} {
|
||||
if got := ValidRemoteBrailleFor(test.role, test.request, test.value); got != test.want {
|
||||
t.Fatalf("%s request=%t %+v = %t", test.role, test.request, test.value, got)
|
||||
}
|
||||
}
|
||||
for _, raw := range []string{
|
||||
`{"op":"cells","cells":[]}`, `{"op":"cells","cells":[-1]}`, `{"op":"size","width":4097}`,
|
||||
`{"op":"gesture","gesture":"route","index":4096}`, `{"op":"gesture","gesture":"script"}`,
|
||||
`{"op":"gesture","gesture":"pan-left","index":1}`, `{"width":3}`, `{"op":"size","width":3,"text":"x"}`,
|
||||
} {
|
||||
var value RemoteBraille
|
||||
if json.Unmarshal([]byte(raw), &value) == nil {
|
||||
t.Fatal("invalid Braille decoded", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ type SessionEvent struct {
|
||||
Speech *RemoteSpeech `json:"speech,omitempty"`
|
||||
Input *RemoteCapturedKey `json:"input,omitempty"`
|
||||
Focus string `json:"focus,omitempty"`
|
||||
Braille *RemoteBraille `json:"braille,omitempty"`
|
||||
}
|
||||
|
||||
func ValidRole(role string) bool { return role == "controller" || role == "controlled" }
|
||||
@@ -97,7 +98,7 @@ func ValidSessionEvent(event SessionEvent) bool {
|
||||
if event.Sequence == 0 || event.Epoch == 0 {
|
||||
return false
|
||||
}
|
||||
if event.Kind != "remote.input" && event.Input != nil || event.Kind != "remote.focus" && event.Focus != "" {
|
||||
if event.Kind != "remote.input" && event.Input != nil || event.Kind != "remote.focus" && event.Focus != "" || event.Kind != "remote.braille" && event.Braille != nil {
|
||||
return false
|
||||
}
|
||||
switch event.Kind {
|
||||
@@ -107,6 +108,11 @@ func ValidSessionEvent(event SessionEvent) bool {
|
||||
return event.Speech == nil && ((event.Action == "" && len(event.Lease) == 32) || (len(event.Action) == 32 && event.Lease == "")) && event.Role == "" && len(event.Grants) == 0 && slices.Contains([]string{"closed", "expired", "local", "replaced"}, event.Reason)
|
||||
case "remote.input":
|
||||
return event.Action == "" && len(event.Lease) == 32 && event.Role == "" && len(event.Grants) == 0 && event.Reason == "" && event.Speech == nil && event.Input != nil && ValidRemoteCapturedKey(*event.Input)
|
||||
case "remote.braille":
|
||||
// Role is not carried on events; either direction-valid form is accepted
|
||||
// and the receiving child applies its own role.
|
||||
return event.Action == "" && len(event.Lease) == 32 && event.Role == "" && len(event.Grants) == 0 && event.Reason == "" && event.Speech == nil && event.Braille != nil &&
|
||||
(ValidRemoteBrailleFor("controlled", false, *event.Braille) || ValidRemoteBrailleFor("controller", false, *event.Braille))
|
||||
case "remote.focus":
|
||||
return event.Action == "" && len(event.Lease) == 32 && event.Role == "" && len(event.Grants) == 0 && event.Speech == nil && ValidRemoteFocusEvent(event.Focus, event.Reason)
|
||||
case "remote.output":
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package tentacle
|
||||
|
||||
import "git.stormux.org/storm/cthulhu/internal/tentaclewire"
|
||||
|
||||
type RemoteBraille = tentaclewire.RemoteBraille
|
||||
type RemoteBrailleRequest = tentaclewire.RemoteBrailleRequest
|
||||
|
||||
func ValidRemoteBraille(value RemoteBraille) bool { return tentaclewire.ValidRemoteBraille(value) }
|
||||
|
||||
func ValidRemoteBrailleRequest(request RemoteBrailleRequest) bool {
|
||||
return tentaclewire.ValidRemoteBrailleRequest(request)
|
||||
}
|
||||
@@ -166,6 +166,48 @@ def valid_remote_focus_request(request):
|
||||
return False
|
||||
|
||||
|
||||
def valid_remote_braille(value, role=None, request=None):
|
||||
"""Validate one Remote Braille operation; role/request also check direction."""
|
||||
try:
|
||||
_fields(value, ("op",), ("width", "gesture", "index", "cells"))
|
||||
op = value["op"]
|
||||
width, gesture, index = value.get("width", 0), value.get("gesture", ""), value.get("index", 0)
|
||||
cells = value.get("cells")
|
||||
if type(width) is not int or type(index) is not int or not isinstance(gesture, str):
|
||||
return False
|
||||
if op in ("size", "geometry"):
|
||||
ok = 0 <= width <= 4096 and gesture == "" and index == 0 and cells is None
|
||||
elif op == "gesture":
|
||||
ok = (width == 0 and cells is None
|
||||
and (gesture in ("pan-left", "pan-right") and index == 0
|
||||
or gesture == "route" and 0 <= index < 4096))
|
||||
elif op == "cells":
|
||||
ok = (isinstance(cells, list) and 1 <= len(cells) <= 4096
|
||||
and all(type(cell) is int and 0 <= cell <= 255 for cell in cells)
|
||||
and width == 0 and gesture == "" and index == 0)
|
||||
else:
|
||||
return False
|
||||
if not ok or role is None:
|
||||
return ok
|
||||
if role == "controlled":
|
||||
return op in ("size", "gesture") if request else op == "cells"
|
||||
if role == "controller":
|
||||
return (op == "cells" or op == "geometry" and width == 0) if request else op in ("gesture", "geometry")
|
||||
return False
|
||||
except (ProtocolError, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def valid_remote_braille_request(request):
|
||||
"""Validate remote.braille parameters; display effects remain owned by core."""
|
||||
try:
|
||||
_fields(request, ("lease", "epoch", "sequence", "braille"))
|
||||
return (_token(request["lease"]) and _positive(request["epoch"])
|
||||
and _positive(request["sequence"]) and valid_remote_braille(request["braille"]))
|
||||
except (ProtocolError, TypeError, ValueError, UnicodeError):
|
||||
return False
|
||||
|
||||
|
||||
REMOTE_FOCUS_REASONS = frozenset(("requested", "escape", "local", "unavailable"))
|
||||
|
||||
|
||||
@@ -358,11 +400,12 @@ class SessionClient:
|
||||
waiter.put_nowait(value)
|
||||
|
||||
def _event(self, event):
|
||||
_fields(event, ("sequence", "kind", "epoch"), ("action", "lease", "role", "grants", "reason", "speech", "input", "focus"))
|
||||
_fields(event, ("sequence", "kind", "epoch"), ("action", "lease", "role", "grants", "reason", "speech", "input", "focus", "braille"))
|
||||
if not _positive(event["sequence"]) or not _positive(event["epoch"]):
|
||||
raise ProtocolError("Invalid event sequence")
|
||||
if (event["kind"] != "remote.input" and "input" in event
|
||||
or event["kind"] != "remote.focus" and event.get("focus", "") != ""):
|
||||
or event["kind"] != "remote.focus" and event.get("focus", "") != ""
|
||||
or event["kind"] != "remote.braille" and "braille" in event):
|
||||
raise ProtocolError("Invalid session event")
|
||||
if event["kind"] == "remote.action":
|
||||
grants = event.get("grants")
|
||||
@@ -386,6 +429,14 @@ class SessionClient:
|
||||
or event.get("reason", "") != "" or "speech" in event
|
||||
or not valid_remote_captured_key(event.get("input"))):
|
||||
raise ProtocolError("Invalid input event")
|
||||
elif event["kind"] == "remote.braille":
|
||||
braille = event.get("braille")
|
||||
if (not _token(event.get("lease")) or event.get("action", "") != ""
|
||||
or event.get("role", "") != "" or event.get("grants", []) != []
|
||||
or event.get("reason", "") != "" or "speech" in event
|
||||
or not (valid_remote_braille(braille, "controlled", False)
|
||||
or valid_remote_braille(braille, "controller", False))):
|
||||
raise ProtocolError("Invalid Braille event")
|
||||
elif event["kind"] == "remote.focus":
|
||||
if (not _token(event.get("lease")) or event.get("action", "") != ""
|
||||
or event.get("role", "") != "" or event.get("grants", []) != []
|
||||
|
||||
Reference in new issue
Block a user