Add Remote session foundations and feasibility checks

Add opt-in Tentacle protocol 1.1, explicit remote grants, lifecycle IPC and expiring core leases in both SDKs. Revoke access before update preparation, including failed updates.

Add transient secret entries, shared Braille cell export and isolated Xorg recovery experiments with focused tests and documented limitations. Production consent, desktop services and live interoperability remain pending.
This commit is contained in:
ak47 committed 2026-11-03 22:33:26 -05:00
1 parent a47253d697
commit 573d15d7b4
45 files changed
+3634 -80

No files matched your search

+60 -5
View File
@@ -38,6 +38,8 @@ type Element struct {
Disabled bool `json:"disabled,omitempty"`
Required bool `json:"required,omitempty"`
MaxLength int `json:"max_length,omitempty"`
// Secret is supported only on transient entries, never preference pages.
Secret bool `json:"secret,omitempty"`
}
type Form struct {
Title string `json:"title"`
@@ -107,6 +109,11 @@ func NewSession(form Form) (*Session, error) {
// ValidatePreferences shares control validation with internal forms, while
// allowing a native, scrolling page to contain several full voice editors.
func ValidatePreferences(form Form) error {
for _, element := range form.Elements {
if element.Secret {
return errors.New("secret entries are not supported in preferences")
}
}
_, err := new_session(form, 256, 256)
return err
}
@@ -132,6 +139,9 @@ func new_session(form Form, maxItems, maxElements int) (*Session, error) {
return nil, errors.New("controls need unique IDs, labels and supported kinds")
}
ids[element.ID] = true
if element.Secret && element.Kind != Entry {
return nil, errors.New("only entries may be secret")
}
if element.MaxLength == 0 {
element.MaxLength = maxTextLength
}
@@ -190,8 +200,12 @@ func (session *Session) Presentation() Presentation {
case List:
text += fmt.Sprintf(": %s, %d of %d", element.Items[state.selected].Label, state.selected+1, len(element.Items))
case Entry:
text += ", edit: " + element.Text
cursor = len([]rune(element.Label+", edit: ")) + state.caret
prefix, value := ", edit: ", element.Text
if element.Secret {
prefix, value = ", password edit: ", strings.Repeat("*", utf8.RuneCountInString(element.Text))
}
text += prefix + value
cursor = len([]rune(element.Label+prefix)) + state.caret
if state.caret != state.anchor {
text += fmt.Sprintf(" (selected %d characters)", abs(state.caret-state.anchor))
}
@@ -205,6 +219,9 @@ func (session *Session) Presentation() Presentation {
text += ", button"
}
speech := text
if element.Secret {
speech = element.Label + ", password edit, " + characterCount(utf8.RuneCountInString(element.Text))
}
if element.Kind == List {
speech = element.Items[state.selected].Label
}
@@ -225,7 +242,12 @@ func (session *Session) Opening() Presentation {
return output
}
// Values includes plaintext secrets for the submitting caller. It must never
// be used for presentation, logging or generic settings persistence.
func (session *Session) Values() map[string]Value {
if session.closed {
return nil
}
if session.grid != nil {
return session.grid.values()
}
@@ -240,6 +262,17 @@ func (session *Session) Values() map[string]Value {
return values
}
// Close drops the editor's references to secrets on submission, cancellation
// or capture revocation. Go strings do not provide guaranteed memory erasure.
func (session *Session) Close() {
session.closed = true
for index := range session.elements {
if session.elements[index].element.Secret {
session.elements[index].element.Text = ""
}
}
}
func (session *Session) Handle(event Event) Effect {
if session.grid != nil {
return session.grid.handle(event)
@@ -248,7 +281,7 @@ func (session *Session) Handle(event Event) Effect {
return Effect{}
}
if event.Key == Escape {
session.closed = true
session.Close()
return Effect{Cancelled: true}
}
if event.Key == Tab {
@@ -372,6 +405,13 @@ func abs(value int) int {
return value
}
func characterCount(count int) string {
if count == 1 {
return "1 character"
}
return fmt.Sprintf("%d characters", count)
}
func (session *Session) edit(event Event) Effect {
state := &session.elements[session.focus]
characters := []rune(state.element.Text)
@@ -386,6 +426,11 @@ func (session *Session) edit(event Event) Effect {
case "v":
return Effect{Paste: true}
case "c", "x":
if state.element.Secret {
output := session.Presentation()
output.Speech = "Copy and cut are unavailable for password fields"
return Effect{Presentation: output}
}
if start == end {
return Effect{}
}
@@ -439,6 +484,8 @@ func (session *Session) edit(event Event) Effect {
output := session.Presentation()
if next == len(characters) {
output.Speech = "End of text"
} else if state.element.Secret {
output.Speech = "Hidden character"
} else {
output.Speech = string(characters[next])
}
@@ -462,7 +509,11 @@ func (session *Session) edit(event Event) Effect {
state.element.Text = string(append(characters[:start], characters[end:]...))
state.caret, state.anchor = start, start
output := session.Presentation()
output.Speech = "Deleted " + removed
if state.element.Secret {
output.Speech = "Deleted " + characterCount(end-start)
} else {
output.Speech = "Deleted " + removed
}
return Effect{Presentation: output}
}
if event.Key == Text && event.Text != "" && !event.Control {
@@ -501,6 +552,10 @@ func (session *Session) Insert(text string) Effect {
state.element.Text = string(characters[:start]) + text + string(characters[end:])
state.caret, state.anchor = start+len(inserted), start+len(inserted)
output := session.Presentation()
output.Speech = text
if state.element.Secret {
output.Speech = "Inserted " + characterCount(len(inserted))
} else {
output.Speech = text
}
return Effect{Presentation: output}
}