M17: illogical control (accounts, devices, enrollment, directory) #29

Open
opened 2026-10-02 01:51:18 +00:00 by jhgaylor · 0 comments
Owner

Depends on: #28
Blocks: #30, #33

From PLAN.md, "Control track → M17: illogical control (accounts, devices, enrollment, directory)".


  • crates/control, the illogical-control binary: axum, SQLite (Postgres optional for the hosted one), and the same release builds as the daemon. illogical-control --domain control.example.com serves the API and the web client.
  • Accounts:
    • sign in with GitHub, Google or a passkey;
    • a personal space by default; teams come in M19.
  • Devices:
    • each browser, phone and CLI gets a device key at sign-in;
    • the first device is trusted on enrollment;
    • every later one shows "approve this device?" on an existing device, with a fingerprint to compare (the trust rule above);
    • recovery codes.
  • Daemons:
    • illogicald join https://control.example.com prints a code; approving it on a device enrolls the daemon to your account;
    • a daemon has its own key;
    • the M4 per-host tokens are minted by control from now on;
    • illogicald leave removes it.
  • Directory:
    • control keeps the host list: your daemons, last seen, and how to reach each one (direct URL or relay);
    • it replaces the home daemon's hosts.rs list for enrolled daemons;
    • the page fetches the list from control and caches it, so known hosts stay reachable while control is down (the same rule as today);
    • geek stops being special.
  • Daemon auth:
    • the daemon accepts a client that presents an approved device key for an account with access (personal: only you);
    • tailnet identity still works for tailnet users;
    • enforcement stays on the daemon, using M12's principals once they exist.
  • The web client:
    • served by control (and still by each daemon);
    • signs in, shows the directory, and connects straight to daemons over the tailnet or the relay (M18).
  • Self-hosting: documented in the README with a single binary and Caddy or tailscale serve in front. No feature is hosted-only except billing (M22).
  • Done when:
    • a stranger with a Mac and a Linux box, and no Tailscale, signs up with GitHub and enrolls both daemons;
    • the page lists both;
    • adding a phone needs approval from the laptop;
    • a self-hosted control on a VPS does the same.

Track overview and decisions

The daemon is the WireGuard: a useful piece of technology for one person
on their own network. This track is the Tailscale: a central service that
makes it work for people who have never heard of a tailnet, and for teams.

The code already draws the line. M4's home daemon is "a directory and
control point, never a relay": it holds the host list and provider tokens,
mints per-host tokens, and receives dial-out connections and log sync.
That is a coordination server running on geek. This track moves that role
into its own program, illogical control (illogical-control), which
anyone can run and which we also host. Then it adds what a single home box
can't do: accounts, reaching machines behind NAT, teams, push and hosted
compute.

Decisions (2026-10-01):

Question Decision Why
Who it's for first Small teams sharing sessions. A few people pairing and watching each other's agents. Sharing is where a central service adds the most. It needs accounts and a relay anyway, so solo use comes along for free.
Can terminal content pass through the service in the clear? Never. Output, input, scrollback, snapshots, history and push payloads are end-to-end encrypted. The service sees metadata only (who, which host, when, sizes). It's shell access. A blanket promise is the trust story, and E2E can't be retrofitted.
Self-hosting From day one. illogical-control is open source, in this repo, and the hosted one runs the same code. Keeps the promise checkable. The business is hosting, compute and teams, not lock-in.
Pricing Free for one person; per seat for teams; sandboxes by usage. Relay traffic is included, with fair-use caps. Tailscale's shape: strangers try it free, teams pay for what teams need, compute costs what it costs. Self-hosted control has no billing.
Whose machines a team's sessions run on Members' own daemons, team-owned daemons, and hosted VMs. A team can enroll shared machines (a build box, a staging server) that belong to the team, not a person. Teams have shared machines. M14's rule still holds: guests type in VMs unless trusted.
M15 (Funnel invites, per-daemon GitHub sign-in, read-only links) Superseded by M19. Invites, sign-in and read-only links move to control. M15 solves per daemon what control solves once. M12–M14 carry over: they're the per-daemon enforcement control relies on.
The tailnet Still a first-class path. Tailnet users can skip control entirely, or enroll and keep direct tailnet connections. Control adds; it doesn't replace.

What control knows and doesn't:

  • Knows (metadata):
    • accounts, teams, members and roles;
    • devices and daemons, and their public keys;
    • the directory: hosts, sessions, tab and pane ids, names, presence;
    • who connected to what and when, and byte counts;
    • audit entries.
  • Never has:
    • terminal bytes, snapshots or logs in the clear;
    • keys that decrypt them;
    • provider tokens for your own machines (those stay on your daemons).
  • Names are metadata. Session and tab names, and the pane titles shown in the directory, are visible to control. The README says so, and a per-team switch keeps names on daemons only (then the directory shows ids).

Trust. The service distributes public keys, so a malicious control server could add a device of its own and read what it's sent. As with Tailscale's Tailnet Lock, a new device must be approved by one of the user's existing devices (the first is trusted on enrollment). Daemons only encrypt to devices carrying that approval, and team membership changes are signed by a team owner's device. Control can refuse service, but it can't read.

Order:

  1. S15, then M17 and M18: accounts, directory, relay and E2E.
  2. M19, teams. It builds on M12 and M13; strangers can use illogical together from here.
  3. M20, sandboxes, and M21, push, in either order.
  4. M22, billing, when there's something to charge for.

The launch issues (#19–#27: licence, releases, install, quickstart) come first: control is worth little if strangers can't install the daemon.

Later, not planned yet:

  • encrypted history in control, with retention and cross-machine search run on clients;
  • the hosted MCP endpoint (M16 over the relay, with scoped tokens);
  • SSO/SCIM and policy;
  • a native mobile app.
**Depends on:** #28 **Blocks:** #30, #33 _From PLAN.md, "Control track → M17: illogical control (accounts, devices, enrollment, directory)"._ --- - **`crates/control`, the `illogical-control` binary:** axum, SQLite (Postgres optional for the hosted one), and the same release builds as the daemon. `illogical-control --domain control.example.com` serves the API and the web client. - **Accounts:** - sign in with GitHub, Google or a passkey; - a personal space by default; teams come in M19. - **Devices:** - each browser, phone and CLI gets a device key at sign-in; - the first device is trusted on enrollment; - every later one shows "approve this device?" on an existing device, with a fingerprint to compare (the trust rule above); - recovery codes. - **Daemons:** - `illogicald join https://control.example.com` prints a code; approving it on a device enrolls the daemon to your account; - a daemon has its own key; - the M4 per-host tokens are minted by control from now on; - `illogicald leave` removes it. - **Directory:** - control keeps the host list: your daemons, last seen, and how to reach each one (direct URL or relay); - it replaces the home daemon's `hosts.rs` list for enrolled daemons; - the page fetches the list from control and caches it, so known hosts stay reachable while control is down (the same rule as today); - geek stops being special. - **Daemon auth:** - the daemon accepts a client that presents an approved device key for an account with access (personal: only you); - tailnet identity still works for tailnet users; - enforcement stays on the daemon, using M12's principals once they exist. - **The web client:** - served by control (and still by each daemon); - signs in, shows the directory, and connects straight to daemons over the tailnet or the relay (M18). - **Self-hosting:** documented in the README with a single binary and Caddy or `tailscale serve` in front. No feature is hosted-only except billing (M22). - **Done when:** - a stranger with a Mac and a Linux box, and no Tailscale, signs up with GitHub and enrolls both daemons; - the page lists both; - adding a phone needs approval from the laptop; - a self-hosted control on a VPS does the same. --- ### Track overview and decisions The daemon is the WireGuard: a useful piece of technology for one person on their own network. This track is the Tailscale: a central service that makes it work for people who have never heard of a tailnet, and for teams. The code already draws the line. M4's **home daemon** is "a directory and control point, never a relay": it holds the host list and provider tokens, mints per-host tokens, and receives dial-out connections and log sync. That is a coordination server running on geek. This track moves that role into its own program, **illogical control** (`illogical-control`), which anyone can run and which we also host. Then it adds what a single home box can't do: accounts, reaching machines behind NAT, teams, push and hosted compute. **Decisions (2026-10-01):** | Question | Decision | Why | |---|---|---| | Who it's for first | **Small teams sharing sessions.** A few people pairing and watching each other's agents. | Sharing is where a central service adds the most. It needs accounts and a relay anyway, so solo use comes along for free. | | Can terminal content pass through the service in the clear? | **Never.** Output, input, scrollback, snapshots, history and push payloads are end-to-end encrypted. The service sees metadata only (who, which host, when, sizes). | It's shell access. A blanket promise is the trust story, and E2E can't be retrofitted. | | Self-hosting | **From day one.** `illogical-control` is open source, in this repo, and the hosted one runs the same code. | Keeps the promise checkable. The business is hosting, compute and teams, not lock-in. | | Pricing | **Free for one person; per seat for teams; sandboxes by usage.** Relay traffic is included, with fair-use caps. | Tailscale's shape: strangers try it free, teams pay for what teams need, compute costs what it costs. Self-hosted control has no billing. | | Whose machines a team's sessions run on | **Members' own daemons, team-owned daemons, and hosted VMs.** A team can enroll shared machines (a build box, a staging server) that belong to the team, not a person. | Teams have shared machines. M14's rule still holds: guests type in VMs unless trusted. | | M15 (Funnel invites, per-daemon GitHub sign-in, read-only links) | **Superseded by M19.** Invites, sign-in and read-only links move to control. | M15 solves per daemon what control solves once. M12–M14 carry over: they're the per-daemon enforcement control relies on. | | The tailnet | **Still a first-class path.** Tailnet users can skip control entirely, or enroll and keep direct tailnet connections. | Control adds; it doesn't replace. | **What control knows and doesn't:** - **Knows (metadata):** - accounts, teams, members and roles; - devices and daemons, and their public keys; - the directory: hosts, sessions, tab and pane ids, names, presence; - who connected to what and when, and byte counts; - audit entries. - **Never has:** - terminal bytes, snapshots or logs in the clear; - keys that decrypt them; - provider tokens for your own machines (those stay on your daemons). - **Names are metadata.** Session and tab names, and the pane titles shown in the directory, are visible to control. The README says so, and a per-team switch keeps names on daemons only (then the directory shows ids). **Trust.** The service distributes public keys, so a malicious control server could add a device of its own and read what it's sent. As with Tailscale's Tailnet Lock, **a new device must be approved by one of the user's existing devices** (the first is trusted on enrollment). Daemons only encrypt to devices carrying that approval, and team membership changes are signed by a team owner's device. Control can refuse service, but it can't read. **Order:** 1. **S15**, then M17 and M18: accounts, directory, relay and E2E. 2. **M19, teams.** It builds on M12 and M13; strangers can use illogical together from here. 3. **M20, sandboxes**, and **M21, push**, in either order. 4. **M22, billing**, when there's something to charge for. The launch issues (#19–#27: licence, releases, install, quickstart) come first: control is worth little if strangers can't install the daemon. **Later, not planned yet:** - encrypted history in control, with retention and cross-machine search run on clients; - the hosted MCP endpoint (M16 over the relay, with scoped tokens); - SSO/SCIM and policy; - a native mobile app.
Sign in to join this conversation.
No description provided.