Skip to content

Commit 6d963eb

Browse files
fix(compiler): gate HoloQR release on HoloScript source
PASS: pnpm check:holoqr-born-from-source
1 parent 0cc46f8 commit 6d963eb

5 files changed

Lines changed: 289 additions & 121 deletions

File tree

Lines changed: 66 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,86 @@
1-
# Universal QR Scanner Meta Quest 3 / 3S
1+
# HoloQR - Universal QR Scanner for Meta Quest
22

3-
The Quest can't natively scan arbitrary QR codes (Meta restricts native scanning to WiFi
4-
provisioning). This app fills that gap: launch it, point the headset's passthrough cameras at
5-
any QR code, and it decodes the value and opens URLs in the Quest Browser.
3+
HoloQR fills a gap in Quest: it scans arbitrary QR codes through the headset passthrough cameras,
4+
decodes them on-device, opens ordinary links in Quest Browser, and enters HoloScript worlds from
5+
world links.
66

7-
First HoloScript-authored app targeting a major XR store. Source of truth is
8-
[`scanner.holo`](./scanner.holo); the Android project under [`android/`](./android) is the
9-
materialization. Origin + full build record:
10-
`ai-ecosystem/research/2026-06-19_universal-qr-scanner-quest-build.md`.
7+
This is a HoloScript-authored app, not a native app with a HoloScript label. The source of truth is
8+
[`scanner.holo`](./scanner.holo) plus [`worlds/`](./worlds). The immersive Quest project under
9+
[`android-mr/`](./android-mr) is compiler output and a bounded native bridge for platform APIs.
10+
11+
## Release invariant
12+
13+
The only supported shipping path is:
14+
15+
```text
16+
scanner.holo + worlds/*.holo
17+
-> HoloCompositionParser + QuestCompiler
18+
-> generated android-mr sources
19+
-> independent golden-diff verification
20+
-> signed Gradle release APK
21+
```
22+
23+
The release command runs that entire source gate before it resolves signing secrets or invokes
24+
Gradle. A direct `gradlew assembleRelease` can be useful during native debugging, but it is not a
25+
shipping command because it does not prove the APK came from the current HoloScript source.
1126

1227
## How it works
13-
- **2D panel app** (not an immersive VR scene). Runs in the Quest windowed environment.
14-
- **Passthrough camera** via standard Android **Camera2** (Meta exposes the headset cameras as
15-
logical Camera2 devices). Selects the forward passthrough RGB camera using Meta's vendor
16-
`CameraCharacteristics` (`com.meta.extra_metadata.camera_source==0`, `position==0`).
17-
- **Decode** on the YUV Y (luminance) plane with **ZXing core** (`com.google.zxing:core:3.5.3`) —
18-
GMS-free, because Quest/Horizon OS has no Google Play Services (ML Kit unbundled would not work).
19-
- **Open URL** in the Quest Browser via the documented Web Task scheme:
20-
`ovrweb://webtask?uri=<url-encoded-url>` (plain `https` `ACTION_VIEW` is *not* the Quest path).
21-
- **Privacy**: frames are decoded on-device; nothing is stored or transmitted.
28+
29+
- Camera frames come from Quest's Android Camera2 passthrough path.
30+
- ZXing decodes the YUV luminance plane locally; Google Play Services are not required.
31+
- Ordinary URLs open in the full Quest Browser, with the Web Task scheme as a fallback.
32+
- HoloScript world links enter compiler-generated immersive scenes.
33+
- Camera frames are not stored or transmitted.
2234

2335
## Requirements
24-
- Quest 3 / 3S on **Horizon OS v76+** (Camera2 passthrough code path).
25-
- The user grants the `Headset cameras` permission on first run (system-enforced opt-in + a
26-
persistent recording indicator while scanning — do not attempt to suppress it).
36+
37+
- Meta Quest 3 or 3S on a Horizon OS version that exposes passthrough Camera2 access.
38+
- JDK 17 and the Android SDK for local native builds.
39+
- The user-granted `Headset cameras` permission.
2740

2841
## Build
2942

30-
### Option A — Cloud build (recommended; no local toolchain)
31-
A GitHub Actions workflow is committed at
32-
`.github/workflows/quest-qr-scanner-build.yml`. It builds the release APK and uploads it as an
33-
artifact. To get a **signed** APK, add these repo secrets (base64 your keystore):
43+
Run these commands from the HoloScript repository root:
3444

35-
| Secret | Value |
36-
|---|---|
37-
| `KEYSTORE_BASE64` | `base64 -w0 release.keystore` |
38-
| `KEY_ALIAS` | your key alias (e.g. `quest_qr`) |
39-
| `KEYSTORE_PASSWORD` | keystore password |
40-
| `KEY_PASSWORD` | key password |
45+
```bash
46+
pnpm holoqr:generate-native
47+
pnpm check:holoqr-born-from-source
48+
pnpm holoqr:build-release
49+
```
4150

42-
Trigger it from the Actions tab (`workflow_dispatch`) or by pushing a change under
43-
`apps/quest-universal-qr-scanner/**`. Without the secrets it still builds an **unsigned** release APK.
51+
- `holoqr:generate-native` materializes the Quest app from HoloScript.
52+
- `check:holoqr-born-from-source` compiles and independently verifies the native reference without
53+
accessing signing custody or running Gradle.
54+
- `holoqr:build-release` runs the same gate, then resolves signing custody and builds the signed APK.
4455

45-
### Option B — Local build
46-
Requires JDK 17, Android SDK (Build-Tools 34+, Platform 34). Then:
47-
```bash
48-
cd android
49-
node ../generate.mjs # refresh spec-driven resources
50-
gradle wrapper --gradle-version 8.9 # one-time: create the gradle wrapper jar
51-
cp keystore.properties.example keystore.properties # then edit with your keystore (gitignored)
52-
./gradlew assembleRelease
53-
# -> app/build/outputs/apk/release/app-release.apk
56+
The signed artifact is written to:
57+
58+
```text
59+
apps/quest-universal-qr-scanner/android-mr/app/build/outputs/apk/release/app-release.apk
5460
```
5561

56-
### Signing — generate a keystore once
62+
See [`RELEASE.md`](./RELEASE.md) for HoloKey signing custody, Meta validation, and store submission.
63+
64+
## Install on a Quest
65+
66+
For a sideloaded test build:
67+
5768
```bash
58-
keytool -genkeypair -v -keystore release.keystore -alias quest_qr \
59-
-keyalg RSA -keysize 2048 -validity 10000
69+
adb install -r apps/quest-universal-qr-scanner/android-mr/app/build/outputs/apk/release/app-release.apk
6070
```
61-
Keep `release.keystore` safe — it is the app's identity on the store. Never commit it (`.gitignore`
62-
already excludes `*.keystore`, `*.jks`, and `keystore.properties`).
6371

64-
## Install on a Quest (sideload, for testing)
72+
Install through an ALPHA or BETA release channel before public submission when validating the exact
73+
store-signed artifact.
74+
75+
## Change the app
76+
77+
Edit [`scanner.holo`](./scanner.holo) or a world under [`worlds/`](./worlds), then run:
78+
6579
```bash
66-
adb install -r app-release.apk
67-
# launch it from the headset's app library (Unknown Sources)
80+
pnpm holoqr:generate-native
81+
pnpm check:holoqr-born-from-source
6882
```
6983

70-
## Publish to the Meta Horizon Store
71-
1. Create a developer **organization** at the Meta Horizon Developer Dashboard and complete
72-
**Admin Verification** (government-issued ID — an individual can publish this way; no DUNS, no
73-
documented fee).
74-
2. Host a **Privacy Policy** URL (template: app decodes QR codes on-device using the headset
75-
camera; no frames stored or transmitted) — required because the app accesses the camera.
76-
3. Create an app, upload the **signed APK** (Horizon Store takes APK; AAB is not documented as
77-
accepted). Target SDK ≥ 32, min SDK 34.
78-
4. Push to an **ALPHA / BETA release channel** first (invite-only, low friction) to validate
79-
on-device, then promote to **Production** (triggers full app review).
80-
81-
## Changing app config
82-
Edit [`scanner.holo`](./scanner.holo), then `node generate.mjs`. That rewrites
83-
`android/app/src/main/res/values/generated.xml` (app name, privacy note, frame size, camera
84-
selection, dedupe window, webtask scheme), which the Kotlin reads at runtime.
84+
Never hand-edit compiler-owned files under `android-mr/`. Native bridge files should contain only
85+
platform capabilities that the HoloScript compiler/runtime cannot yet express, and each such bridge
86+
is an explicit language gap rather than a second product source of truth.

apps/quest-universal-qr-scanner/RELEASE.md

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ your Meta developer identity — neither can (or should) be done by an agent.
66

77
> Native principle (F.126): the app icon, `build.gradle.kts`, and `AndroidManifest.xml` are
88
> **`@generated` from `scanner.holo`** by the quest compiler. To change app metadata (version, icon
9-
> colors, name), edit `scanner.holo` and recompile — never hand-edit `android-mr/`.
9+
> colors, name), edit `scanner.holo` and recompile — never hand-edit `android-mr/`. The canonical
10+
> signed-build command compiles `scanner.holo` plus `worlds/*.holo`, verifies the materialization
11+
> independently, and only then accesses signing custody or invokes Gradle.
1012
1113
---
1214

@@ -31,17 +33,17 @@ your Meta developer identity — neither can (or should) be done by an agent.
3133

3234
Review runs **Technical → Content → Publishing**. Mandatory technical VRCs and our status:
3335

34-
| VRC | Requirement | Status |
35-
|---|---|---|
36-
| Packaging.2 | APK v2 signature | ✅ verified v2 |
37-
| Packaging.6 | 64-bit (arm64-v8a) only | ✅ fixed — arm64-only |
38-
| Packaging.1 | Manifest conforms (VR category, version) | ✅ emitted |
39-
| Packaging.5 | APK < 1 GB |~120 MB |
40-
| Functional.14 | Passthrough app launches in passthrough |`enablePassthrough(true)` on scene-ready |
41-
| Functional.1 / 5 | No crashes; responds to head tracking | ▶ playtest (BETA channel) |
42-
| Performance.1 / 3 | Hits refresh rate; graphics ≤ 4 s or VR loader | ▶ playtest (lightweight panel + passthrough) |
43-
| Security.2 | Minimum permissions | ⚠ manifest declares `HAND_TRACKING`/`RENDER_MODEL` (Spatial-SDK starter inheritance) the scanner may not use — trim after a headset test confirms controller input still works; not a hard blocker |
44-
| Security.1 | Entitlement check |**recommended, NOT required** — no Platform SDK integration needed |
36+
| VRC | Requirement | Status |
37+
| ----------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
38+
| Packaging.2 | APK v2 signature | ✅ verified v2 |
39+
| Packaging.6 | 64-bit (arm64-v8a) only | ✅ fixed — arm64-only |
40+
| Packaging.1 | Manifest conforms (VR category, version) | ✅ emitted |
41+
| Packaging.5 | APK < 1 GB |~120 MB |
42+
| Functional.14 | Passthrough app launches in passthrough |`enablePassthrough(true)` on scene-ready |
43+
| Functional.1 / 5 | No crashes; responds to head tracking | ▶ playtest (BETA channel) |
44+
| Performance.1 / 3 | Hits refresh rate; graphics ≤ 4 s or VR loader | ▶ playtest (lightweight panel + passthrough) |
45+
| Security.2 | Minimum permissions | ⚠ manifest declares `HAND_TRACKING`/`RENDER_MODEL` (Spatial-SDK starter inheritance) the scanner may not use — trim after a headset test confirms controller input still works; not a hard blocker |
46+
| Security.1 | Entitlement check |**recommended, NOT required** — no Platform SDK integration needed |
4547

4648
**Founder/submission-side (not build):** Data Use Checkup (declare the passthrough camera — "frames decoded on-device, not stored/transmitted"), Content Guidelines, IARC age rating, and the listing assets. Use a **Release Channel (ALPHA/BETA)** to install on-headset with no review before the public submission.
4749

@@ -59,42 +61,55 @@ time — never in a plaintext file. `build-release.mjs` resolves them, materiali
5961
private tmp file, builds, and **deletes the keystore after the build**.
6062

6163
### 1a. Generate the keystore (once, ever)
64+
6265
```bash
6366
keytool -genkeypair -v \
6467
-keystore release.keystore \
6568
-alias quest_qr \
6669
-keyalg RSA -keysize 2048 -validity 10000 \
6770
-dname "CN=Joseph Krzywoszyja, OU=HoloScript, O=HoloScript, L=YourCity, S=YourState, C=US"
6871
```
72+
6973
**Back up `release.keystore` to two secure locations (offline). Record the passwords.** HoloKey holds
70-
the *operational* copy; your offline backup is the custody master. Losing the HoloKey KEK ≠ losing the
74+
the _operational_ copy; your offline backup is the custody master. Losing the HoloKey KEK ≠ losing the
7175
keystore as long as that offline backup exists.
7276

7377
### 1b. Store the four secrets in HoloKey (once)
78+
7479
The vault needs a KEK + Postgres in env: `HOLOKEY_PROD_KEK_CURRENT` + `HOLOKEY_PROD_KEK_<ID>`
7580
(`node scripts/holokey.mjs gen-kek` prints them) and `DATABASE_URL`.
81+
7682
```bash
7783
node scripts/holokey.mjs set KEYSTORE_PASSWORD '<store-password>'
7884
node scripts/holokey.mjs set KEY_PASSWORD '<key-password>'
7985
node scripts/holokey.mjs set KEY_ALIAS 'quest_qr'
8086
node scripts/holokey.mjs set ANDROID_KEYSTORE_B64 "$(base64 -w0 release.keystore)"
8187
```
88+
8289
(I can run these for you secrets-safely — the values go only into the encrypted vault, never into chat
8390
or a committed/logged file.) Secrets are owner-bound under `HOLOKEY_OWNER` (default `infra`); the build
8491
must resolve under the same owner.
8592

8693
### 1c. Build the signed APK
94+
8795
```bash
8896
# env: JAVA_HOME (JDK 17) + ANDROID_HOME, plus the HoloKey KEK + DATABASE_URL (so the vault is ON)
89-
node scripts/build-release.mjs
97+
pnpm holoqr:build-release
9098
# → android-mr/app/build/outputs/apk/release/app-release.apk (signed v2)
9199
```
92-
`build-release.mjs` resolves the four secrets from HoloKey (or, if the vault is OFF — no KEK — from
93-
matching env vars `KEYSTORE_PASSWORD`/`KEY_PASSWORD`/`KEY_ALIAS`/`ANDROID_KEYSTORE_B64`), writes the
94-
keystore to a `0o600` tmp file, runs `gradlew assembleRelease`, and unlinks the tmp keystore. **No
95-
gradle change was needed**`android-mr/app/build.gradle.kts` already reads `KEYSTORE_FILE`/`` from
96-
env. A plaintext `keystore.properties` still works as a *local-only optional override* but is
97-
gitignored and unnecessary with HoloKey.
100+
101+
`holoqr:build-release` first runs the real HoloCompositionParser and QuestCompiler over
102+
`scanner.holo` and every bundled world, then runs the independent Quest golden-diff gate. A parser,
103+
compiler, or generated-output mismatch blocks the build before signing secrets are resolved. For a
104+
safe proof with no signing or Gradle, run `pnpm check:holoqr-born-from-source`.
105+
106+
After the source gate passes, `build-release.mjs` resolves the four secrets from HoloKey (or, if the
107+
vault is OFF, from matching env vars
108+
`KEYSTORE_PASSWORD`/`KEY_PASSWORD`/`KEY_ALIAS`/`ANDROID_KEYSTORE_B64`), writes the keystore to a
109+
`0o600` temporary file, runs `gradlew assembleRelease`, and unlinks the temporary keystore. The
110+
`android-mr/app/build.gradle.kts` file already reads `KEYSTORE_FILE` and the other values from env. A
111+
plaintext `keystore.properties` still works as a local-only optional override but is gitignored and
112+
unnecessary with HoloKey.
98113

99114
> Verified (2026-06-22): the full resolve → materialize → `assembleRelease` → v2-signed APK → cleanup
100115
> chain builds GREEN on-device via the env-fallback path (same resolver code the vault uses).
@@ -115,12 +130,13 @@ review-skip path, but a clean utility passes the basic technical/content/privacy
115130
to install on your headset and verify before public submission.
116131
5. **Fill listing + declarations** (assets list below).
117132
6. **IARC age-rating questionnaire** (in-dashboard).
118-
7. **Data Use questionnaire** — declare the passthrough camera: *"camera frames are processed on-device
119-
to decode QR codes; frames are not stored or transmitted."* A **Privacy Policy URL is required**
133+
7. **Data Use questionnaire** — declare the passthrough camera: _"camera frames are processed on-device
134+
to decode QR codes; frames are not stored or transmitted."_ A **Privacy Policy URL is required**
120135
(see `PRIVACY.md`).
121136
8. **Submit for review** — Technical → Content → Publishing. Submit ≥ 2 weeks before any target date.
122137

123138
### Listing assets to provide (24-bit PNG unless noted)
139+
124140
- App icon **512×512** (no transparency) · Spatialized icon **180×180** (transparent)
125141
- Hero **3000×900** · Cover landscape **2560×1440** · square **1440×1440** · portrait **1008×1440** · mini **1080×360**
126142
- Logo (transparent) up to **9000×1440** (32-bit)
@@ -133,6 +149,7 @@ I can produce the icon/cover/logo art set from the emitted `ic_launcher.xml` bra
133149
---
134150

135151
## Sources
152+
136153
Meta Horizon publishing docs (upload, manifest, signing, submit, asset guidelines, release channels)
137154
— verified June 2026. Key facts: APK + v2 signing, `quest3|quest3s` supportedDevices, monotonic
138155
`versionCode`, App-Lab-merged-into-Store.

0 commit comments

Comments
 (0)