Prd/docs/runbooks/graphene-build.md
Lucy Doupalů be9f14ce34 Helpdesk - operator console + patched GrapheneOS Dialer for call handling
A small helpdesk system: an office Pixel running a patched GrapheneOS Dialer
answers technician calls, records both call legs as separate channels, and a
Ruby backend transcribes them through Whisper and files an AI summary against
the caller.

Squashed to a single commit for sharing. No credentials are included; secrets
live outside the repo in /etc/helpdesk/env on the server or a gitignored
.claude/env.local locally. See .claude/env.local.example for the shape.

Start at README.md, then docs/architecture.md.
2026-07-27 18:50:32 +02:00

6.5 KiB

Runbook - GrapheneOS build, Dialer patch, flash (Pixel 10a "stallion")

The privileged recording path only works inside a self-signed OS build, so the patched Dialer is built from GrapheneOS source and flashed, never shipped as an app. This is the build side; the hands-on flashing procedure with all the brick-safety rules is pixel10a-flash-manual.md.

Tags advance weekly - check grapheneos.org/releases for the current Stable tag and confirm it lists "Pixel 10a" before you sync a fresh tree.

This project's build box

The working tree lives on the dev machine at /build/grapheneos (tag 2026062800, AOSP base android-17.0.0_r1, device stallion). Facts that cost time to rediscover:

  • The tool shell defines a grep function wrapping ugrep, which breaks lunch's product parsing. Before sourcing the build env, run unset -f grep and put PATH=/usr/bin:/bin:$PATH first.
  • Build-matched adb/fastboot are at /build/grapheneos/out/host/linux-x86/bin/.
  • An incremental m Dialer after touching only Java is minutes; if the build environment changed it can invalidate the framework layer and take about an hour. Check what soong is rebuilding (siso.INFO shows the current module) before assuming it hung.
  • Dev iteration loop: build the APK, push it, reboot. No full reflash needed:
cd /build/grapheneos
unset -f grep; export PATH=/usr/bin:/bin:$PATH
source build/envsetup.sh
lunch stallion-cur-userdebug
m Dialer
adb root && adb remount
adb push out/target/product/stallion/product/priv-app/Dialer/Dialer.apk /product/priv-app/Dialer/
adb reboot     # unlock the phone once after boot so the helpdesk services start

Building from scratch

Host requirements

x86_64 Linux (Arch, Debian 12, Ubuntu 24.04/24.10 are the supported set), 32 GiB+ RAM (the LTO/CFI link step needs it), ~300 GiB disk (source with history + build output). Packages: repo yarnpkg zip rsync git python3 gnupg openssh-client curl, Node.js LTS for adevtool, and the 32-bit libs for Vanadium (libc6:i386 lib32stdc++6 lib32gcc-s1 after dpkg --add-architecture i386). On Debian, use yarnpkg - the yarn binary is an unrelated package.

Fetch, verify, sync

TAG=2026062800                       # re-check grapheneos.org/releases; must list "Pixel 10a"
mkdir grapheneos-$TAG && cd grapheneos-$TAG
repo init -u https://github.com/GrapheneOS/platform_manifest.git -b refs/tags/$TAG

# verify the manifest tag before syncing:
curl https://grapheneos.org/allowed_signers > ~/.ssh/grapheneos_allowed_signers
( cd .repo/manifests \
  && git config gpg.ssh.allowedSignersFile ~/.ssh/grapheneos_allowed_signers \
  && git verify-tag "$(git describe)" )   # expect: Good "git" signature ... contact@grapheneos.org

repo sync -j"$(nproc)"               # hours; re-run the same command to resume after a drop

resources/scripts/fetch-graphene-source.sh wraps this.

Vendor blobs

source build/envsetup.sh
yarnpkg --cwd vendor/adevtool/ install          # once
adevtool generate-all -d stallion               # downloads the stock factory image itself; no phone needed

Apply the Helpdesk patches

cd /path/to/Helpdesk
DIALER_TREE=/path/to/grapheneos/packages/apps/Dialer bash components/dialer-patch/apply.sh
SEPOLICY_TREE=/path/to/grapheneos/system/sepolicy bash components/sepolicy-patch/apply.sh

Both scripts copy the overlay, apply their patches (0001-0009 for the Dialer, one for sepolicy), and are safe to re-run - already-applied patches are detected and skipped. The env vars default to the build box's /build/grapheneos paths. Validate sepolicy with m selinux_policy; compile-check the Dialer with m Dialer.

Build the image

lunch stallion-cur-userdebug     # what the office device currently runs (test keys, unlocked)
m target-files-package
m otatools-package

Then flash per pixel10a-flash-manual.md (fastboot flashall, without -w unless you mean to wipe).

On-device acceptance

After any build lands on the phone, place a real call to the office number and watch the backend - the server log is the source of truth, logcat is for diagnosing a step that didn't fire:

  1. Ring → POST /calls/incoming arrives, screen-pop shows on the console.
  2. Answer → POST /calls/answered; recording starts within ~500 ms, with no consent dialog.
  3. Hang up → POST /calls/ended with a sensible disconnect cause.
  4. PUT /recordings/{uuid} → 200, and the event reaches recording_uploaded, then transcribed, then summarised on the console.
  5. Both parties are audible in the stored file.
  6. From the console: accept, mute/unmute, hold, hangup, and dial each round-trip to the phone.

Point the device at the backend first (persist.helpdesk.base_url, credentials): see docs/phone-connectivity.md. The production values target https://moje.al.army:8443/api/v1.

The single USB-C port

Runtime uses zero USB - the phone talks to the backend over the network and the headset splitter occupies the port. USB matters only for flashing and wired adb, and it physically conflicts with the headset. So: unplug the splitter for any fastboot work, and use wireless adb for the everyday dev loop (adb pair / adb connect in Developer options). Only a full OS reflash forces the cable back on. GrapheneOS friction to expect: wireless debugging doesn't cleanly survive reboots, and USB peripherals need "allow peripherals" in settings or the headset never enumerates.

For dev LAN discovery without pinning an IP, the backend host can advertise _helpdesk._tcp via avahi; docs/phone-connectivity.md has the resolution order. Enable NTP on the phone - the backend rejects requests with more than 300 s of clock skew.

Signed release and relock (not done yet)

The office device currently runs an unlocked userdebug build on test keys. Locking the bootloader requires generating our own signing keys (development/tools/make_key for the app/platform set, an AVB key via avbtool), building stallion-cur-user, signing with script/finalize.sh + script/generate-release.sh, provisioning avb_pkmd.bin, and only then fastboot flashing lock - after confirming the signed image boots and records while still unlocked. Never relock on test keys; that is the one real way to brick the device. Keys would be immutable after locking and must be kept in offline custody. When this happens, budget for a monthly re-flash cadence: GrapheneOS ships security updates on stable tags, and a self-signed build has to be rebuilt and re-flashed for each one. Expect attestation (Auditor-style) to report SelfSigned rather than the official keys.