# PlumTV Fleet Manager + Self-Healing Torrent Mesh — Design Spec

> Captured 2026-06-08 from a design conversation. Forward-looking spec for unbuilt
> work; spans the PlumTV repos (see plum-tv-category-unification). Not yet implemented.

## Target audience

People who download media via **magnet/torrent links** — sophisticated, already run
clients, understand seeders/ratio/DHT. Implication: **expose the mechanics as features**
instead of hiding them. They already live in Discord (scene/tracker/game communities).

Positioning line:

> **A private tracker made of your friends — with the retention guarantee, minus the
> gatekeeping and the ratio police.**

The four painkillers (lead with these, not "social discovery"):
1. Dead magnets / zombies — mesh + re-source kills them.
2. Ratio anxiety / hit-and-run bans — custody floor automates good seeding.
3. Magnet roulette (results are corpses) — a friend with it is one F2F hop away.
4. Multi-device sprawl — the fleet model is what they cobble together by hand.

Competitive lane: *arr stack automates *acquisition*; nothing makes *availability*
social and self-healing. Win there, don't out-*arr the *arr stack.

Privacy: torrenting over Tor is the wrong mechanism (leaks real IP via the protocol,
harms the Tor network). Correct expression of the same instinct = F2F/darknet mesh over
WireGuard + VPN on the public-swarm leg.

## Two graphs on one Discord identity layer

- **Custody graph** — NARROW, trust-bounded (1° friends + always-on nodes). Holds the
  seeder floor. This is where the zombie-prevention guarantee lives.
- **Discovery/signal graph** — WIDE, six-degrees, anonymized + aggregated. Carries
  popularity signal and relays F2F requests. "Penetration" is a feature here.

Unifier = **friend-to-friend (darknet) routing** (Freenet darknet / RetroShare model):
you connect directly only to people you trust; requests + bytes relay hop-by-hop until a
holder is reached. Six-degrees *reach*, local *trust*, never peer with a stranger.

Trust as a distance gradient: 1° = full custodian peer; 2° = rate-limited pull via
relay; 3°+ = signal only unless vouched.

Discovery signal needs **k-anonymity** (suppress titles below K≈5 distinct watchers) and
**per-fleet dedup** (one person watching on plum then phone = one watch). Load-bearing
because of adult content in scope — a single-watcher leak fingerprints someone.

## Zombie prevention (custody floor)

The mesh prevents *future* zombies for pinned content; it cannot raise the already-dead
(that's the reaper's job — re-search for an alternate magnet).

Mechanism:
- **Watch = auto-seed with TTL** — every viewing adds a seeder. Content heals as it's used.
- **Replication floor enforced by the governor** — every wanted title keeps ≥N complete
  copies; drop to N−1 triggers a re-pin from a surviving seeder *before* the last vanishes.
- **Always-on backstop** in every floor (vps-0 / black / seedbox) — un-killable anchor.

Custodianship = **rolling baton, not a life sentence**:
- Newest downloader joins the floor; oldest is released (demoted to voluntary).
- A custodian reclaiming disk must **floor-check + hand off** before dropping — never
  breach the floor unilaterally.
- ≥1 floor slot reserved for an always-on node (laptops-only custodians = momentarily
  dead when asleep).

Zombie reaper (cron daemon by the governor): classifies every torrent
`healthy | stalled | dead`; for dead-but-wanted, recover from the **mesh first**, public
re-search as fallback. The mesh and reaper are the same system from two ends.

## The fleet is the atom

Unit is neither person nor device — it's a **fleet**. One Discord identity, typed devices.
- **Identity** = person.
- **Custody** = the *always-on subset* of the fleet (black, apricot, seedbox).
- **Signal** = per-fleet-deduped watch events.

A single fleet (black + apricot) already contains its own 2-node floor → **the system
works for one user with zero friends; the mesh is the same code with more identities.**
Build for the fleet, get the mesh for free.

Distributional truth: a typical 4-device user (laptop+phone+tablet+work-laptop) has ZERO
always-on nodes → net consumer. The **seedbox is how those fleets get a floor at all** →
"add a seedbox" is the most important onboarding step for non-power-users.

---

# Fleet Manager — Data Model

The fleet manager = the governor generalized again (same bandwidth-arbitration brain).

## Entities

```
Identity { discord_id, display_name, fleets[] }
Fleet    { id, identity, hosts[], sources[], custody_capacity }
Host     { ...registry... }
Source   { ...peer-source... }
```

`custody_capacity = Σ over always_on hosts of (disk_free × uptime_score)`
— says whether a fleet can hold a floor or is a net consumer.
Gift-economy mode ignores it for prioritization; ratio mode weights by it.

## Host registry

```
Host {
  id
  fleet_id
  class:       server | roamer | consumer | seedbox | broadcast
  reachable:   home_lan | wireguard | public_ip
  always_on:   bool
  on_home_ip:  bool                 # true = public-swarm traffic exposes home connection
  api:         transmission_rpc | qbittorrent | utorrent_web | none
  capacity:    { disk_free, up_bw, uptime_score }   # uptime_score ∈ [0,1], rolling
  duties:      Duty[]               # assigned by manager, not hardcoded
}
Duty = custody_floor | public_swarm_face | f2f_relay | broadcast
```

Class = what a device IS; duty = what the manager tells it to DO.

Device-class examples (Natalie's fleet): black = server, apricot = secondary always-on,
plum = roamer (TTL seeder), phone = consumer (PURE SINK — never any duty).

## Duty-assignment rules (deterministic, capability-driven, run on registry change)

| Duty                | Eligibility                                | Rule |
|---------------------|--------------------------------------------|------|
| `broadcast`         | `public_ip && always_on`                   | exactly ONE per fleet (or shared pool); prefer seedbox > vps-0 > server |
| `f2f_relay`         | `always_on && reachable ∈ {wireguard,public_ip}` | broadcast host + any other always-on server |
| `public_swarm_face` | prefer `!on_home_ip`                        | seedbox FIRST; never a consumer; never on_home_ip if an off-home option exists |
| `custody_floor`     | `always_on && disk_free > title_size`       | N most-recent eligible holders; ≥1 slot reserved for always-on node |

Invariants:
- A `consumer` (phone) NEVER receives any duty. Checked first.
- `public_swarm_face` prefers off-home-IP so home connections stay dark.
- Every active title's floor keeps ≥1 always-on holder.

## "Global broadcast server" duties

Pin to the most-reachable always-on node (seedbox or vps-0):
1. F2F rendezvous / relay anchor (NAT'd members + friends find each other through it).
2. Holds the aggregated peer/seeder registry.
3. Runs the bot / Discord bridge.
4. Public-swarm face (optionally the ONLY node touching public swarms — keeps fleet dark).

## Peer-source model — the holistic seeder list + the policy that contains it

```
Source {
  id
  fleet_id
  kind:            dht | public_tracker | private_tracker | friend_mesh | fleet_host | seedbox
  api/creds:       <opaque, encrypted at rest>
  share_policy:    search_only | content        # private_tracker DEFAULT-CLOSED (search_only)
  swarm_isolation: f2f_only | open              # private_tracker FORCED to f2f_only
}
```

Broadcast host answers `peers_for(infohash) → Peer[]` by unioning every permitted source:

```
Peer { addr, source_kind, source_id, served_via: public | wireguard }
```

Holistic list = your privates ∪ your fleet ∪ friends' fleets ∪ seedbox ∪ DHT/public —
a union no single tracker can assemble. This is the product magic: **a user-owned
meta-tracker.** Provenance-tagged so the UI shows *why* a peer exists and over what transport.

### Private-tracker "mixing" — the blast radius

"Busting out of private" bundles two operations with very different consequences:
- **search_only** (a release exists, here's the infohash) — low/moderate risk, sharing info.
- **content** (serve bytes / hand over the private .torrent) — ACCOUNT-KILLER, and the
  consequence lands on the SOURCE user, not the friend.

Non-obvious technical reality: the private `.torrent` has the **passkey embedded**. If a
friend's client announces to the private tracker → tracker sees the passkey on an
unregistered peer → **source user permanently banned for passkey sharing.** Cross-seed
detection + retention monitoring also point at the seeder.

Two policy gates (both load-bearing):
1. `share_policy` gates the registry merge. Private = `search_only` by default; flipping to
   `content` triggers a consequence-explicit warning naming the tracker and that the
   source's account is what burns.
2. `swarm_isolation = f2f_only` FORCED + un-overridable for private sources. Private content
   is re-hosted into the fleet's OWN WireGuard swarm; the friend pulls `served_via:
   wireguard` and NEVER announces to the private tracker. Structural kill-switch for the
   passkey-share instant-ban.

> The architecture fully shields the FRIEND but cannot shield the SOURCE — the sharer is
> always the leak. So content-share is opt-in, default-closed, eyes-open.

Seedbox source by contrast = `content` + `open` by default. Clean, no gates.

## Two derived outputs (everything above exists to produce these)

```
peers_for(infohash)  → Peer[]      # the holistic seeder list / meta-tracker
custodians_of(title) → Host[]      # who's obligated to keep it alive
```

---

# Resolved product decisions (2026-06-08)

## A. Gift-economy vs. ratio → SHIP GIFT-ECONOMY, build the knob, leave it off

Ratio enforcement contradicts the "minus the ratio police" positioning. Ratio anxiety is
the pain being sold against.
- Default: gift-economy, no enforcement.
- Build `custody_capacity` weighting but leave it disabled; enable per-GROUP only when a
  group outgrows trust and server-havers feel the asymmetry.
- First lever is **visibility, not enforcement** — show contribution (disk pledged, bytes
  served, titles custodied) without gating. Social pressure > mechanical bans in a trust
  group. Hard ratio = escalation of last resort, per-group, never default.

## B. Warn vs. fingerprint-strip on private sharing → WARN HARD, STRIP NEVER (default)

Stripping = re-encoding = destroys the encode quality that makes a private release
valuable. Defeats its own purpose.
- Friend protected structurally by `f2f_only` isolation.
- Source protected by an explicit warning at content-share flip (the warning IS the
  protection — don't paper over with false safety from stripping).
- Private-sourced titles EXCLUDED from broadcast/global-trending signal + higher
  k-anonymity threshold in personal-network signal (trending private release = tips off
  tracker staff).
- Strip offered only as a per-release opt-in, labeled "re-encodes, loses quality." Never auto.

---

# Build order (de-risked; each stage independently shippable)

1. Host registry + duty assignment, SINGLE FLEET (black+apricot+plum+phone). No mesh, no
   Discord. Unifies what's run by hand today.
2. Seedbox source + `public_swarm_face` duty. Adds always-on custodian; proves the source
   model with the zero-risk source first.
3. `peers_for()` over local sources (fleet + seedbox + DHT/public). Meta-tracker, single-fleet.
4. Friend-mesh source + F2F relay. Federates; custody floor goes cross-fleet.
5. Private-tracker source, default-closed. LAST — highest consequence; ship only once the
   f2f isolation path is battle-tested on safe sources.

Discord planes (keep separate):
- Control/ops + QA/community + release announcements → project-owned home server. Fine.
- Content/availability (magnets, custody signal, trending-among-friends) → NEVER the public
  home server (Discord ToS + ties project identity to distribution + adult-content rules).
  Rides the private mesh + the bot inside users' OWN servers.
- **Discord is a surface you notify, not a backbone you route through** — custody/reaper/F2F
  must run over WireGuard regardless; a Discord ban must not take the mesh down.

Watch parties: synchronized LOCAL playback (Discord carries only voice + timestamps), never
stream copyrighted video through Discord.

---

# Networking — two independent planes (added 2026-06-09)

Two VPN/overlay concerns that are **separate systems on separate interfaces** and must
never be conflated or allowed to collide. Plane 1 connects *your own devices to each
other*; plane 2 is *how a public-swarm-facing node leaves the building*.

```
                fleet WireGuard (tva0)            commercial VPN (.ovpn → tun)
 plum  ───────────────┐                                   ┌────────────────────►  public swarms
 black ───── internal mesh, scoped ──── always-on node ───┤  (only public_swarm_face egress)
 phone ───────────────┘    AllowedIPs = fleet subnet only  └────────────────────►
        every device gets ONE stable fleet IP,             home IP stays dark
        reachable home OR away                              (off-home exit)
```

## Plane 1 — Fleet WireGuard (internal mesh): auto-provisioned, tv-anarchy-only, collision-proof

**Why foundational, not stage-4.** Today the app juggles black's home-LAN address
(`10.0.0.11`) vs an overlay address (`10.9.0.4`) and breaks when a device is away
(see [[black-endpoint-lan-vpn]] — the LAN→VPN transmission fallback shipped 2026-06-09
is the *interim* stopgap). A fleet WG fabric gives every device **one stable fleet IP,
reachable home or away**, which collapses `devices.json` to a single IP per device and
removes the juggling entirely. So WG-fabric is promoted to an **early foundational
stage** (before friend-mesh), even though cross-fleet F2F still lands at stage 4.

**Split brain: the package decides, a thin per-platform actuator brings the interface up.**
The fleet package owns all logic — generates each device's Curve25519 keypair (REUSED as
the device's mesh identity, per the anchor-trust model), assigns a stable fleet IP,
distributes the peer list via the broadcast/rendezvous anchor (the public-IP always-on
node), and writes the interface config. Raising the interface is per-OS:
- **Linux** (black, apricot, seedbox): `wg-quick` + a `systemd` unit, root, fully
  scriptable end-to-end. Buildable first.
- **macOS** (plum): no kernel WireGuard — wireguard-go over a named `utun`. The app
  shells `wg-quick up` **under sudo** (same precedent as the mpv `"sudo": true` path in
  `devices.json`); a sandboxed app cannot otherwise raise an interface. The design states
  this explicitly rather than assuming `wg-quick up` "just works".
- **iOS** (phone): auto-provisioning is not possible without the WireGuard app + manual
  import/MDM. Phone is **manual-config / out of the auto-path** — it's a pure consumer
  sink anyway and never holds a duty.

**tv-anarchy-only — a scoped mesh, NOT a VPN.** `AllowedIPs` = the **fleet subnet only**
(never `0.0.0.0/0`), `Table=off`, no default route, no DNS takeover. Only traffic *to
fleet peers* enters the tunnel; the user's general internet and any other VPN are
untouched.

**"Never conflict" is a runtime probe, not a lucky default** (the user said *never*):
at bring-up the actuator enumerates existing routes/interfaces (`netstat -rn` on macOS /
`ip route` + `ip link` on Linux), picks a fleet subnet that overlaps **nothing** present
(then persists it for stability), auto-picks a free UDP listen port (51820 may be the
user's other WG), and uses a dedicated interface name (`tva0`). It refuses to clobber an
existing interface/route and shifts its own subnet instead.

**Single-fleet trust bootstrap is trivial** — the user owns every device, so NO M-of-N
anchor co-signing (that's the cross-fleet stage-4 problem). Keys are minted and trusted
locally within the one fleet.

**Open question (blocks finalizing this plane):** what is `10.9.0.4` today — a
general-purpose VPN/WG the user runs for *other* infra (→ fleet WG is purely additive and
must never route through it), or just an ad-hoc tv-anarchy overlay (→ fleet WG *replaces*
it; a migration)? The answer changes whether plane 1 coexists-with or supersedes the
current overlay.

## Plane 2 — Commercial VPN via uploaded `.ovpn` (the public-swarm exit)

This is the "+ VPN on the public-swarm leg" from the privacy note (line 28) and the
mechanism behind the `public_swarm_face` duty (a node — seedbox FIRST, never a consumer,
prefer `!on_home_ip` — is the only one touching public swarms, keeping the rest of the
fleet dark).

**App setting — VPN configs (import + manage):**
- **Import** individual `.ovpn` files OR a `.zip` bundle. Providers (Mullvad, PIA, AirVPN,
  ProtonVPN, …) ship a **zip of per-server `.ovpn` files** (one per location); on zip
  import, unpack every `.ovpn` entry (recurse nested folders), ignore non-ovpn files.
- **Parse** each config for: display name (filename, or a `#`-comment / the `remote`
  host), `remote` endpoint(s) + `proto` (udp/tcp), and whether certs/keys are **inline**
  (`<ca>…</ca>`, `<cert>`, `<key>`, `<tls-auth>`) or **sidecar files** shared across the
  bundle (a zip often carries one `ca.crt` + an `auth-user-pass` reference for all
  servers). Keep sidecars together with the configs that reference them.
- **Credentials** (`auth-user-pass`): a secure username/password field stored in the
  **Keychain**, never written beside the config in plaintext.
- **Manage:** a single managed dir (`~/.config/tv-anarchy/vpn/`); list / rename / group by
  provider / delete; pick the active config(s) for the device(s) holding
  `public_swarm_face`. Show parsed endpoint/proto/location so the user picks sensibly.

**Actuation (where it runs):** `public_swarm_face` lives on an always-on, ideally
off-home node (seedbox / black — Linux), so OpenVPN bring-up is a Linux concern
(`openvpn` + systemd, root). The egress is **split-routed** — only the torrent client's
public-swarm traffic leaves through the `tun` interface (policy routing / a network
namespace on Linux), so it is **not** a full-device VPN and the box stays reachable on
its normal + fleet interfaces.

## Coexistence — the two planes run simultaneously without collision

A `public_swarm_face` node runs **both** at once: fleet WireGuard on `tva0` (internal
mesh, scoped `AllowedIPs`) AND a commercial-VPN `tun` (public-swarm egress, split-routed).
They occupy different interfaces and different routing scopes:
- fleet-peer traffic → `tva0` (WG)
- public-swarm torrent egress → `tun` (.ovpn)
- everything else → the box's normal route
No interface, subnet, port, or default-route overlap — by the runtime collision-probe
(plane 1) + split-routing (plane 2). This is the structural guarantee behind the user's
"never conflict with other wg or vpn".
