Rename setups and add provider reauthentication
This commit is contained in:
1 parent
e9a0f1b9da
commit
7d9df5d572
36 files changed
+1866
-684
No files matched your search
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user