Providers and hosts: what runs, and where, as independent choices

A session now names a provider (what: driver kind, command, models) and
optionally a host (where: an ssh target). Keeping them independent is
what the real setup needs -- the backend runs where the phone can reach
it, which isn't where the CLI is installed -- and it means any provider
can be sent to any host rather than a machine being baked into one.

The first provider is claude-cli, named for the CLI rather than bare
"claude", which would suggest the credit-billed API. A fresh config is
seeded with it so a new install has something to spawn and a worked
example to edit; echo stays a built-in provider needing no config.

ssh.rs builds the child process either way: locally, or `ssh -T` with
BatchMode and keepalives, every argument single-quoted for the remote
shell (a working directory that tries to close the quote and start a
command is covered by a test), and `exec` so dropping the connection
takes the CLI down instead of orphaning it.

App: the spawn screen reads /providers and /hosts instead of hardcoded
lists, so config changes need no rebuild. Chip rows are FlowRow, fixing
the reported bug where a row of models that didn't fit wrapped *inside*
each chip -- one letter of "haiku" per line -- rather than onto a second
line.

Verified: 29 tests, clippy clean; the same claude-cli provider run once
locally and once over ssh, with the remote one visibly in a different
environment; an unknown host name refused with the configured list; and
the spawn screen on the emulator showing server-driven providers, hosts,
and models that wrap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
irisandClaude Fable 5 committed 2026-08-25 03:34:33 -04:00
1 parent 91bbc73ae5
commit fff1fb49e8
12 files changed
+833 -170

No files matched your search

+38 -4
View File
@@ -44,6 +44,26 @@ Decisions already made (2026-08-24):
## Architecture
### Providers and hosts (decided 2026-08-25)
Two independent axes, configured separately and chosen per session:
- A **provider** is *what* runs: a driver kind, the command to invoke, and
the models worth offering. `claude-cli` is the first — named for the CLI
specifically, since bare "claude" would suggest the credit-billed API,
which this is not. llama.cpp becomes a second provider later.
- A **host** is *where* it runs: an ssh target. Absent means the backend
machine itself.
Sessions name both. Keeping them independent is what the motivating setup
requires: the backend runs on the machine the phone can reach (where
WireGuard terminates), which is not necessarily where a CLI is installed —
here the Claude CLI lives only in a VM on that machine, while llama.cpp
will be on the host itself. Pinning a host into a provider would make "the
Claude CLI" and "the Claude CLI over there" two things to configure and
choose between, and would stop the same provider from being sent somewhere
else for one session.
```
Android app (Compose)
│ HTTPS (pinned CA) — REST for actions, SSE for live events
@@ -52,7 +72,8 @@ backend (Rust/Axum, desktop)
├─ SessionManager ── Session ── Driver (trait)
│ ├─ ClaudeDriver (claude stream-json)
│ └─ PiDriver (pi --mode rpc)
│ each driver's process is spawned locally or as `ssh host …`
│ each driver's process is spawned locally or as `ssh host …`,
│ decided per session by the host it names
├─ LlamaServerManager (llama-server lifecycle, local + SSH)
├─ UsageMonitor (Anthropic OAuth usage endpoint)
└─ config.json + per-session transcript files
@@ -214,8 +235,10 @@ the pinned TLS listener. SSE over WebSocket because resume-by-cursor
is plain POSTs anyway.
```
GET /sessions list (id, kind, title, host, model, status, last activity)
POST /sessions spawn {kind, host, model, cwd, permission_mode, title}
GET /providers what can be spawned (name, kind, models)
GET /hosts machines a session can be run on
GET /sessions list (id, provider, host, title, model, status, last activity)
POST /sessions spawn {provider, host, model, cwd, permission_mode, title}
GET /sessions/:id/events?after=N SSE: transcript replay from N, then live
POST /sessions/:id/message {text, attachment_ids}
POST /sessions/:id/answer {question_id, answer} (questions and permissions)
@@ -416,7 +439,18 @@ window just fills.
up in this VM, so this phase isn't testable here — Claude first; the
driver seam is ready when it is.*
5. **SSH** — host config, remote spawn for both kinds, remote llama-server
with port forward, attachment shipping.
with port forward, attachment shipping. *Host config and remote spawn
done 2026-08-25* (any session of any provider can name a host; the
command is the identical one wrapped in `ssh -T`, with every argument
shell-quoted). Attachment shipping turned out to be unnecessary for the
Claude driver — images ride the stdio JSONL as base64 in both
directions, so nothing needs `scp`. Still outstanding: remote
llama-server with its port forward, which comes with phase 4.
Two things learned doing it: a remote session inherits ssh's non-login
PATH, which is narrower than an interactive shell's (point `command` at
an absolute path if a CLI isn't found), and the remote command is run
with `exec` so dropping the connection takes the CLI down rather than
orphaning it.
6. **Polish** — reconnect edges, notification when a session awaits an answer
(the "your turn" push), transcript search, whatever daily use surfaces.