Keep a typed path as typed, and expand ~ where it is used
A llama session's tools all answered "failed to spawn process [exit code: -1]": `llama-server` takes the working directory as an `x-tool-cwd` header and `chdir`s to it with no shell in the way, so a `~/…` cwd named a directory of that name. `files::resolve_blocking` asks the machine that will serve the session what the path is, and the driver does that once at launch. The other half is the storing. `~` and `/home/someone` are a path and a snapshot of where it pointed, and it is the snapshot that breaks when an account is renamed -- so `machines::tidy` no longer expands one and `set_cwd` no longer contracts one (`shorten_home` is gone with it). The identity file is expanded at the point `ssh` is invoked instead. Spawn now asks the same question of a typed working directory that `set_cwd` already did: absolute or home-relative, and actually there on the machine that will run it. It accepted anything, so a typo became a session whose process could not start, reported later and pointing at nothing. Also: a provider written before `mcp_servers` existed adopts the defaults a probe would give it now, so a machine discovered before 2026-09-19 stops silently having no web search. Exercised end to end against a real llama session spawned with `cwd: "~/repos/ai-app/server"`: `exec_shell_command` with `pwd` answered `/home/bob/repos/ai-app/server`, exit 0.
This commit is contained in:
1 parent
bd9596d782
commit
3b309766d7
8 files changed
+201
-101
No files matched your search
@@ -176,6 +176,34 @@ pub async fn list(transport: &Transport, path: &str) -> Result<Listing> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The absolute path `path` names on that machine, with a leading `~` expanded
|
||||
/// *there*.
|
||||
///
|
||||
/// What [`list`] answers as its `path`, asked on its own: a caller that has to
|
||||
/// hand a directory to something with no shell in front of it needs the
|
||||
/// resolved form and nothing else. `llama-server` is the one such caller --
|
||||
/// its tools take a working directory as a request header and `chdir` to it
|
||||
/// literally, so the `~` that every other path in this server carries through
|
||||
/// to the far side's shell arrived there as a directory called `~`, and every
|
||||
/// tool that uses one failed with "failed to spawn process".
|
||||
///
|
||||
/// Blocking because a driver's launch is, and this is a question for the
|
||||
/// machine that will serve the session rather than for this one: a remote
|
||||
/// `~` is the remote home, and expanding it here would name a directory on
|
||||
/// the wrong machine -- which is also why the answer is not cached anywhere
|
||||
/// but on the driver that asked.
|
||||
pub fn resolve_blocking(transport: &Transport, path: &str) -> Result<String> {
|
||||
let script = format!("{PATH_PRELUDE}cd -- \"$p\" && pwd -P");
|
||||
let out = transport.capture_blocking(&launch(script, path, None))?;
|
||||
// Only the newline `pwd` ends with: a directory name may legitimately end
|
||||
// in a space, and trimming whitespace would rename it.
|
||||
let resolved = out.trim_end_matches('\n');
|
||||
if resolved.is_empty() {
|
||||
anyhow::bail!("the machine did not say what {path} resolves to");
|
||||
}
|
||||
Ok(resolved.to_string())
|
||||
}
|
||||
|
||||
/// The `find` output above, as rows. A record without all five fields is
|
||||
/// dropped rather than guessed at: it can only come from a `find` that printed
|
||||
/// something else, and half a row is worse than no row.
|
||||
@@ -436,6 +464,30 @@ mod tests {
|
||||
assert_eq!(names, ["binary.bin", "hello.txt", "it's a file", "sub"]);
|
||||
}
|
||||
|
||||
/// The regression `resolve_blocking` exists for: a working directory typed
|
||||
/// as `~/…` reaches `llama-server` as a header it `chdir`s to, so it has to
|
||||
/// arrive absolute or every tool that uses one fails to spawn.
|
||||
#[test]
|
||||
fn a_tilde_resolves_to_that_machine_s_home() {
|
||||
let Some(home) = std::env::home_dir() else {
|
||||
return;
|
||||
};
|
||||
let resolved = resolve_blocking(&Transport::Here, "~").unwrap();
|
||||
assert!(resolved.starts_with('/'), "{resolved}");
|
||||
assert_eq!(
|
||||
std::fs::canonicalize(&resolved).unwrap(),
|
||||
std::fs::canonicalize(&home).unwrap(),
|
||||
);
|
||||
let dir = tree();
|
||||
// An absolute path is answered as itself, resolved.
|
||||
let full = dir.path().to_string_lossy().into_owned();
|
||||
assert_eq!(
|
||||
resolve_blocking(&Transport::Here, &full).unwrap(),
|
||||
std::fs::canonicalize(&full).unwrap().to_string_lossy(),
|
||||
);
|
||||
assert!(resolve_blocking(&Transport::Here, &at(&dir, "nope")).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_missing_directory_fails_with_the_machine_s_own_message() {
|
||||
let dir = tree();
|
||||
|
||||
Reference in new issue
Block a user