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.
132 lines
6.5 KiB
Markdown
132 lines
6.5 KiB
Markdown
# 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:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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.
|