Architecture story
PR #225 · Finalize split runtime cutover

One key press, two trust boundaries, one clear architecture.

KeyPath used to rely on a runtime path where the thing macOS launched, the thing users were told to trust, and the thing that actually touched the keyboard could drift apart. The split-runtime cutover fixes that by making the user-session host own input capture while a separate privileged bridge owns VirtualHID output.

Old mental model “Kanata is the daemon.” That was simple to say, but wrong in important macOS-specific ways.
New mental model “The app coordinates, the host captures input, the bridge emits output.” Each boundary now matches a real operating-system boundary.

Why this matters on macOS

Input Monitoring is user-session oriented, while pqrs VirtualHID access still crosses a privileged boundary. Treating those as one process made health, permissions, and recovery harder to reason about.

Why the old approach had to change

The previous runtime worked often enough to look healthy, but it had three structural problems. They were not just bugs. They were signs that the architecture no longer matched how macOS actually grants permission and isolates privileged I/O.

Permission identity drift

The launch subject, the canonical permission target, and the effective HID-owning process could disagree. That makes onboarding and debugging confusing for users and developers.

Registration was mistaken for liveness

`SMAppService` being enabled only means something is registered. It does not prove the runtime is actually running, responding, and capturing input.

Privileged output leaked into the input path

A user-session runtime could still trip over pqrs root-only boundaries because output readiness and event emission were too tightly coupled to the same runtime path.

Interactive architecture comparison

Switch views, then step through a single key press to see where responsibility moves.

Original architecture

A single runtime path carried too many responsibilities and too many assumptions.

Orchestration KeyPath.app

UI, configuration, diagnostics, and service control.

Launch identity kanata-launcher

The thing launchd starts, but not necessarily the long-term runtime identity users reason about.

Input + output + recovery /Library/KeyPath/bin/kanata

Captures input, runs remapping logic, checks output readiness, and emits output through the same path.

Privileged boundary pqrs VirtualHID / DriverKit

Required for output, but not aligned with the same trust model as Input Monitoring.

New split runtime

Each component now owns one job and the trust boundaries are explicit.

Orchestration KeyPath.app

Still owns PermissionOracle, InstallerEngine, diagnostics, and high-level coordination.

User-session input identity Bundled runtime host

Owns HID capture and runs the Kanata runtime under the stable app-owned identity macOS sees.

Privileged output boundary Output bridge companion

Owns the root-scoped path to VirtualHID and exposes handshake, emit, sync, reset, and health status.

Privileged device access pqrs VirtualHID / DriverKit

Still exists, but no longer drags the input-capture identity across the same boundary.

Original architecture: the launcher, system binary, and user-facing permission story could diverge, which made failure analysis harder than it needed to be.

What the new architecture improves

The split-runtime design is not just “more components.” It is a more accurate map of the operating system. That gives KeyPath better correctness, better upgrade behavior, and a cleaner story for novice contributors.

Stable permission model

The process that opens HID devices is now the process whose identity matters. The permission contract is no longer hidden behind a launcher handoff.

Better failure isolation

If input capture breaks, that is different from the output bridge breaking. Each problem has its own health surface, logs, and recovery path.

Safer postcondition checks

Installer and repair flows can verify “running and actually ready” instead of assuming a registered daemon means everything is fine.

Architecture rules this page is teaching

These are the deeper engineering ideas behind the cutover. They are the difference between a feature that “works on my machine” and a system that stays understandable after months of evolution.

Match code boundaries to OS boundaries

macOS does not treat input permission, launch registration, and privileged output as the same thing. The software should not pretend they are.

Keep ownership centralized

`PermissionOracle` still owns permission truth. `InstallerEngine` still owns installation and repair. The cutover refines runtime responsibilities without scattering decision-making.

Prefer narrow protocols at privilege boundaries

The output bridge only needs a versioned contract for handshake, emit, modifier sync, reset, and health. A small protocol is easier to secure, test, and evolve.

What stays the same

Kanata still does remapping

The split runtime changes hosting and transport on macOS. It does not replace Kanata’s parsing or remapping core.

The app still coordinates

KeyPath.app remains the place where users see diagnostics, permissions guidance, and installation state.

Reliability is still verified, not assumed

The service lifecycle rules remain: registration metadata is not liveness, and success must be postcondition-verified.

Based on the implementation

This page summarizes the split-runtime cutover described in PR #225 and the supporting design docs around the macOS runtime identity change, the bridge-host spike, and the Kanata backend seam.

  • The core design target is a stable app-bundled input runtime identity paired with a separate privileged output path.
  • The split was motivated by real runtime evidence, not by abstract layering preferences.
  • The goal is boring reliability: clearer permissions, clearer diagnostics, and fewer hidden macOS-specific traps.