Files
Chase CichorzandClaude Opus 5.5 2651b5e9bd 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>
2026-11-06 16:06:38 -05:00

141 lines
5.3 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package tentaclewire
import "slices"
const (
SessionQueueSize = 32
SessionQueueBytes = 256 * 1024
)
// RemoteGrants are independent install-time permissions, never session consent.
func RemoteGrants() []string {
return []string{"output-observe", "remote-present", "keyboard-capture", "keyboard-inject", "braille-stream", "clipboard-exchange"}
}
func ValidRemoteGrants(grants []string) bool {
seen := map[string]bool{}
for _, grant := range grants {
if seen[grant] || !slices.Contains(RemoteGrants(), grant) {
return false
}
seen[grant] = true
}
return true
}
// ValidNegotiation keeps the 1.0 prefix contract. In 1.1 APIs are sets and
// capabilities are permissions: commands and transport APIs confer no access.
func ValidNegotiation(minor int, apis, capabilities []string) bool {
if minor == 0 {
return ValidAPIs(apis, capabilities)
}
if minor != 1 {
return false
}
a, c := map[string]bool{}, map[string]bool{}
for _, api := range apis {
if a[api] || !slices.Contains([]string{"commands", "presentation", "interaction", "reader-settings", "session-events", "remote-access"}, api) {
return false
}
a[api] = true
}
for _, capability := range capabilities {
if c[capability] || !(slices.Contains(RemoteGrants(), capability) || slices.Contains([]string{"presentation", "interaction", "reader-settings"}, capability)) {
return false
}
c[capability] = true
}
if !a["commands"] || !a["session-events"] || a["interaction"] && !a["presentation"] || a["reader-settings"] && !a["interaction"] {
return false
}
for _, api := range []string{"presentation", "interaction", "reader-settings"} {
if a[api] != c[api] {
return false
}
}
hasRemote := false
for _, grant := range RemoteGrants() {
hasRemote = hasRemote || c[grant]
}
return a["remote-access"] == hasRemote
}
// SessionEvent carries lifecycle metadata and explicitly typed output. Output
// observation still requires a reader owner and a live role-bound lease.
type SessionEvent struct {
Sequence uint64 `json:"sequence"`
Kind string `json:"kind"`
Action string `json:"action,omitempty"`
Lease string `json:"lease,omitempty"`
Epoch uint64 `json:"epoch"`
Role string `json:"role,omitempty"`
Grants []string `json:"grants,omitempty"`
Reason string `json:"reason,omitempty"`
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" }
func ValidRoleGrants(role string, grants []string) bool {
if !ValidRole(role) || !ValidRemoteGrants(grants) || len(grants) == 0 {
return false
}
for _, grant := range grants {
if role == "controller" && (grant == "output-observe" || grant == "keyboard-inject") || role == "controlled" && (grant == "remote-present" || grant == "keyboard-capture") {
return false
}
}
return true
}
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 != "" || event.Kind != "remote.braille" && event.Braille != nil {
return false
}
switch event.Kind {
case "remote.action":
return event.Speech == nil && len(event.Action) == 32 && event.Lease == "" && event.Reason == "" && ValidRoleGrants(event.Role, event.Grants)
case "remote.revoked":
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":
return event.Action == "" && len(event.Lease) == 32 && event.Role == "" && len(event.Grants) == 0 && event.Reason == "" && event.Speech != nil && ValidRemoteSpeech(*event.Speech)
}
return false
}
type RemoteOpen struct {
Action string `json:"action"`
Epoch uint64 `json:"epoch"`
Role string `json:"role"`
Grants []string `json:"grants"`
}
type RemoteLease struct {
Lease string `json:"lease"`
Epoch uint64 `json:"epoch"`
}
type RemoteOperation struct {
Lease string `json:"lease"`
Epoch uint64 `json:"epoch"`
Sequence uint64 `json:"sequence"`
}