Rename setups and add provider reauthentication

This commit is contained in:
iris committed 2026-09-12 22:56:43 -04:00
1 parent e9a0f1b9da
commit 7d9df5d572
36 files changed
+1866 -684

No files matched your search

+59 -27
View File
@@ -33,7 +33,7 @@ backend (Rust/Axum, desktop)
│ ├─ LlamaDriver (llama-server over HTTP)
│ └─ EchoDriver (the test rig)
│ each driver's process is spawned through a Transport,
│ locally or as `ssh host …`, decided by the setup it names
│ locally or as `ssh host …`, decided by the configured machine
├─ usage.rs (provider usage meters, per machine)
├─ models.rs (HuggingFace browsing and GGUF downloads)
├─ files.rs (the file explorer's half of the backend)
@@ -42,11 +42,22 @@ backend (Rust/Axum, desktop)
## Architecture
### Setups and providers (2026-08-28)
### Machines and providers (2026-08-28; terminology corrected 2026-09-12)
**A setup is a machine, and it carries the providers that machine has.**
Optional ssh details, plus the list of what can be run there. Spawning is
two choices in order: pick a setup, then pick one of its providers.
**A machine is an execution environment, and it carries the providers that
environment has.** It may be a physical computer, a VM, or an SSH target;
"host" is reserved for the address used to reach one. A session is therefore
the pair of a machine and one provider installed there. Optional ssh details,
plus the list of what can be run there. Spawning is two choices in order: pick
a machine, then pick one of its providers.
The first implementation called this whole object a "setup", even though the
word was originally meant to name the machine/provider pair. The public model,
Machines tab, source names and HTTP surface now consistently say `machine` and
`/machines`. Existing RON accepts `setups` and a session's `setup` as read-only
aliases so an update cannot orphan configured environments or conversations;
the next write uses `machines` and `machine`. There is deliberately no legacy
HTTP alias: the server and APK are versioned together.
This replaced an independent providers × hosts cross-product, because the
two axes are not independent: a provider is only real on a machine where
@@ -55,7 +66,7 @@ work — `claude-cli` on a machine with no `claude`, and every provider paired
with a host the driver ignores (`EchoDriver` takes no host, so "Run on" was
a control that silently did nothing).
- **Echo is seeded, not implicit.** It lives in the setup with no ssh,
- **Echo is seeded, not implicit.** It lives in the machine with no ssh,
because it runs in-process and has no transport to cross. It is written
into `config.ron` on first run rather than conjured at read time — a
provider nobody can see in the file is one nobody can edit from the phone.
@@ -78,7 +89,7 @@ axum 0.8, axum-server + rustls, tokio, serde, clap, tracing. Rust edition
comment is the surface's source of truth**; this file does not repeat it.
- `auth.rs` — the bearer-token middleware.
- `config.rs` — the persisted schema.
- `setups.rs` — machines and provider discovery.
- `machines.rs` — machines and provider discovery.
- `files.rs` — the file explorer (`EXPLORER.md`).
- `usage.rs` — provider usage polling, per machine.
- `models.rs` — HuggingFace browsing and GGUF downloads.
@@ -223,16 +234,16 @@ is applied once Codex supplies that turn's id, so it cannot leak forward and
hide a later failure.
Codex subscription limits come from the CLI's `account/rateLimits/read`
app-server request on the machine whose setup runs Codex. This keeps login and
app-server request on the machine that runs Codex. This keeps login and
token refresh inside the CLI. Its primary and secondary windows are normalized
into the existing usage snapshot shape, under provider name `codex`, so the
phone and auto-resume need no Codex branch. This is the CLI's local protocol
and is treated defensively for the same reason as Claude's undocumented usage
endpoint: missing fields or a refusal degrade to an unavailable snapshot.
The model picker asks the selected setup's Codex app-server for `model/list`
The model picker asks the selected machine's Codex app-server for `model/list`
when it is opened. The catalog is account- and CLI-version-specific, so it is
never copied into the app or inferred from another setup; a lookup failure is
never copied into the app or inferred from another machine; a lookup failure is
shown as unavailable while the free-text escape remains. Permission choices
are likewise reported per provider: Codex offers its read-only,
workspace-write and full-access modes, while Claude keeps its own modes.
@@ -249,7 +260,7 @@ not exist on that machine. `thread/tokenUsage/updated.last.inputTokens` is the
measured context (cached input is already included), while `last.totalTokens`
remains the turn's usage. If an older common transcript has no such event yet,
the server seeds the same measurement from the last `token_count` in Codex's
own rollout, including when that rollout is on an SSH setup.
own rollout, including when that rollout is on an SSH machine.
App-server's `item/agentMessage/delta` notifications are provisional: safety
buffering can revise their words before `item/completed` supplies the durable
@@ -270,7 +281,7 @@ deliberate and easy to undo by accident:
when the process restarts. That leaves the Claude driver as the odd one
out rather than this one — the CLI's memory is a cache in front of the same
transcript. Resolve any inconsistency in this direction.
- **A llama session runs on whatever machine its setup names** (2026-09-04,
- **A llama session runs on its configured machine** (2026-09-04,
the last of phase 5). A transport is "run this" plus "reach this port", and
the second half is `Transport::reserve_port` — the port the server binds
*there* and the port that reaches it *here*, the same number locally —
@@ -281,12 +292,12 @@ deliberate and easy to undo by accident:
avoids racing the bind anyway; a collision is not silent, since the server
fails to bind and the readiness poll reports what its log said.
- **The model file lives on the machine that serves it** (2026-09-04). Each
setup names its own models directory (`SshConfig::models_dir`, default
machine has its own models directory (`SshConfig::models_dir`, default
`~/.local/share/ai-app/models` expanded *there*), and a spawn resolves the
key on that machine — one round trip answering "at /abs/path" or "missing",
so a model that is not there is refused at the spawn rather than becoming a
server that never becomes ready. The spawn screen offers
`GET /setups/{id}/models`, that machine's list, rather than `GET /models`,
`GET /machines/{id}/models`, that machine's list, rather than `GET /models`,
which is this backend's downloads. Downloading *to* another machine is
deliberately not built: a multi-gigabyte transfer with no progress
anywhere, and the file gets there however anything else on that machine
@@ -344,7 +355,7 @@ deliberate and easy to undo by accident:
upload is streamed to disk under the session's attachments on this machine,
and the message ends with `Attached file: /abs/path`. For a session on
another machine the upload also copies the file there in the same request,
over one `ssh` invocation, landing in the setup's `attachmentsDir` if set,
over one `ssh` invocation, landing in the machine's `attachmentsDir` if set,
else the session's cwd, else the login home. The resolved remote path is
recorded beside the file (`<name>.remote`) and is what the driver names. A
copy that fails fails the upload, so no message ever names a file that is
@@ -611,7 +622,7 @@ prompt id, and the adopted copy billed for re-reading all of it. It ended at
the account's session limit.
**Importing and deleting run on the server, and a batch is handed over in one
call.** `POST /setups/{id}/importable/{delete,import}` each take a list of
call.** `POST /machines/{id}/importable/{delete,import}` each take a list of
ids, answer 202, and do the work in spawned tasks — the phone that asked is
free to leave, and used to cancel its own batch by doing so. A list rather
than a route per session because one request per row made a handover only as
@@ -679,15 +690,36 @@ account being billed — and in the layout this aims at, `ai-server` is on the
host, the host has no `claude`, and the CLI machine is a remote. So
credentials are read through the session `Transport` (`$HOME` expanded by the
far shell, because a path built locally is the wrong home), one snapshot per
setup that offers Claude. The HTTP call stays on the backend, so the far end
machine that offers Claude. The HTTP call stays on the backend, so the far end
needs nothing but a shell.
The snapshot says which of four things happened rather than carrying a flag
and a message: `ok`, `notLoggedIn`, `unreachable`, `failed`. `notLoggedIn` is
The snapshot says what happened rather than carrying a flag and a message:
`ok`, `notLoggedIn`, `authenticating`, `loginRequired`, `unreachable`, or
`failed`. `notLoggedIn` is
the one that matters — a machine nobody put an account on is working as
configured, and collapsing it into an error string made a healthy setup read
configured, and collapsing it into an error string made a healthy machine read
as broken. A machine with no Claude provider is not asked at all.
**Refresh and sign-in are serialized per machine and metered provider
(2026-09-12).** OAuth credentials rotate in a store shared by every CLI process
on that environment. Concurrent `/usage` requests used to be able to run two
refresh probes against the same file, while globally serializing would make an
unreachable machine stall unrelated accounts. `UsageMonitor` therefore has one
gate for each `(machine id, usage provider)` and rechecks the cache after taking
it. A concurrent read serves the last answer when there is one; a first read
waits for the single producer. Different machines and providers proceed
independently.
When Claude cannot renew an expired login, the phone offers sign-in from both
the Machines tab and the usage dialog. The backend starts the configured
Claude CLI's headless `auth login` on that machine, returns only its Anthropic
authorization URL, and accepts the one code copied back from the browser. The
CLI remains the OAuth client and the only credential writer: the backend keeps
the URL and code only for the live attempt and never sees or persists an access
token or refresh token. An explicit login holds
the same machine/provider gate as usage refresh, is cancelable, expires after
ten minutes, and is killed when the backend stops.
**The five-hour window has no reset time between blocks, and that is not a
missing value.** Measured 2026-08-31: the API anchors the window to the block
it started in, and when no block is running there is nothing to reset, so
@@ -890,7 +922,7 @@ time.
### The file explorer (2026-09-03)
**`EXPLORER.md` holds this design.** The one-line version: a machine's
filesystem, seen from the phone through the backend, keyed on the **setup**
filesystem, seen from the phone through the backend, keyed on the **machine**
rather than on a session (a session only says where to start), with every
operation one fixed shell script run through `Transport` so the local and the
ssh case are one implementation.
@@ -942,7 +974,7 @@ ssh case are one implementation.
"read me an arbitrary file" — the import listing is written that way. The
explorer is different because the path is the whole feature, and it
grants nothing new: the same token already spawns a bypass-permissions
agent in any directory on any machine a setup names. The import rule
agent in any directory on any configured machine. The import rule
stands where it is, because there a path was unnecessary.
- **Generation**: 256 bits from the OS CSPRNG, base64url. A machine
credential, never typed twice, so at this entropy no stretching is needed.
@@ -988,7 +1020,7 @@ ssh case are one implementation.
tunnel once DDNS catches up. The symptom is obvious and lossless — the SSE
cursor design replays whatever was missed.
- Rejected: **Tailscale** and **Headscale**, which add a coordination
service this setup does not need at two or three devices; **forwarding
service this machine does not need at two or three devices; **forwarding
the HTTPS port directly**, which puts every internet scanner one pre-auth
bug away from RCE on a machine holding SSH keys.
- The server refuses to start without TLS, so the token cannot travel
@@ -1002,7 +1034,7 @@ ssh case are one implementation.
Kotlin + Compose Multiplatform, single `:androidApp` module, same versions as
dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21).
1. **Session list** — kind icon, title, setup, model, status, last activity.
1. **Session list** — kind icon, title, machine, model, status, last activity.
Sessions awaiting an answer sort to the top: the "your turn" inbox.
2. **Import** — Claude Code sessions the machine already has, selected in
batches (hold to enter, tap to add), with Delete and Import along the
@@ -1013,7 +1045,7 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21).
not been imported, and tapping it starts a second CLI on the same
transcript. That makes rows below slide up under the reader's finger, so a
row that has just moved ignores taps for `SETTLE_MS`.
3. **Models** and **Setups** — browsing and downloading GGUFs; adding,
3. **Models** and **Machines** — browsing and downloading GGUFs; adding,
renaming, re-probing and removing machines.
4. **Session screen** — the core:
- The transcript rendered from the event stream: markdown, inline images,
@@ -1174,8 +1206,8 @@ verified by running it, matching dev-updater's posture.
- **Remote llama-server** needs its port forwarded (`ssh -L`) and is not
built; such a session is refused rather than misdirected.
- **Claude sessions over ssh need the remote machine logged in to Claude.**
Usage reporting reads each machine's own credentials, so this is visible
rather than silent.
Usage reporting reads each machine's own credentials, and the Machines tab
can run that machine's CLI login without requiring an interactive SSH shell.
## References