HydraIssues

Phase 5: WiVRn client distribution for headsets
open unclassified Priority: high Project: hydraheadquest Parent: #734 Reporter: 15 Sep 2026 18:46

Description

Part of #734.

Today the client on the Quest 2 is a DEBUG-SIGNED APK pulled from a CI artifact and published as GitHub prerelease pyrowave-test-1, side-loaded over adb through hydraadb. Package id org.meumeu.wivrn.github.testing, version 26.6-234-g3576bbc3. That is a test rig, not a distribution path.

Scope:

  • A properly signed release build, with the keystore backed up the way hydraheadquest's is (#544 records that lesson: never lose it).
  • A real install path: MDM or the hydraadb route used deliberately, not by hand.
  • Decide how this coexists with hydraheadquest on the same headset, since both are XR clients and only one can hold the session. This likely feeds the head-side driver declaration in Phase 3.
  • Version reporting, so the cluster knows which client a head is running.

DESIGN DONE 2026-09-16. Full document: hydra-wivrn/docs/design/739-client-distribution.md, with the adversarial review it was hardened against in 739-critique.md.

Produced by a multi-agent pass: four parallel surveys of the real constraints, four independent proposals from different angles (minimal-change, one-app, fleet-ops-first, risk-first), scored judging, a completeness critic, then synthesis. Scores were close (29, 29, 28, 26 out of 40), which is itself a finding: no approach dominated, so the recommendation grafts across them.

THE DECISION
Build a Hydra-signed WiVRn client as its own APK with package id com.experiencenet.hydraxr, publish it to a new hydrawivrnclient project on releases.experiencenet.com, and deliver it through Meta rather than adb. hydraheadquest stays the kiosk and launcher, gaining a driver table and reporting which XR clients a head actually carries, so the cluster can match heads to bodies on driver the way it already matches bodies.

Explicitly NOT doing: absorbing WiVRn into the hydraheadquest APK (kept as a documented escape hatch); building our own Quest MDM (Horizon OS only enrols Meta-approved ones); buying ManageXR or ArborXR yet; a session-blocking version gate; any venue path that needs adb, a cable or a laptop.

SPLIT RECOMMENDED, and this is the main structural finding. Three of the four proposals had a rollout whose fourth step blocked on a Meta business verification call that nobody owns and nobody has started. So:

  • #739 keeps the parts with no external dependency: build, sign, publish, version reporting. Shippable this month.
  • A new issue takes Quest fleet distribution: the Meta organisation, HMS or a private release channel, one enrolled headset, the ACTIVATE_VPN question, the missing hydramdm Quest runbook. It blocks hydraheadquest too, not just WiVRn, and its clock should start now.
  • A new issue takes XR driver launch and arbitration in hydraheadquest, gated on a 20-minute hardware experiment.

WHAT THE CRITIC CAUGHT, verified afterwards and all of it load-bearing:

  • The constant-filename release convention every proposal assumed HAS NEVER BEEN EXERCISED. I re-checked: .../hydrawivrn/production/latest/hydra-wivrn-server.pkg.tar.zst returns 404; only the versioned name resolves through /latest/. The permanent install URL handed to Meta also dereferences to bxl1.hydramirror.experiencenet.com, a district-named host that also fronts the issue tracker.
  • "Only one XR client can hold an OpenXR session" is asserted everywhere and has never been observed here. What matters is whether the incumbent gets STOPPING (free handoff) or EXITING (WiVRn calls exit(0)). That is a 20-minute experiment on hardware we already own, and it decides which coexistence design is correct. It should have been a gate in every proposal and was a gate in none.
  • The WireGuard tunnel is the kill shot nobody gated on: it exists only because someone ran adb appops set ACTIVATE_VPN allow, which needs developer mode, which needs adb. If managed enrolment cannot pre-grant it, the head never heartbeats and every design collapses.
  • The debug-signed APK now on the Quest is a security finding, not untidiness. It is signed with the Android debug key whose private half ships in every SDK, it is debuggable so run-as exposes app-private data including PIN-derived pairing material, and the public 32 MB prerelease advertises the target.
  • Nobody has ever restored the hydraheadquest keystore backup. For a secret whose loss forces a fleet reinstall, an unrestored backup is a claim, not a control.
  • A modality nobody proposed: our own Meta app id plus a private release channel, using ovr-platform-util which already has working code in the fork's Build.yml. It is the only path giving silent updates on a stock consumer Quest with no MDM, no developer mode and no cable.
  • Framing: the target is Quest 3/3S and we own zero. The one Quest 2 is excluded by the codec ruling and has been offline since 2026-09-15.

FIRST STEPS, none blocked on anything external:
0. Set HYDRARELEASE_PUBLISH_TOKEN on hydraheadquest (it has the ANDROID_* secrets but not this one, which is why nothing has ever been published for it) and exercise the constant-filename convention. About an hour.

  1. Run the session-handoff experiment on the Quest and paste the logcat into this issue. It decides the coexistence design.
  2. File the two split issues and start the Meta organisation application.

DIRECTION RULED BY OWNER 2026-09-16, AFTER the design above. Read this before acting on it; it invalidates the spine of that document.

  1. We will NOT release through Meta.
  2. The target platform will probably be PICO, not Quest.
  3. Side-loading is acceptable for now.

WHAT THIS KILLS outright, and it is most of the delivery half of the design:

  • The Meta business organisation, Horizon Managed Services, and the private release channel via ovr-platform-util. That was the recommended spine and the critic's favourite unproposed modality. All moot.
  • The recommended issue split. Its whole justification was that three proposals blocked on a Meta verification call nobody owned. With Meta out, that blocker evaporates and #739 can stay whole.
  • The Quest-specific manifest work: the oculus build type, com.oculus.supportedDevices, the horizonos:uses-horizonos-sdk question.
  • Rollout steps 2, 9 and 10 as written, and open questions O-2, O-3 and O-8, which were all Meta-shaped.

WHAT SURVIVES, and is now the actual scope of this ticket:

  • Signing and keystore custody (section 2), unchanged and arguably MORE urgent. Side-loading a debug-signed APK is the status quo we are keeping for now, and the security finding stands: the Android debug key's private half ships in every SDK, the build is debuggable so run-as exposes PIN-derived pairing material, and a public prerelease advertises the target. A signed release build is worth doing even when the channel is a cable.
  • Version and capability reporting to the cluster (section 5). Platform-agnostic, and it is what lets the cluster match a head to a body on XR driver the way it already matches bodies.
  • Coexistence and arbitration (section 4). The question does not go away on Pico, only the incumbent changes: on Quest it is hydraheadquest, on Pico it is Pico Business Streaming, whose runtime is already the ActiveRuntime on fluffy. The session-handoff experiment is still the gate and is now MORE valuable, because it should be run on Pico hardware.
  • The staging canary ring, the keystore restore drill, and deleting the pyrowave-test-1 prerelease.
  • Rollout step 0, already half done: HYDRARELEASE_PUBLISH_TOKEN is now set on hydraheadquest (it had the four ANDROID_* secrets and not this one, so every release it ever cut silently skipped publishing while reporting success). Still to do there: the constant-filename artifact, which the critic proved has never worked in this estate.

GOOD NEWS, verified in the fork today: the WiVRn client ALREADY SUPPORTS PICO as a first-class target. client/application.cpp carries interaction profiles for pico_neo3, pico4 and pico4s, and client/hmd_traits.cpp has a Pico branch handling Pico-specific eye-tracking and face-tracking permissions and reading pxr.vendorhw.product.model. So the platform change does not invalidate the client itself, only the delivery channel.

NEW QUESTIONS this direction opens, none of them answered yet:

  • Which Pico model, and does it clear the PyroWave bar? The codec needs 200+ Mbit/s sustained and a GPU with headroom to decode in shaders. Pico 4 and 4S are the plausible candidates; nobody has measured either.
  • How does a Pico get an app in a fleet? Pico has its own device management story. Side-loading is fine for now, but the venue answer is unknown and should not be assumed to be easier than Meta's.
  • Does WiVRn coexist with Pico Business Streaming, or replace it? PBS is native OpenXR and is already the ActiveRuntime on fluffy for the hydragon work (#626). Two runtimes on one headset is the same arbitration problem wearing different clothes.
  • We own zero Pico devices today, exactly as we own zero Quest 3/3S. The framing critique in the design applies unchanged: we are designing for hardware not yet in the building.

CORRECTIONS FROM THE OWNER, same day. Two of my statements above were wrong.

  1. WE DO HAVE A PICO. It is not a gap, it is just not enrolled as a head.
    It lives at the Gallo-Romeins Museum in Tongeren, running PICO Business
    Streaming against body fluffy-dumpling-87 (node-11da9ea3, hostname
    hydra-s-0000), joining that body's Mobile Hotspot SSID "experiencenet".
    RomeinsMuseum.exe runs locally on the body; that venue deliberately does NOT
    use Sunshine/Moonlight, and HydraBody's task is disabled there on purpose.
    Runbook: hydravenues/docs/runbooks/gallo-romeins-museum.md (#547).
    So "we own zero Picos, designing for hardware not in the building" was wrong.
    There is a Pico in production, and a venue already running the PBS lane.
    What is missing is enrolment: no Pico appears in GET /api/v1/heads today.

  2. THE DRIVER TAXONOMY IS CLEANER THAN THE DESIGN ASSUMED.
    PBS, ALVR and WiVRn are all PEERS: they are VR drivers.
    Moonlight/Sunshine is NOT an XR driver at all; it is the FLAT lane.

    That is exactly the shape xr_drivers already has, and it means the enum needs
    a third member:

    xr_drivers: ["alvr"] | ["wivrn"] | ["pbs"] | combinations
    flat lane:  sunshine + moonlight, outside this list entirely
    

    Consequences worth writing down:

    • hydrabody should advertise "pbs" the same way it now advertises "alvr" and
      "wivrn": role plus evidence on disk plus platform. fluffy already runs the
      Pico Business runtime co-located, so it is the obvious first pbs body.
    • The head side declares which VR driver it speaks, and a Pico head speaks
      pbs, or wivrn once the client is signed and side-loaded onto it.
    • The driver-intersection matching shipped in hydracluster v2.0.120/121
      already handles three drivers without further change. Nothing about it
      assumed two.
    • Coexistence on a Pico is therefore PBS versus WiVRn, both native OpenXR
      runtimes, which is a sharper version of the same arbitration question.

    This is not new ground: #557 already scoped "hydrabody tri-mode roles
    (Sunshine/ALVR/PBS graceful switching)" back on 2026-08-26, and #559 filed
    the hydraheadpico lane. Those predate this work and should be linked into
    #734 rather than re-derived. #544's phase 3 policy already rules Pico
    enterprise the golden path precisely because PBS bypasses SteamVR and needs
    zero accounts, which is the same reasoning that now rejects the Meta lane.

REVISED NEXT STEP for this ticket, replacing the Meta-shaped one: get the
existing Pico enrolled as a head so it can speak a driver, and decide whether
its first driver is pbs (already working at a venue) or wivrn (needs the signed
side-loaded client). Enrolling it also answers, for free, how a Pico gets an app
in this fleet, which is the open question that replaced the Meta channel.


QUEST LANE BUILT 2026-09-16, hydraheadquest master 9eaf8ee5, compile smoke test green.

Owner direction: park Pico for now, make the Quest switch between Sunshine and WiVRn depending on whether the experience is flat or XR, in ONE app where the visitor picks the experience. One driver preferred, WiVRn; ALVR or picobusinessstreaming only where a body forces it.

THE KEY FINDING: almost all of that already existed. hydraheadquest is already one app with a catalog the visitor picks from, and HydraState already routes on stream_mode: XR one way, flat to Sunshine and Moonlight the other. What was hardcoded was WHICH VR driver the XR path used. The work was turning that into a choice, not building a switch.

What landed:

  • WivrnLauncher implementing the existing XrHooks interface. Deliberately NOT a copy of AlvrLauncher, because the two dial in opposite directions: the body dials the headset for ALVR, while for WiVRn the CLIENT dials the body and therefore needs the body host and the pairing PIN. Both travel in wivrn://[:PIN@]host[:port], PIN in the password position, verified against application::set_server_uri in the fork.
  • The intent uses an EXPLICIT component. The client's VIEW filter declares DEFAULT and BROWSABLE but not the VR category, so an implicit VIEW intent carrying VR matches nothing; an explicit component skips filter matching. The VR category is still required or vrshell rejects the placement.
  • XrDrivers picks the launcher: WiVRn wins where both clients are installed, ALVR remains the answer for a Windows body, which WiVRn's Monado-based server cannot serve at all.
  • XrHooks gained driverName, so the heartbeat reports the driver actually in use. It reported a hardcoded "alvr", which was honest only while alvr was the only driver.
  • Heads now report xr_drivers, the head's half of the capability question bodies already answer. That completes the matching shipped in hydracluster v2.0.120 and v2.0.121, which until now had a body side and no head side.
  • Default WiVRn package is com.experiencenet.hydraxr, NOT the debug-signed org.meumeu.wivrn.github.testing, so a test build never satisfies a production head. Both are declared for package visibility so a developer headset can be told apart.
  • docs/hydra-api-contract.md updated, since it is authoritative. CLAUDE.md phase 3 marked superseded where it still said the XR client ships via Horizon Managed Services.

WHAT IS STILL MISSING before a visitor can actually do this:

  1. hydrabody does not arm hydra-wivrn.service on assignment. wivrnArm and wivrnDisarm exist and compile; nothing calls them from the XR state machine. Without this the body never starts serving. (#736)
  2. The PIN is not plumbed. WiVRn pairs with a PIN the server prints; the head needs it to build the URI. Nothing carries it from body to cluster to head today. The launcher accepts a null PIN and falls back to the client's own discovery and prior pairing, which is why the Quest 2 still connects, but that is not a fleet mechanism.
  3. The signed client does not exist yet: com.experiencenet.hydraxr is the package the kiosk looks for, and only the debug-signed test build is installed anywhere. Section 2 of the design covers this and is unblocked.
  4. Nothing has been run on hardware. Compile-green is not a working lane.