Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
2887966
Add generation-bound supervised lighting qualification
offalexjackson777-stack Jul 22, 2026
2143bc5
Fix installed DKMS module discovery
offalexjackson777-stack Jul 22, 2026
e9361ef
Unify packaged runtime directory modes
offalexjackson777-stack Jul 22, 2026
0d9dbc4
Preserve canonical modes in native packages
offalexjackson777-stack Jul 22, 2026
703e693
Report receiver-bound driver activation first
offalexjackson777-stack Jul 22, 2026
75d666b
feat(qualification): supervise hardware lifecycle gates
offalexjackson777-stack Jul 22, 2026
3c4d10b
fix(packaging): allow bounded activation state access
offalexjackson777-stack Jul 23, 2026
560f65a
fix(packaging): permit read-only module inspection
offalexjackson777-stack Jul 23, 2026
36805aa
fix(ops): negotiate features per client
offalexjackson777-stack Jul 23, 2026
d046ebf
fix(qualification): preserve full generation identities
offalexjackson777-stack Jul 23, 2026
4ac175c
fix(runtime): preserve writable routes across bridge restart
offalexjackson777-stack Jul 23, 2026
37cd57f
fix(runtime): restore devices after quiet activity
offalexjackson777-stack Jul 23, 2026
2b27e86
fix(qualification): keep transient telemetry retryable
offalexjackson777-stack Jul 23, 2026
d17bdb5
fix(kernel): classify composite receiver activity
offalexjackson777-stack Jul 23, 2026
df1cdb3
fix(kernel): bootstrap generation pairing inventory
offalexjackson777-stack Jul 23, 2026
481107c
test(qualification): refresh receiver profile fixtures
offalexjackson777-stack Jul 23, 2026
7ad9242
fix: gate lifecycle restore on device readiness
offalexjackson777-stack Jul 23, 2026
e342610
fix: make resume restoration ordering durable
offalexjackson777-stack Jul 23, 2026
9e1c7b6
fix: serialize retryable lifecycle actions
offalexjackson777-stack Jul 23, 2026
90b8e6d
fix: preserve keyboard activity while externally powered
offalexjackson777-stack Jul 23, 2026
b6d3955
build: select pinned verification automatically
offalexjackson777-stack Jul 23, 2026
a037b3e
fix: settle retained-route lifecycle restores
offalexjackson777-stack Jul 23, 2026
577171b
fix: migrate qualification stages across regenerated ids
offalexjackson777-stack Jul 23, 2026
7667a1c
fix: settle keyboard lifecycle restores
offalexjackson777-stack Jul 23, 2026
5ac4183
fix: confirm keyboard return readiness
offalexjackson777-stack Jul 24, 2026
037423b
test: refresh qualification profile fixtures
offalexjackson777-stack Jul 24, 2026
758fc8f
fix: reassert confirmed device returns
offalexjackson777-stack Jul 24, 2026
e052263
fix: migrate compatible stable lighting state
offalexjackson777-stack Jul 24, 2026
48adf49
fix: replan migrated restoration claims
offalexjackson777-stack Jul 24, 2026
57c3d74
fix: retry restoration after child identity
offalexjackson777-stack Jul 24, 2026
0f5b13e
fix: recover stale lifecycle checkpoints
offalexjackson777-stack Jul 24, 2026
ff1f71d
test: freeze proven behavior before wake restoration
offalexjackson777-stack Jul 24, 2026
0644483
Implement profile-driven return restoration
offalexjackson777-stack Jul 24, 2026
29bf5ab
Keep package binaries within reviewed size budgets
offalexjackson777-stack Jul 24, 2026
1397527
Recover profile migrations across package restart
offalexjackson777-stack Jul 24, 2026
d579e9a
assurance: freeze native tunnel baseline
offalexjackson777-stack Jul 25, 2026
b99e00d
integrations: bind native OpenRazer control authority
offalexjackson777-stack Jul 25, 2026
8487cc5
feat: add generic native receiver exchange
offalexjackson777-stack Jul 25, 2026
b71e8fa
feat: add source-bound native parity qualification
offalexjackson777-stack Jul 25, 2026
6598d19
feat: add source-bound OpenRazer native relay
offalexjackson777-stack Jul 25, 2026
a1741e9
feat: enforce serial-proven direct route precedence
offalexjackson777-stack Jul 25, 2026
ea04da7
feat: add source-bound OpenRGB native route
offalexjackson777-stack Jul 25, 2026
13afbea
fix: package REUSE-compliant OpenRazer licenses
offalexjackson777-stack Jul 25, 2026
8241f8a
fix: unify source-tree license validation
offalexjackson777-stack Jul 25, 2026
4437d86
fix: keep native relay within thin-process budget
offalexjackson777-stack Jul 25, 2026
29b110c
feat: add declarative native device onboarding
offalexjackson777-stack Jul 25, 2026
6123626
feat: bound native exchange scheduling
offalexjackson777-stack Jul 25, 2026
16d8536
fix: preserve actor runtime tick ordering
offalexjackson777-stack Jul 25, 2026
0b62999
fix: validate native report structure
offalexjackson777-stack Jul 25, 2026
849e2fd
test: bind native report contract fixtures
offalexjackson777-stack Jul 25, 2026
a2c80f5
feat: complete declarative native route integration
offalexjackson777-stack Jul 25, 2026
d70d0ec
build: keep operations binaries within budget
offalexjackson777-stack Jul 25, 2026
c21ddd7
Fix native relay module preflight ordering
offalexjackson777-stack Jul 26, 2026
f9d4598
feat(native): add bounded read-only parity qualifier
offalexjackson777-stack Jul 26, 2026
0d33f3a
fix(native): retry qualifier reads only after busy
offalexjackson777-stack Jul 26, 2026
b00f844
fix(native): preserve terminal device status parity
offalexjackson777-stack Jul 26, 2026
47ea56c
feat(native): add declarative receiver route selectors
offalexjackson777-stack Jul 26, 2026
6cc4fbe
fix(native): mirror upstream device mode reads
offalexjackson777-stack Jul 26, 2026
27a08ea
feat(native): add supervised direct parity references
offalexjackson777-stack Jul 26, 2026
94297d1
fix(verification): prune generated license trees
offalexjackson777-stack Jul 26, 2026
479503a
fix(native): keep raw capture under process ownership
offalexjackson777-stack Jul 26, 2026
b19395f
fix(native): parse direct usbmon captures without tshark
offalexjackson777-stack Jul 26, 2026
e89e6c5
feat(openrazer): package native receiver route
offalexjackson777-stack Jul 26, 2026
e473e0c
Rebase native OpenRazer route onto 3.12.4
offalexjackson777-stack Jul 26, 2026
1c1a0b3
Harden OpenRazer update ownership and provenance
offalexjackson777-stack Jul 26, 2026
1e50dad
Fix exact upstream cache isolation
offalexjackson777-stack Jul 26, 2026
d64776a
Run OpenRazer probe harness from build root
offalexjackson777-stack Jul 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Unified diffs contain significant one-character context prefixes.
*.patch -whitespace
11 changes: 8 additions & 3 deletions .github/workflows/full-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,21 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Restore verified software caches
- name: Restore exact upstream sources
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
.hfx/upstreams
key: hfx-upstreams-${{ runner.os }}-${{ hashFiles('integrations/catalog.json') }}
- name: Restore verified build caches
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
.hfx/cargo
target
key: hfx-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'toolchains/development-environment.json', 'integrations/catalog.json') }}
key: hfx-build-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'toolchains/development-environment.json') }}
restore-keys: |
hfx-${{ runner.os }}-
hfx-build-${{ runner.os }}-
- name: Set up Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Build the pinned development environment
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,21 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Restore verified software caches
- name: Restore exact upstream sources
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
.hfx/upstreams
key: hfx-upstreams-${{ runner.os }}-${{ hashFiles('integrations/catalog.json') }}
- name: Restore verified build caches
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
.hfx/cargo
target
key: hfx-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'toolchains/development-environment.json', 'integrations/catalog.json') }}
key: hfx-build-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'toolchains/development-environment.json') }}
restore-keys: |
hfx-${{ runner.os }}-
hfx-build-${{ runner.os }}-
- name: Set up Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Build the pinned development environment
Expand Down
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["crates/hfx-bridge", "crates/hfx-core", "crates/hfx-daemon", "crates/hfx-domain", "crates/hfx-errors", "crates/hfx-integration-model", "crates/hfx-kernel-transport", "crates/hfx-ops", "crates/hfx-profiles", "crates/hfx-protocol", "crates/hfx-runtime", "crates/hfx-sdk", "crates/hfx-sim"]
members = ["crates/hfx-bridge", "crates/hfx-core", "crates/hfx-daemon", "crates/hfx-domain", "crates/hfx-errors", "crates/hfx-integration-model", "crates/hfx-kernel-transport", "crates/hfx-ops", "crates/hfx-profiles", "crates/hfx-protocol", "crates/hfx-runtime", "crates/hfx-sdk", "crates/hfx-sim", "crates/hfx-uhid-relay"]
resolver = "3"

[workspace.package]
Expand All @@ -22,3 +22,17 @@ unsafe_code = "deny"
[workspace.lints.clippy]
all = "deny"
pedantic = "deny"

[profile.operations]
inherits = "release"
codegen-units = 1
lto = "thin"
panic = "abort"
strip = "symbols"

[profile.relay]
inherits = "release"
codegen-units = 1
lto = "thin"
panic = "abort"
strip = "symbols"
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,41 @@

```mermaid
flowchart LR
Applications["Applications"] --> SDK["Versioned SDK"]
SDK --> Bridge["Bridge and policy authority"]
Bridge --> Kernel["Minimal HID transport"]
Applications["OpenRazer and OpenRGB"] --> SDK["Versioned native-route SDK"]
SDK --> Bridge["Route and exchange authority"]
Bridge --> Kernel["Bounded receiver transport"]
Kernel --> Receiver["HyperFlux receiver and paired devices"]
```

Applications own user interaction, layouts, and effects. The SDK owns the typed
application boundary. The bridge is the sole userspace writer and owns policy,
qualification, scheduling, restoration, and outcomes. The kernel preserves
ordinary HID input and transports bounded generation-bound envelopes.
Applications own model commands, settings, layouts, effects, profiles, and
persistence. The SDK carries qualified route discovery and exact 90-byte Razer
Feature exchanges. The bridge is the sole userspace receiver writer and owns
route qualification, generation safety, NativeControl arbitration, bounded
dispatch, availability, outcome certainty, and diagnostics. The kernel
preserves ordinary HID input and transports generation-bound exchanges without
interpreting device semantics.

## OpenRazer Route

HyperFlux applies a narrow transport patch to official
[OpenRazer 3.12.4](https://github.com/openrazer/openrazer/commit/1ef8a91712906a89fd332a2ed62bbd105315d6d1).
The upstream daemon, Python client, device classes, ordinary USB tables, and
direct-device behavior remain OpenRazer-owned. The patched DKMS modules accept
only trusted, feature-only HyperFlux UHID routes and advertise
`hyperflux_native_transport=razer-feature-report-v1`.

| Qualified receiver-backed controller | Native route | Direct USB precedence |
| --- | --- | --- |
| Razer Basilisk V3 Pro 35K (Wireless) | `1532:00cd` | `1532:00cc` |
| Razer DeathStalker V2 Pro TKL (Wireless) | `1532:0296` | `1532:0298` |

Distribution release suffixes do not affect compatibility. A newer OpenRazer
source line requires a reviewed rebase and fresh verification; until then,
`hyperflux-next-openrazer-relay.service` remains stopped. See the
[deployment contract](docs/generated/openrazer-native-deployment.md),
[installation contract](docs/generated/installation.md), and
[hardware boundary](docs/generated/supported-hardware.md). Other catalog
devices remain research-only and non-writable.

## Current Readiness

Expand Down
2 changes: 1 addition & 1 deletion apps/device-qualification/assets/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ code,
text-align: center;
}

@media (max-width: 1080px) {
@media (max-width: 1200px) {
.qualification-layout { grid-template-columns: 1fr; }
.inspector { position: static; }
.identity-strip { grid-template-columns: 1fr 1fr; row-gap: 15px; }
Expand Down
42 changes: 28 additions & 14 deletions apps/device-qualification/assets/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const refreshButton = document.querySelector("#refresh-view");

const state = {
view: null,
selectedDeviceId: null,
selectedDeviceKey: null,
selectedStageId: null,
busyActionId: null,
loading: true,
Expand All @@ -20,9 +20,9 @@ const state = {
refreshButton.addEventListener("click", () => refreshView(true));

document.addEventListener("click", (event) => {
const deviceButton = event.target.closest("[data-device-id]");
const deviceButton = event.target.closest("[data-device-key]");
if (deviceButton) {
state.selectedDeviceId = deviceButton.dataset.deviceId;
state.selectedDeviceKey = deviceButton.dataset.deviceKey;
state.selectedStageId = null;
state.hasDraft = false;
render();
Expand Down Expand Up @@ -78,6 +78,8 @@ async function refreshView(visible) {
reconcileSelection();
if (value.companion.state === "legacy-v2-detected") {
renderConnection("warning", "HyperFlux V2 detected");
} else if (value.companion.state === "lifecycle-transition") {
renderConnection("warning", "Lifecycle transition");
} else {
renderConnection("ready", value.companion.state === "ready" ? "Connected" : "Needs attention");
}
Expand Down Expand Up @@ -149,7 +151,7 @@ async function invokeStage(form) {

async function fetchWithTimeout(path, options) {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 6_000);
const timeout = window.setTimeout(() => controller.abort(), 15_000);
try {
return await fetch(path, {
credentials: "same-origin",
Expand All @@ -163,8 +165,8 @@ async function fetchWithTimeout(path, options) {

function reconcileSelection() {
const contexts = deviceContexts();
if (!contexts.some(({ device }) => device.device_id === state.selectedDeviceId)) {
state.selectedDeviceId = contexts[0]?.device.device_id ?? null;
if (!contexts.some((context) => context.key === state.selectedDeviceKey)) {
state.selectedDeviceKey = contexts[0]?.key ?? null;
state.selectedStageId = null;
state.hasDraft = false;
}
Expand All @@ -182,7 +184,7 @@ function render() {
return;
}
if (!state.view) return;
if (state.view.companion.state !== "ready") {
if (!["ready", "lifecycle-transition"].includes(state.view.companion.state)) {
app.innerHTML = systemGate(state.view.companion.state);
return;
}
Expand All @@ -208,10 +210,10 @@ function renderDeviceRail(contexts) {
<aside class="device-rail" aria-label="Detected controllers">
<header class="rail-heading"><span>Detected controllers</span><strong>${contexts.length}</strong></header>
<div class="device-list">
${contexts.map(({ device, plan }) => {
${contexts.map(({ key, device, plan }) => {
const progress = stageProgress(plan);
return `
<button type="button" class="device-row ${device.device_id === state.selectedDeviceId ? "is-selected" : ""}" data-device-id="${escapeAttribute(device.device_id)}">
<button type="button" class="device-row ${key === state.selectedDeviceKey ? "is-selected" : ""}" data-device-key="${escapeAttribute(key)}">
<span class="device-kind" aria-hidden="true">${device.kind === "mouse" ? "M" : device.kind === "keyboard" ? "K" : "D"}</span>
<span class="device-copy">
<strong>${escapeHtml(device.model_name || "Unrecognized controller")}</strong>
Expand Down Expand Up @@ -405,14 +407,21 @@ function emptyInventory() {
function deviceContexts() {
if (!state.view) return [];
return state.view.receivers.flatMap((receiver) => receiver.devices.map((device) => ({
key: deviceContextKey(receiver, device),
receiver,
device,
plan: state.view.plans.find((plan) => plan.device_id === device.device_id) || null,
plan: state.view.plans.find((plan) => plan.receiver_id === receiver.receiver_id
&& plan.generation_id === receiver.generation_id
&& plan.device_id === device.device_id) || null,
})));
}

function selectedContext() {
return deviceContexts().find(({ device }) => device.device_id === state.selectedDeviceId) || null;
return deviceContexts().find(({ key }) => key === state.selectedDeviceKey) || null;
}

function deviceContextKey(receiver, device) {
return `${encodeURIComponent(receiver.receiver_id)}:${receiver.generation_id}:${encodeURIComponent(device.device_id)}`;
}

function allStages(plan) {
Expand Down Expand Up @@ -458,9 +467,14 @@ function supportLabel(verdict, support) {
}

function batteryLabel(battery) {
return battery.availability === "reported" && battery.percentage !== null
? `${battery.percentage}% battery`
: `Battery ${labelize(battery.availability)}`;
if (battery.availability === "reported" && battery.percentage !== null) {
return `${battery.percentage}% battery`;
}
return {
unknown: "Waiting for battery report",
stale: "Battery report is out of date",
unavailable: "Battery report unavailable",
}[battery.availability] || "Battery report unavailable";
}

function riskCode(risk) {
Expand Down
35 changes: 28 additions & 7 deletions apps/device-qualification/assets/contract.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// SPDX-License-Identifier: GPL-2.0-only

const COMPANION_STATES = new Set(["ready", "driver-pending", "legacy-v2-detected", "bridge-unavailable", "no-receiver"]);
const COMPANION_STATES = new Set(["ready", "lifecycle-transition", "driver-pending", "legacy-v2-detected", "bridge-unavailable", "no-receiver"]);
const PRESENCE_STATES = new Set(["active", "sleeping", "unavailable", "unknown"]);
const SUPPORT_STATES = new Set(["unknown", "identified", "profile-qualified", "production-qualified"]);
const VERDICTS = new Set(["not-run", "in-progress", "blocked", "failed", "evidence-ready", "accepted"]);
const STAGE_STATUSES = new Set(["locked", "ready", "running", "awaiting-observation", "passed", "failed", "blocked", "skipped"]);
const RISKS = new Set(["read-only", "lighting-write", "device-lifecycle", "system-lifecycle"]);
const DIGEST = /^[a-f0-9]{64}$/;
const DECIMAL_GENERATION_ID = /^[1-9][0-9]{0,19}$/;
const MAX_GENERATION_ID = 18_446_744_073_709_551_615n;
const LOCAL_ENDPOINT = /^http:\/\/(127\.0\.0\.1|localhost)(:[0-9]+)?$/;

export function validateQualificationView(value) {
Expand All @@ -26,15 +28,19 @@ export function validateQualificationView(value) {
array(value.actions, "view.actions", 256);
value.actions.forEach((item, index) => action(item, `view.actions[${index}]`));

const devices = value.receivers.flatMap((item) => item.devices);
unique(devices.map((item) => item.device_id), "device IDs");
const receiverKeys = value.receivers.map((item) => receiverKey(item.receiver_id, item.generation_id));
unique(receiverKeys, "receiver generation IDs");
const receiverByKey = new Map(value.receivers.map((item) => [receiverKey(item.receiver_id, item.generation_id), item]));
const deviceKeys = value.receivers.flatMap((receiver) => receiver.devices.map((device) => `${receiverKey(receiver.receiver_id, receiver.generation_id)}\u0000${device.device_id}`));
unique(deviceKeys, "receiver-bound device IDs");
unique(value.plans.map((item) => item.plan_id), "plan IDs");
unique(value.actions.map((item) => item.id), "action IDs");
const deviceById = new Map(devices.map((item) => [item.device_id, item]));
const actionById = new Map(value.actions.map((item) => [item.id, item]));
const stageIds = [];
for (const item of value.plans) {
const device = deviceById.get(item.device_id);
const receiver = receiverByKey.get(receiverKey(item.receiver_id, item.generation_id));
require(receiver, `plan ${item.plan_id} references an unknown receiver generation`);
const device = receiver.devices.find((candidate) => candidate.device_id === item.device_id);
require(device, `plan ${item.plan_id} references an unknown device`);
if (item.profile_binding || device.profile) {
require(item.profile_binding && device.profile, `plan ${item.plan_id} profile binding is incomplete`);
Expand Down Expand Up @@ -80,7 +86,7 @@ function receiver(value, label) {
object(value, label);
exact(value, ["receiver_id", "generation_id", "model_name", "vendor_id", "product_id", "profile", "lifecycle", "devices"], label);
string(value.receiver_id, `${label}.receiver_id`);
integer(value.generation_id, `${label}.generation_id`, 1);
generationId(value.generation_id, `${label}.generation_id`);
nullableString(value.model_name, `${label}.model_name`);
nullableInteger(value.vendor_id, `${label}.vendor_id`, 0, 65535);
nullableInteger(value.product_id, `${label}.product_id`, 0, 65535);
Expand Down Expand Up @@ -124,8 +130,10 @@ function capability(value, label) {

function plan(value, label) {
object(value, label);
exact(value, ["plan_id", "device_id", "profile_binding", "verdict", "summary", "groups", "evidence"], label);
exact(value, ["plan_id", "receiver_id", "generation_id", "device_id", "profile_binding", "verdict", "summary", "groups", "evidence"], label);
string(value.plan_id, `${label}.plan_id`);
string(value.receiver_id, `${label}.receiver_id`);
generationId(value.generation_id, `${label}.generation_id`);
string(value.device_id, `${label}.device_id`);
nullableProfile(value.profile_binding, `${label}.profile_binding`);
enumeration(value.verdict, VERDICTS, `${label}.verdict`);
Expand All @@ -135,6 +143,10 @@ function plan(value, label) {
evidence(value.evidence, `${label}.evidence`);
}

function receiverKey(receiverId, generationId) {
return `${receiverId}\u0000${generationId}`;
}

function group(value, label) {
object(value, label);
exact(value, ["group_id", "title", "description", "stages"], label);
Expand Down Expand Up @@ -257,6 +269,15 @@ function nullableInteger(value, label, minimum, maximum = Number.MAX_SAFE_INTEGE
if (value !== null) integer(value, label, minimum, maximum);
}

function generationId(value, label) {
require(
typeof value === "string"
&& DECIMAL_GENERATION_ID.test(value)
&& BigInt(value) <= MAX_GENERATION_ID,
`${label} must be a canonical decimal u64 string`,
);
}

function boolean(value, label) {
require(typeof value === "boolean", `${label} must be boolean`);
}
Expand Down
Loading
Loading