Phone Agent¶
The phone-agent NixOS module wires volnix to a Galaxy S26 Ultra running a Termux-based MCP
server, turning the phone into a set of remote tools and sensors the laptop can call over Tailscale.
The module lives in
nixos/phone-agent/ and is
imported and enabled from
nixos/configuration.nix.
It provides four capabilities, each an independently toggleable systemd user service, plus a
phone-agent CLI for ad-hoc tool calls:
- MCP transport — HTTP MCP over Tailscale, bearer-token authenticated.
- Ingest — periodic pull of staged files off the phone, integrity-checked.
- Proximity lock — lock the laptop when the phone leaves the desk (lock only).
- Network routing — derive a routing profile from the phone's current SSID (opt-in, off by default).
Scope
This page documents the laptop side (the NixOS module declared in this repo). The phone-side
MCP server is built on-device with Claude Code and is out of scope here — see
nixos/phone-agent/README.md
and the phone repo's PHONE-ENV.md for that half.
Wiring¶
# nixos/configuration.nix
imports = [ ./phone-agent ];
phone-agent = {
enable = true;
phoneTailscaleIP = "100.101.229.9"; # from the Tailscale Android app
tokenFile = config.sops.secrets.phone_agent_token.path;
};
The bearer token is a sops-nix secret (phone_agent_token) materialized at
runtime; its value must match ~/.config/phone-agent/token on the phone. tokenFile is typed str,
not path, on purpose — a path literal would copy the secret into the world-readable Nix store. The
module asserts tokenFile is set.
Options¶
| Option | Default | Purpose |
|---|---|---|
enable |
false |
Master switch for the subsystem. |
phoneTailscaleIP |
100.101.229.9 |
Tailscale IP of the phone MCP server. |
port |
8462 |
Phone MCP server port. |
tokenFile |
(required) | Path to the sops bearer-token file. |
ingestDir |
/home/lowcache/ingest |
Laptop dir mirroring staged phone output. |
ollamaHost |
volnix |
Hostname the phone uses to reach this laptop's Ollama (documentation only). |
enableIngestSync |
true |
Periodic pull of staged files. |
enableIngestWatcher |
true |
Process staged files as they land. |
enableProximityLock |
true |
Lock on phone-away. |
enableNetworkRouting |
false |
SSID-derived routing profile. |
proximityIntervalSec |
5 |
IMU poll interval for proximity. |
allowUnlock |
false |
Experimental; logs intent only (see below). |
The phone-agent CLI¶
phone-agent <tool> [args-json] calls a phone MCP tool and pretty-prints the result (errors surface as
{error: …}):
phone-agent phone.system.ping
phone-agent phone.sensor.read_imu '{"sample_count":10}'
phone-agent phone.npu.transcribe '{"audio_path":"/tmp/test.wav"}'
Health check the transport directly with curl -sf http://<phoneTailscaleIP>:8462/health.
MCP gateway backend¶
The module writes a template to /etc/phone-agent/gateway-peer.example.yaml and emits a build-time
warning to register it — ~/.config/mcp-gateway/gateway.yaml is hand-managed, so the module surfaces
the values rather than editing it for you:
backends:
phone-agent:
transport: http
url: http://<phoneTailscaleIP>:8462/mcp
# Authorization: Bearer $(cat <tokenFile>)
namespace: phone
Ingest¶
phone-ingest-sync (oneshot + timer, every 2 min) pulls staged files off the phone via the
phone.ingest.list / phone.ingest.fetch MCP tools, base64-decodes each payload, verifies its
sha256, and only then moves it into <ingestDir>/staged/ (deleting the phone-side copy). It exits
silently if the phone is unreachable. phone-ingest-watcher is a systemd path unit watching
<ingestDir>/staged/*.json; new files trigger ingest-watcher.sh to process them.
Proximity lock¶
phone-proximity-daemon polls the phone's IMU every proximityIntervalSec. When the inferred motion
state transitions from on_desk/stationary to walking/in_pocket, it runs
niri msg action lock-screen.
Lock only — no auto-unlock
There is no safe programmatic unlock, so the daemon never unlocks. allowUnlock is experimental and
intentionally a no-op: it only logs unlock intent, it does not unlock the session.
Network routing¶
Off by default. When enabled, phone-network-routing reads the phone's current Wi-Fi SSID
(phone.sensor.read_modem), maps it to a home / untrusted / secure profile, and writes the result
to /run/user/$UID/phone-agent/network-profile, optionally starting a
phone-network-profile@<profile>.service.
Ceiling
Profile derivation and the runtime file are in place; wiring the profile through to the net-gate microvm is not yet implemented.