Refusing Caps, Num and Scroll Lock retired all remote input, so a controller using Caps Lock as its screen-reader modifier lost keyboard control on the first press. Lock keys are now owned like other keys and released once at retirement without another toggle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.3 KiB
Quarantined Remote destination-key prerequisite
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. The source-built helper
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 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
One ledger belongs to one serial native owner and helper lifetime. Its caller must validate the original lease immediately before each effect, retire on cancellation, and finish cleanup before allowing replacement input. The ledger itself neither issues consent nor checks lease epochs. It must not be shared between leases or mutated concurrently.
A new logical down resolves one destination code and rejects invalid codes, another owned name resolving to that code, or an already-held server key. It reserves the code before attempting the down, because a native failure can leave the outcome uncertain. Duplicate downs are idempotent and perform no additional injection; native repeat fidelity remains unfinished. An unowned up performs no native operation. An owned up uses its reserved code without resolving the name again, including after layout/Num Lock changes.
Mapping failure, collision, capacity overflow or native failure stops admission.
Retire attempts every owned up in reverse down order. Successful ups are
removed; failed ups retain their exact code and original relative order for
explicit cleanup retry. Retirement never resumes admission. Backend errors
become constant local errors, without native diagnostic text or key names.
The native wrapper closes its connection only after successful ledger cleanup. Failed cleanup leaves the connection and pending identities available for retry. 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 demonstrates that 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
The logical-key vocabulary now belongs to input.ValidRemoteKeyName; the wire
and Go SDK validators delegate to it, preserving the existing IPC schema and
Python conformance. Native input therefore uses the same bounded names as the
public SDK, without depending on the Tentacle wire package.
The backend fetches a fresh XKB map/state for each new resolution and requires
one unambiguous matching destination code. It supports group zero only;
nonzero groups are unavailable. Shift/Caps state is removed for logical base-key
resolution while Num Lock is retained. Shift-only punctuation, unavailable or
ambiguous symbols are rejected. It does not synthesize modifiers, assume a
Windows scan code equals an X code, or rewrite the global keymap. The private
fixture changes maps and lock state explicitly to exercise these cases.
See the XKB library specification
for XkbGetMap, XkbGetState and XkbTranslateKeyCode.
(Superseded: see Lock keys below.) Lock-key downs were unavailable until lock synchronization and reader modifier handling have a defined policy. This includes Caps Lock even when a reader configuration would otherwise use it as a modifier. AltGr, IME/dead keys, general group/level handling and exact remote autorepeat remain unfinished.
The already-held-key check is not atomic with injection. A later physical or other-client press of an owned code is not separately represented by the global bitmap. This backend cannot preserve that press during cleanup. Fresh map reads also do not freeze configuration changes by other clients. No physical-device, 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 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
From the reader checkout:
go test -race ./internal/input ./internal/tentaclewire -count=1
python3 -B tests/native_remote_keys.py
python3 -B tests/native_remote_input.py
The new harness requires Go, Python, Xvfb, private D-Bus and setxkbmap. It starts
a 24-bit Xvfb server before private D-Bus using native_test_session.py, with a
disposable runtime directory. It never uses the active reader, display or
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 owned modifier cleanup, preservation of an already-held key from another synthetic client, US/German map replacement, Num Lock changes, unsupported-group and shift-only-symbol rejection, and lock-key refusal. They inspect server key state, not application command delivery or hardware behavior.
The new native harness, existing ten-experiment/six-native-test feasibility
harness, input/wire/Xorg race suites, focused host keyboard/lifecycle race tests,
external Remote race suite, affected-package vet, a separate reader build,
the existing speech regression harness and Tentacles make check passed.
No full reader suite, installed-binary behavior,
audible speech, physical input/Braille or live NVDA/Orca acceptance was performed.
Lock keys (2026-11-06)
Caps Lock, Num Lock and Scroll Lock are now owned like other keys instead of being refused. Refusal retired all remote input, so a controller using Caps Lock as its NVDA or Cthulhu modifier lost keyboard control on the first press. A remote press and release toggles the lock here, as the same physical key would. Retirement releases a held lock key once and never re-toggles a lock; when Caps Lock is the controlled reader's Cthulhu modifier, the command owner keeps its local lock state. A private Xvfb test checks that each lock key is owned and released at retirement. Lock-state synchronization between computers is not attempted.