A screen for the machines, and failures a phone can act on

The other half of making setups editable: add, rename, rediscover and
remove, with a Test that tries a machine before anything is saved.

The screen cannot name a program, which is the point rather than an
omission -- providers are what the server found when it asked, so this app
has no way to introduce something to run. The dialog says so, because
"what it can run is discovered, not typed" is the answer to the question a
person will otherwise ask when they look for a command field.

Two things running it changed. The card showed "this machine / this
machine", because the seeded setup is *called* that and my fallback line
for a local setup said the same -- the line now says something the name
cannot also be. And the header row absorbed a fifth action without
complaint, which is the earlier title-and-actions split paying off exactly
as its comment predicted.

**Host key verification is the failure that would have made this look
broken.** Every machine fails it the first time, because its key is not in
known_hosts yet, and ssh's own words -- "Host key verification failed." --
are written for somebody at a terminal on the backend, which is exactly who
is not reading a phone. It now says what to do: ssh to it once from the
backend and try again. Permission denied gets the same treatment.

Deliberately *not* fixed by relaxing StrictHostKeyChecking. Accepting a new
key is a decision somebody should make with the key in front of them, not
something this app does quietly on their behalf while adding a machine.

Verified on the emulator against a running server: the seeded setup renders
with what was discovered on it, the add dialog explains itself, and Test
against an untrusted machine produces the full explanation rather than
ssh's four words.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-28 13:17:08 -04:00
1 parent 19e3531c5d
commit 3c144f8070
5 files changed
+506 -19

No files matched your search

+31 -1
View File
@@ -52,7 +52,7 @@ pub async fn discover(transport: &Transport) -> Result<Vec<ProviderConfig>> {
wanted.join(" ")
);
let launch = Launch::new("sh", vec!["-c".to_string(), script], None);
let found = transport.capture(&launch).await?;
let found = transport.capture(&launch).await.map_err(explain)?;
let mut providers = Vec::new();
// Echo runs inside this server, so it exists exactly where this server
@@ -90,6 +90,36 @@ pub async fn discover(transport: &Transport) -> Result<Vec<ProviderConfig>> {
Ok(providers)
}
/// Adds what to do to failures whose own wording does not say.
///
/// ssh's messages are written for someone at a terminal on the backend,
/// which is exactly who is not reading this one. Host key verification is
/// the case that matters: **every** machine fails it the first time,
/// because its key is not in `known_hosts` yet -- so without this, adding
/// a machine from the phone looks broken rather than unfinished.
///
/// Deliberately not fixed by relaxing the check. `StrictHostKeyChecking`
/// stays at its default, so a first connection is a decision somebody
/// makes on the backend with the key in front of them, rather than
/// something this app quietly accepts on their behalf.
fn explain(err: anyhow::Error) -> anyhow::Error {
let message = format!("{err:#}");
if message.contains("Host key verification failed") {
return anyhow::anyhow!(
"{message} This machine has not been connected to before, so its key is not \
trusted yet. Ssh to it once from the backend -- that is where the decision to \
trust a key belongs -- and try again.",
);
}
if message.contains("Permission denied") {
return anyhow::anyhow!(
"{message} The key named here has to be authorized on that machine, and the path \
is read on the backend rather than on the phone.",
);
}
err
}
/// A short, stable, filename-safe id derived from a label.
///
/// Derived once when a setup is added and then fixed, so the label stays