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.
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
grepfunction wrapping ugrep, which breakslunch's product parsing. Before sourcing the build env, rununset -f grepand putPATH=/usr/bin:/bin:$PATHfirst. - Build-matched adb/fastboot are at
/build/grapheneos/out/host/linux-x86/bin/. - An incremental
m Dialerafter 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.INFOshows 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:
- Ring →
POST /calls/incomingarrives, screen-pop shows on the console. - Answer →
POST /calls/answered; recording starts within ~500 ms, with no consent dialog. - Hang up →
POST /calls/endedwith a sensible disconnect cause. PUT /recordings/{uuid}→ 200, and the event reachesrecording_uploaded, thentranscribed, thensummarisedon the console.- Both parties are audible in the stored file.
- 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.