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>
141 lines
5.3 KiB
Go
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"`
|
|
}
|