Remote access — YouOS from your phone or another machine

YouOS runs locally on Apple Silicon by design — the model, the LoRA, your inbox corpus, your draft history all stay on your Mac. But the UI (/triage, /feedback, /stats, /settings) is just a FastAPI server; it can be reached from any device with a network route to your Mac.

This doc covers the Tailscale + PIN path, which is the recommended remote-access setup. It gives you a private network between your Mac and your phone (and any other devices on your Tailnet) without exposing anything to the public internet.

What you can do remotely

Action Where it lives
Read drafts the agent queued Gmail.com (after Push to Gmail Drafts)
Finish and send a drafted reply Gmail.com
Browse the /triage queue YouOS server (this doc)
Dismiss with a categorical reason YouOS server
Save as training pair (edit + capture for LoRA retrain) YouOS server
Check Agent health (sweep stats, dismissal rate, score histogram) YouOS server
Toggle flags at /settings YouOS server

The Gmail path works on any device without additional setup. The YouOS server requires a network route, which Tailscale provides.

Prerequisites

You don't need a paid plan — the free tier covers everything below.

Setup (one-time, ~2 minutes)

1. Find your Mac's Tailscale hostname

tailscale status | head -1

The first column is the IP, the second is the hostname. Use the hostname (e.g. bbots-mac-mini) — it's stable across reboots.

2. Set a PIN (required — protects the exposed UI)

# Stored HASHED (PBKDF2), never plaintext. Pass your real PIN as the argument.
youos config set-pin <PIN>

PIN can be any string — 6+ characters recommended. You'll be prompted for it once per device (then it's cookied).

Do not hand-edit server.pin in youos_config.yaml — a plaintext value there can never match (it's compared as a hash), locking you out. Use youos config set-pin (or POST /api/config/set-pin), which hashes it. The CLI applies on the next server start; the API endpoint takes effect immediately.

Don't skip this. Once you bind the server to a non-loopback interface, anyone on your Tailnet can reach it. The PIN is what limits access to you.

3. Bind YouOS to the Tailscale interface

youos config set server.host 0.0.0.0
youos config set tailscale.hostname bbots-mac-mini   # use your hostname from step 1

0.0.0.0 binds to all interfaces on your Mac. Tailscale routes Tailnet traffic to it; localhost still works from the Mac itself; LAN devices that aren't on your Tailnet are still blocked by your local firewall (assuming default macOS settings).

If you want stricter binding (only the Tailscale interface, not LAN at all), use your Tailscale IP from step 1: youos config set server.host 100.79.48.17. The trade-off is that your IP changes if Tailscale reassigns it (rare on free tier).

4. Restart the server

pkill -f "youos serve"
youos serve &
youos status

youos status should now print:

Tailscale:   ✅ http://bbots-mac-mini:8765

5. Open /triage on your phone

On your phone (with Tailscale running and connected to the same Tailnet):

http://bbots-mac-mini:8765/triage

Or use the IP form (http://100.79.48.17:8765/triage) if MagicDNS isn't resolving on your phone.

First visit prompts for the PIN. After that, it's cookied for ~30 days.

6. (Optional) Add to your phone's home screen

In Safari: tap the Share button → "Add to Home Screen". Names it whatever you want. From your home screen it opens like an app.

What's protected

What's not yet supported (gaps to know about)

Remote dismissal via Gmail labels

If /triage isn't reachable but Gmail is (the universal case — phone, friend's laptop, work web client), you can still dismiss a queued draft by applying a Gmail label to the original thread. You can also pick the dismissal reason by choosing which label you apply — YouOS/skip-wrong-content records the same categorical signal as the /triage dismiss selector's "Wrong content" choice.

Setup (once)

In Gmail web (or any client), create these labels (slashes nest them under a YouOS folder for tidiness):

Label Dismissal reason When to use
YouOS/skip noise Generic "the agent shouldn't have drafted this" (b57 default)
YouOS/skip-noise noise Same as above — explicit form
YouOS/skip-wrong-sender wrong_sender Right kind of mail, but the wrong person to reply to right now
YouOS/skip-wrong-content wrong_content The agent drafted but missed the point — drafting-quality signal, NOT a filter signal
YouOS/skip-handled already_handled I replied outside YouOS already
YouOS/skip-other other Catch-all when none of the above fit

You don't need to create all of them — only the ones you'll actually use. The agent only processes labels that exist; unknown labels are silently no-ops.

Usage

When you see one of the agent's drafts in Gmail Drafts that shouldn't have been drafted:

  1. Open the original inbound thread the draft replies to (not the Draft itself).
  2. Apply one of the labels above based on why you're dismissing.
  3. Next sweep (within agent.interval_minutes), the matching agent_pending_drafts row is marked dismissed with the corresponding reason, and the label is removed from the thread so it isn't reprocessed.

You can also run the sync immediately:

youos sync-labels                              # iterate ALL known categorical labels
youos sync-labels --account other@x.com       # specific account
youos sync-labels --label "YouOS/skip-noise"  # restrict to one label

The dismissal goes through the same path as /triage's dismiss button — counted in the dismissal-feedback aggregate, contributes to agent.auto_promote_skip_senders after 3 dismissals from the same sender (for noise-class), and surfaces on the next digest. The categorical reason flows through too: wrong_content dismissals from labels route to the LoRA training signal exactly like the /triage button.

What if I label something not in the queue?

Safe — sync-labels skips threads with no matching agent_pending_drafts row (e.g. an old thread you labelled before the agent saw it). The label stays applied; nothing happens.

Multi-account setup

YouOS handles multiple Gmail accounts on one machine. Each account runs through gog's stored OAuth tokens; the agent sweeps them sequentially on each scheduler tick.

Setup

  1. Authorize each account with gog (one-time per account):

bash gog auth login --account you@gmail.com gog auth login --account baher@work.example gog auth list # confirm both are listed

  1. Add both to user.emails:

bash youos config set user.emails 'you@gmail.com, baher@work.example'

  1. The scheduler now sweeps both accounts each tick (no agent restart needed — get_agent_config re-reads on every iteration).

What's per-account vs global

Item Per-account Global
agent_pending_drafts rows (the queue at /triage)
agent_audit rows (sweep history)
/api/agent/observability?account=X
Dismissal stats / candidates / promotion list
agent.enabled
agent.interval_minutes
agent.standing_instructions
agent.skip_senders ✓ (mute applies to both inboxes)
agent.daily_draft_cap ✓ (per-UTC-day quota applies per account)
agent.threshold / window / limit
agent.auto_promote_skip_senders
Standing instructions snapshot (column on each row) ✓ (snapshotted at draft time)

If you want different standing-instructions or skip-senders per account, that's a deliberate future feature — current design is "configure once, applies to both."

Selecting which accounts the scheduler sweeps

By default the scheduler picks up everything in user.emails. To restrict (e.g. pause one account temporarily without losing its identity entry):

youos config set agent.accounts 'you@gmail.com'   # sweep only you
youos config set agent.accounts ''                    # back to user.emails

Verifying multi-account sweeps

The /triage Recent activity table shows the Account column for each sweep. The digest CLI is per-account too:

youos digest --account you@gmail.com
youos digest --account baher@work.example

On a typical inbox, one account-sweep takes 20–45 seconds. With two accounts and a 1-minute interval, expect the loop to be busy most of the time and finish each tick around the 60s mark. Set agent.interval_minutes higher (5, 15) for less churn.

Daily digest email (poor-man's push notification)

youos digest prints a summary of the agent's recent activity — sweep counts, drafts pending vs pushed, dismissal rate by reason, auto-promoted senders, top noise-dismissed senders, and a clickable Tailscale link to /triage. Pipe it to mail via cron and you get a daily email digest of what the agent did while you were away.

youos digest                      # today, plain text
youos digest --days 7             # last week
youos digest --format html        # HTML for richer email rendering
youos digest --format json | jq . # structured for further processing

Cron recipe (sends an HTML digest every weekday at 7am):

# crontab -e
0 7 * * 1-5  cd ~/YouOS && ~/YouOS/.venv/bin/youos digest --format html | mail -s "$(date '+YouOS daily — %a %b %d')" -a 'Content-Type: text/html; charset=utf-8' you@yourmail.com

The -a 'Content-Type: text/html' flag is what makes mail send HTML instead of escaping it. Tested on macOS mail (which uses /usr/sbin/sendmail under the hood).

If you don't have mail configured, the digest also works fine piped to pbcopy (clipboard) or saved to a file you sync to your phone via iCloud Drive.

Troubleshooting

youos status doesn't show Tailscale URL. Check tailscale.hostname is set (youos config get tailscale.hostname) and that tailscale status lists your Mac.

Phone can resolve but can't connect. Confirm server.host is 0.0.0.0 (or your Tailscale IP), not 127.0.0.1. Restart the server after changing.

Phone resolves the hostname but is told connection refused. Server isn't running, or it's bound to loopback only. Check pgrep -af "uvicorn.*app.main:app".

PIN prompt loops. Cookie storage may be disabled on the phone browser. Try a different browser or check the browser's site-settings → cookies.

Tailscale shows the Mac as offline. Make sure Tailscale is actually running on the Mac (brew services start tailscale or the menubar icon). Tailscale sometimes pauses on macOS sleep — wake the Mac.