Add independent Remote input helper with EOF and watchdog cleanup

This commit is contained in:
ak47 committed 2026-11-05 22:35:40 -05:00
1 parent cd9ae05bbe
commit 21d8904ee3
13 files changed
+1501 -20

No files matched your search

+5 -3
View File
@@ -109,10 +109,12 @@ contract. None of these tests injects a desktop key.
An unconnected [destination-key prerequisite](testing/remote-key-target.md) now
retains native codes through layout changes and uncertain effects, with a
quarantined Xorg resolver/backend. It is not attached to this mailbox and supplies
no independent process/watchdog cleanup or physical collision guarantee.
no physical collision guarantee. A subsequent [source-built helper](testing/remote-input-helper.md)
now supplies private process-lifetime/EOF/watchdog cleanup, but no production
reader launch or mailbox owner is attached.
No capture event API, production Xorg input owner, independent native cleanup
helper, core escape/bypass integration or installed Remote keyboard command is
No capture event API, production Xorg input owner, supervised helper attachment,
core escape/bypass integration or installed Remote keyboard command is
provided. Physical-source approval, physical collision/recovery, audible speech,
Braille and real NVDA/Orca interoperability remain unverified. Private IPC and
relay checks close no live release gate.
+150
View File
@@ -0,0 +1,150 @@
# Source-built Remote input helper prerequisite
`cmd/cthulhu-input-helper` now runs the quarantined Xorg destination target in
its own process. `internal/remoteinput` supplies bounded private IPC, a separate
EOF lifeline, and a three-second heartbeat watchdog. The helper owns its X
connection and stored destination-key ledger on one locked OS thread.
No production reader owner launches this helper or attaches `remote.key`.
Remote's UI still offers experimental speech only and discards keyboard traffic.
The ordinary build/install script still builds the reader and Braille helper;
this additional executable is source-built by the private test harness only.
It has no manifest, catalog entry or service-manager dependency.
## Descriptors and process lifetime
The private launcher must supply three inherited anonymous pipe directions:
| Descriptor | Direction | Purpose |
| --- | --- | --- |
| stdin | Helper reads | Fixed operation records |
| stdout | Helper writes | Fixed acknowledgements |
| 3 | Helper reads | Empty lifetime pipe; EOF cancels the owner |
The launcher retains the sole write ends of control and lifetime. Neither the
helper nor unrelated descendants may inherit these write ends. The lifetime
pipe carries no bytes: any data is a protocol failure. Its EOF is observed
independently of control buffering, so a queued or incomplete key record cannot
postpone ownership cancellation until the control stream drains. Cancellation
is checked before every queued effect and after native calls return.
The helper requires `--epoch` with a nonzero uint64. That epoch is bookkeeping,
not consent or a secret. A future reader launcher must bind it to the original
approved controlled lease, check `keyboard-inject` and policy at delivery, and
keep all three pipes away from the network Tentacle. Possession of an inherited
pipe is not an OS security boundary against arbitrary same-user processes.
Terminals, regular files and wrong-direction pipe descriptors are rejected.
The helper adopts owned nonblocking pipe duplicates so Go's poller can interrupt blocked
I/O on Close, including inherited stdin/stdout. No key names, raw frames, native
diagnostics, channel keys or parser/argument excerpts are printed. Stdout is
reserved for the private acknowledgement records; failure stderr is constant.
The future launcher must put the helper outside the writer/Tentacle process
group. The private harness explicitly uses separate groups. Parent failure
must close the lifeline and allow cleanup; it must not kill the helper before
cleanup. Explicit cancellation can also use SIGTERM/SIGINT, which cancel the
helper context and attempt orderly retirement. SIGKILL of the helper itself
still cannot clean up XTEST keys.
## Private protocol version 1
All integers use unsigned big-endian representation. The frame version and all
reserved/padding bytes are validated. There is no arbitrary JSON/text dispatch,
network endpoint, path, script name or synthetic modifier fallback.
An operation is exactly 52 bytes:
| Bytes | Field |
| --- | --- |
| 0 | Version 1 |
| 1 | Kind: heartbeat=1, down=2, up=3, close=4 |
| 2 | Key-name byte length, 0–32 |
| 3 | Reserved zero |
| 4–11 | Original fixed epoch |
| 12–19 | Strictly increasing positive operation sequence |
| 20–51 | Canonical key name followed by zero padding |
Down/up require a name accepted by `input.ValidRemoteKeyName`. Heartbeat/close
require an empty name. A different epoch, replay, unsupported version/kind,
invalid name, nonzero padding or incomplete final record retires the helper.
The sequence is private to this helper and must be ordered separately from
public Tentacle RPC sequences. A running helper never accepts a new epoch.
An acknowledgement is exactly 20 bytes:
| Bytes | Field |
| --- | --- |
| 0 | Version 1 |
| 1 | Kind: ready=1, accepted=2, closed=3 |
| 2 | Closed reason; zero for other acknowledgement kinds |
| 3 | Cleanup-failed flag, 0 or 1; zero except on closed |
| 4–11 | Fixed epoch |
| 12–19 | Operation sequence; ready uses zero |
Closed reasons are explicit close=1, EOF=2, expiry=3, protocol failure=4, target
failure=5, output failure=6 and context cancellation=7. The terminal sequence is
the last processed operation, which can include a failed native attempt.
Accepted confirms operation processing; a duplicate down/unowned up can be an
idempotent native no-op. It does not confirm application/reader command delivery.
Close has no preliminary Accepted response. Only Closed with cleanup-failed=0
confirms successful native retirement; missing confirmation, failed cleanup or
an uncertain process exit must block replacement input. There is no reconnect
or automatic restart that restores consent or held state.
## Watchdog, backpressure and cleanup
Only a valid heartbeat received before the current deadline renews the fixed
three-second watchdog. Key traffic, partial input and ACK writes do not renew
it. The owner checks the absolute deadline before each frame and after native
effects, so busy traffic cannot starve expiry and a late heartbeat cannot revive
an expired helper. Parent cancellation, lifetime EOF, malformed input or output
failure also stop admission independently of control queue delivery.
One read worker retains at most a queued operation and one frame being read.
One write worker owns a 32-record ACK queue plus one in-flight record. Native
effects remain on the main owner. Full output capacity retires the helper;
acknowledgements are not silently lost while native input continues. Cleanup
runs before attempting the terminal ACK, including with stdout stalled. After
cleanup, final ACK flushing is bounded by 250 milliseconds, then owned pipe
directions close and all I/O workers join.
Native target failure triggers stored-code retirement. Failed retirement is
reported with cleanup-failed=1 and a nonzero exit, rather than a false clean
acknowledgement. The existing target ledger retains failed identities while
alive; a parent must treat helper failure as uncertain and refuse fresh input.
This helper does not solve recovery from its own death or a broken X server.
## Evidence and remaining gates
Run `python3 -B tests/native_remote_keys.py` from the reader checkout. It builds
the actual helper with the race detector in a disposable directory and starts
24-bit Xvfb before private D-Bus. It never uses the active display or reader.
The supervising Go test owns a synthetic writer and the helper separately, so
it can inspect key state and reap both after writer SIGKILL. The writer alone
holds both write ends. This models writer/descriptor failure; it is not a
production reader launch or actual reader-SIGKILL test.
Pure helper tests cover exact framing, fragmentation, stale epochs/sequences,
reserved fields, lifetime data/EOF, cancellation of queued downs, blocked ACKs,
key-only expiry, cleanup failure reporting, private errors and interruptible
inherited reads. Eight private native process scenarios cover explicit close,
writer SIGKILL, writer SIGSTOP with watchdog expiry, SIGTERM during a partial
frame, replay, wrong epoch, successful heartbeat renewal beyond three seconds,
and a full real ACK pipe. A separate X connection verifies owned Shift/Control
are released. The six destination-target native tests remain regressions.
Production reader launch/supervision, mailbox ownership and retirement barriers
are still absent. Physical same-code collisions, the check/inject race, input
provenance, lock/AltGr/group/repeat policy and local escape/bypass remain open.
The watchdog and signals cannot interrupt an Xlib call that blocks inside the
native target; the private results require a responsive server. No hard desktop
recovery bound, physical keyboard/Braille, audible speech, installed-package or
live NVDA/Orca acceptance is established. Keyboard control remains unavailable.
The helper/input/wire/Xorg race suites, both private native harnesses, complete
external Remote race suite, affected-package vet, separate reader build,
existing speech regression harness and Tentacles `make check` passed. The
repository check now includes private helper protocol/lifecycle tests and
compiles the helper command, without launching it on a desktop. No full reader
suite or active/installed-reader replacement was performed.
+16 -10
View File
@@ -3,13 +3,15 @@
The reader has a destination-key ledger and an unconnected Xorg backend.
`internal/input/remote_key_ledger.go` owns at most 32 native destination codes;
`internal/platform/xorg/remote_keys.go` resolves and applies the supported
logical keys on a dedicated X connection. No production code constructs this
target, and no reader input owner is attached to `remote.key`. The Remote
logical keys on a dedicated X connection. The [source-built helper](remote-input-helper.md)
and private fixtures construct this target; no reader owner launches that helper
or attaches `remote.key`. The Remote
command still discards relay keyboard traffic and offers no keyboard grant.
This is a native prerequisite, not a usable input service. It must remain
quarantined until independent helper cleanup, input provenance and physical
collision handling are established.
quarantined until production helper supervision, input provenance and physical
collision handling are established. The source helper now demonstrates
responsive-server EOF/watchdog cleanup in private process tests.
## Ownership and cleanup
@@ -39,8 +41,9 @@ Opening replacement input must wait for confirmed old cleanup; a new consent
epoch does not make an old native release harmless. Closing an XTEST connection
or killing this process does not release held keys. The earlier
[Python feasibility harness](remote-input-feasibility.md) demonstrates that
limitation; its separate EOF/watchdog mechanism is not implemented by this Go
target.
limitation. The ledger alone has no EOF/watchdog mechanism; the separate Go
helper now supplies that process-lifetime owner. Helper death and blocked-server
recovery remain unresolved.
## Native resolution and limits
@@ -72,10 +75,11 @@ concurrent-map-change or same-code collision guarantee is claimed.
All native wrapper methods require one serial caller on a locked OS thread.
Xlib calls and the error guard can block or terminate a process after X-server
failure. The future production target must run in an independent helper, with
bounded inherited-descriptor IPC, watchdog/EOF cleanup, exclusive ownership,
provenance, policy cancellation and explicit failure reporting. Do not attach
this synchronous wrapper to the reader event loop or directly to its mailbox.
failure. The source-built helper now provides independent execution, bounded
inherited-descriptor IPC and watchdog/EOF cleanup. Production launch must preserve
exclusive ownership, provenance, policy cancellation and explicit failure
reporting. Do not attach this synchronous wrapper to the reader event loop or
directly to its mailbox.
## Private verification
@@ -94,6 +98,8 @@ accessibility bus. Missing dependencies fail the harness rather than count as
skipped acceptance. The native tests skip in ordinary unflagged Go suites;
only an actual harness run establishes their private results.
The harness now builds and runs the source helper and tests its private process
lifetime in addition to the destination target; see the linked helper contract.
Unit tests cover stored-code releases, idempotent downs, unowned ups, alias and
preexisting collisions, constant errors, held-key limits, uncertain down/up
outcomes, partial cleanup and retry order. Six new native Go race tests verify