feat(kvm): layout-aware desktop key protocol #127

Merged
David merged 1 commit from feat/VA-126-layout-aware-desktop-keys into main 2026-07-11 02:30:29 +02:00
Owner

What

Replaces the layout-fragile Windows-virtual-key desktop key frame with a layout-aware key contract so the number row, shifted digits, and punctuation reproduce from a macOS or other non-US client. This is the agent side of VA-126; VAPP-78 is the client (SPA) side.

The old frame ([0,1,0,6, action, vk]) carried only a deprecated event.keyCode byte, whose Windows OEM codes are layout-dependent, and no modifier state, so the agent had to reconstruct shifted characters from a separately-received Shift event. That could not work for the number row / -_ =+ / punctuation off a Mac.

Wire contract (Vervain owns this number space; native extensions at 200+)

MNG_KVM_KEY_LAYOUT = 200, big-endian: [u16 cmd][u16 size][u8 action][u8 mods][u8 layout_id][u8 code_len][u8 key_len][code UTF-8][key UTF-8]. action 0=down/1=up; mods bit0 Shift / bit1 Ctrl / bit2 Alt / bit3 Meta; code = event.code; key = event.key; layout_id selects a fallback layout table when key is empty.

MNG_KVM_CLIENT_HELLO = 201 negotiates the desktop protocol version (DESKTOP_PROTOCOL_VERSION = 2): the agent replies with its version and logs a mismatch loudly. Vervain is not in production, so no legacy path is kept: the retired VK frame (cmd 1) is rejected loudly rather than silently mis-injected.

Resolution (vervain-agent-kvm::keymap)

Every backend consumes one canonical form, an X11 keysym. Resolution trusts event.key for printable input (the client already applied its own layout), maps named / function keys directly, and falls back to a per-layout physical-code table otherwise. The US and Mac-US tables (Mac US ANSI matches US across the affected keys, so it is fully knowable without a physical Mac) are the deterministic authority the unit tests assert against. Modifier state comes from the bitmask, not separate Shift events: Shift is folded into a printable keysym and synthesized only for named-key chords (e.g. Shift+ArrowLeft); a standalone modifier frame resolves to nothing.

Injection

A single serialized injector drains the keysym transitions in order, so modifier-then-key and down-then-up ordering hold regardless of backend round-trip timing (the old fire-and-forget-per-frame path could not guarantee this). inject_keysym selects uinput (evdev scancode + shift level via keysym_to_evdev), the RemoteDesktop portal (keysym direct), X11 XTEST, or the Windows SendInput path. The X11 backend now presses Shift for a shifted-level keysym (a keysym is an absolute symbol, so @ must hold Shift, not tap the 2 keycode); this also corrects the VNC-native and MCP key paths.

Completeness sweep

Invariant: every desktop key-injection backend reproduces the intended keystroke, including the Shift level of a shifted printable keysym.

Backend Site Status
uinput uinput::key_keysym compliant (added shift level)
portal portal::keyboard_keysym compliant (inherent)
X11 XTEST input.rs linux::key_event compliant (fixed shift level)
Windows windows/input.rs key_event compliant (Unicode codepoint)
MCP kvm_input_key dispatch.rs compliant (rides X11 fix)
legacy VK cmd 1 handle_desktop_input retired, rejected loudly

Acceptance criteria

  • Agent parses the event.code + event.key + modifier frame.
  • Mac number row (with/without Shift), -/_, =/+, and other punctuation reproduce (unit-tested against the Mac-US layout, no physical Mac needed).
  • Modifier state applied from the explicit bitmask, not inferred from separate Shift events.
  • Desktop protocol version negotiated; mismatch fails loudly.
  • US-layout input and shortcuts still work (Ctrl+C, named keys, function keys covered).

Tests

12 new keymap unit tests (Mac number row, punctuation, layout fallback, named/function keys, standalone-modifier suppression, printable-shift non-resynthesis, named-key shift synthesis, Ctrl shortcut hold, release ordering, keysym->evdev, keysym->name). Full just check and the pre-commit clean Docker CI build are green.

#VA-126

## What Replaces the layout-fragile Windows-virtual-key desktop key frame with a layout-aware key contract so the number row, shifted digits, and punctuation reproduce from a macOS or other non-US client. This is the agent side of VA-126; VAPP-78 is the client (SPA) side. The old frame (`[0,1,0,6, action, vk]`) carried only a deprecated `event.keyCode` byte, whose Windows OEM codes are layout-dependent, and no modifier state, so the agent had to reconstruct shifted characters from a separately-received Shift event. That could not work for the number row / `-_ =+` / punctuation off a Mac. ## Wire contract (Vervain owns this number space; native extensions at 200+) `MNG_KVM_KEY_LAYOUT = 200`, big-endian: `[u16 cmd][u16 size][u8 action][u8 mods][u8 layout_id][u8 code_len][u8 key_len][code UTF-8][key UTF-8]`. `action` 0=down/1=up; `mods` bit0 Shift / bit1 Ctrl / bit2 Alt / bit3 Meta; `code` = `event.code`; `key` = `event.key`; `layout_id` selects a fallback layout table when `key` is empty. `MNG_KVM_CLIENT_HELLO = 201` negotiates the desktop protocol version (`DESKTOP_PROTOCOL_VERSION = 2`): the agent replies with its version and logs a mismatch loudly. Vervain is not in production, so no legacy path is kept: the retired VK frame (cmd 1) is rejected loudly rather than silently mis-injected. ## Resolution (`vervain-agent-kvm::keymap`) Every backend consumes one canonical form, an X11 keysym. Resolution trusts `event.key` for printable input (the client already applied its own layout), maps named / function keys directly, and falls back to a per-layout physical-code table otherwise. The US and Mac-US tables (Mac US ANSI matches US across the affected keys, so it is fully knowable without a physical Mac) are the deterministic authority the unit tests assert against. Modifier state comes from the bitmask, not separate Shift events: Shift is folded into a printable keysym and synthesized only for named-key chords (e.g. Shift+ArrowLeft); a standalone modifier frame resolves to nothing. ## Injection A single serialized injector drains the keysym transitions in order, so modifier-then-key and down-then-up ordering hold regardless of backend round-trip timing (the old fire-and-forget-per-frame path could not guarantee this). `inject_keysym` selects uinput (evdev scancode + shift level via `keysym_to_evdev`), the RemoteDesktop portal (keysym direct), X11 XTEST, or the Windows SendInput path. The X11 backend now presses Shift for a shifted-level keysym (a keysym is an absolute symbol, so `@` must hold Shift, not tap the `2` keycode); this also corrects the VNC-native and MCP key paths. ## Completeness sweep Invariant: every desktop key-injection backend reproduces the intended keystroke, including the Shift level of a shifted printable keysym. | Backend | Site | Status | | --- | --- | --- | | uinput | `uinput::key_keysym` | compliant (added shift level) | | portal | `portal::keyboard_keysym` | compliant (inherent) | | X11 XTEST | `input.rs linux::key_event` | compliant (fixed shift level) | | Windows | `windows/input.rs key_event` | compliant (Unicode codepoint) | | MCP `kvm_input_key` | `dispatch.rs` | compliant (rides X11 fix) | | legacy VK `cmd 1` | `handle_desktop_input` | retired, rejected loudly | ## Acceptance criteria - [x] Agent parses the `event.code` + `event.key` + modifier frame. - [x] Mac number row (with/without Shift), `-`/`_`, `=`/`+`, and other punctuation reproduce (unit-tested against the Mac-US layout, no physical Mac needed). - [x] Modifier state applied from the explicit bitmask, not inferred from separate Shift events. - [x] Desktop protocol version negotiated; mismatch fails loudly. - [x] US-layout input and shortcuts still work (Ctrl+C, named keys, function keys covered). ## Tests 12 new `keymap` unit tests (Mac number row, punctuation, layout fallback, named/function keys, standalone-modifier suppression, printable-shift non-resynthesis, named-key shift synthesis, Ctrl shortcut hold, release ordering, keysym->evdev, keysym->name). Full `just check` and the pre-commit clean Docker CI build are green. #VA-126
feat(kvm): layout-aware desktop key protocol
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 5m30s
Create release / Create release from merged PR (pull_request) Has been skipped
16ddb06ade
Replace the layout-fragile Windows-virtual-key desktop key frame with a layout-aware contract so the number row, shifted digits, and punctuation reproduce from a macOS or other non-US client (VAPP-78 is the client side). The old frame carried only a deprecated `event.keyCode` byte, whose OEM codes are layout-dependent, and no modifier state, so the agent had to reconstruct shifted characters from separate Shift events.

The new frame (`MNG_KVM_KEY_LAYOUT`, cmd 200) carries the physical key (`event.code`), the produced value (`event.key`), an explicit modifier bitmask, and a layout id. `vervain-agent-kvm::keymap` resolves that into canonical X11 keysym transitions that every input backend consumes: it trusts `event.key` for printable input (the client has already applied its own layout), maps named keys and function keys directly, and falls back to a per-layout physical-code table when `event.key` is empty. The US and Mac-US tables are the deterministic authority the unit tests assert against, so a layout is validated without the physical keyboard. Modifier state comes from the bitmask, not separate Shift events: Shift is folded into a printable keysym and synthesized only for named-key chords; a standalone modifier frame resolves to nothing.

A single serialized injector drains the keysym transitions in order, so modifier-then-key and down-then-up ordering hold regardless of backend round-trip timing, which the old fire-and-forget-per-frame path could not guarantee. `inject_keysym` selects uinput (evdev scancode + shift level via `keysym_to_evdev`), the RemoteDesktop portal (keysym direct), X11 XTEST, or the Windows SendInput path. The X11 backend now presses Shift for a shifted-level keysym (a keysym is an absolute symbol, so `@` must hold Shift, not tap the `2` keycode); this also corrects the VNC-native and MCP key paths.

The desktop protocol version is negotiated via `MNG_KVM_CLIENT_HELLO` (cmd 201): the agent replies with its version and logs a mismatch loudly. Vervain is not in production, so no legacy compatibility path is kept: the retired VK frame (cmd 1) is rejected loudly rather than silently mis-injected. The kvm command-code header is corrected to state that Vervain owns this number space outright (no MeshCentral compatibility guarantee) and that native extensions live at 200+.

#VA-126
David merged commit c85667d9f8 into main 2026-07-11 02:30:29 +02:00
David deleted branch feat/VA-126-layout-aware-desktop-keys 2026-07-11 02:30:29 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-agent!127
No description provided.