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
@@ -471,6 +471,22 @@ written, and the fold uses that same predicate to decide a reply is settled.
|
|||||||
`generate` now fails the turn for both -- a reply that stops early is not a
|
`generate` now fails the turn for both -- a reply that stops early is not a
|
||||||
reply, and the transcript keeps whatever arrived before it.
|
reply, and the transcript keeps whatever arrived before it.
|
||||||
|
|
||||||
|
- **A path is stored as it was typed, and `~` is expanded where it is used.**
|
||||||
|
`~/repos/x` and `/home/someone/repos/x` are a path and a snapshot of where it
|
||||||
|
pointed, and the snapshot is what breaks when an account is renamed or the
|
||||||
|
value is read on another machine -- so nothing at the boundary rewrites one
|
||||||
|
in either direction (`machines::tidy` used to expand and `shorten_home` used
|
||||||
|
to contract; both are gone). Expansion belongs to the machine the path is on:
|
||||||
|
`ssh::quote_path` and `files::PATH_PRELUDE` for a remote one,
|
||||||
|
`ssh::expand_home` for one here. The exception that proves it is
|
||||||
|
**`llama-server`'s tools**, which take the working directory as an
|
||||||
|
`x-tool-cwd` header and `chdir` to it with no shell in the way: a `~` arrives
|
||||||
|
there as a directory of that name and *every* tool using one answers "failed
|
||||||
|
to spawn process\n[exit code: -1]", which on the phone looks like a session
|
||||||
|
whose tools are all broken. `files::resolve_blocking` is what the llama
|
||||||
|
driver resolves it with at launch, on the machine that will serve the
|
||||||
|
session.
|
||||||
|
|
||||||
- **A transcript outlives the enum.** Removing `Event::TaskNote` hours after
|
- **A transcript outlives the enum.** Removing `Event::TaskNote` hours after
|
||||||
adding it made every transcript that had recorded one unreadable, so
|
adding it made every transcript that had recorded one unreadable, so
|
||||||
`launch` failed for those sessions and `SessionManager::new` skipped them —
|
`launch` failed for those sessions and `SessionManager::new` skipped them —
|
||||||
|
|||||||
+28
-1
@@ -777,7 +777,11 @@ impl Config {
|
|||||||
pub fn load(path: &Path) -> Result<Self> {
|
pub fn load(path: &Path) -> Result<Self> {
|
||||||
match std::fs::read_to_string(path) {
|
match std::fs::read_to_string(path) {
|
||||||
Ok(text) => format::parse(&text)
|
Ok(text) => format::parse(&text)
|
||||||
.with_context(|| format!("{} is not valid config RON", path.display())),
|
.with_context(|| format!("{} is not valid config RON", path.display()))
|
||||||
|
.map(|mut config| {
|
||||||
|
Self::adopt_mcp_defaults(&mut config);
|
||||||
|
config
|
||||||
|
}),
|
||||||
// A first run has no config -- the normal starting state; a token is
|
// A first run has no config -- the normal starting state; a token is
|
||||||
// generated and saved on that first start.
|
// generated and saved on that first start.
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
@@ -788,6 +792,29 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gives a provider written before `mcp_servers` existed the defaults a
|
||||||
|
/// probe would give it now.
|
||||||
|
///
|
||||||
|
/// A migration, and a temporary one: the field arrived on 2026-09-19 and a
|
||||||
|
/// machine discovered before then has an empty list, which reads on screen
|
||||||
|
/// exactly like a machine somebody chose to give no MCP servers -- so a
|
||||||
|
/// llama session silently had no web search and nothing said why. Pressing
|
||||||
|
/// Rediscover fixes it, which is not something a person can be expected to
|
||||||
|
/// know. Delete this once every config here has been through it.
|
||||||
|
///
|
||||||
|
/// Empty rather than absent is the condition, because there is no way to
|
||||||
|
/// remove one from the phone: nothing can have chosen the empty list yet.
|
||||||
|
fn adopt_mcp_defaults(&mut self) {
|
||||||
|
for provider in self
|
||||||
|
.machines
|
||||||
|
.iter_mut()
|
||||||
|
.flat_map(|machine| machine.providers.iter_mut())
|
||||||
|
.filter(|provider| provider.mcp_servers.is_empty())
|
||||||
|
{
|
||||||
|
provider.mcp_servers = crate::machines::mcp_defaults(provider.kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Writes the config, owner-readable only.
|
/// Writes the config, owner-readable only.
|
||||||
///
|
///
|
||||||
/// The token hashes here are verifiers rather than secrets, but the file
|
/// The token hashes here are verifiers rather than secrets, but the file
|
||||||
|
|||||||
@@ -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
|
/// 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
|
/// 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.
|
/// 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"]);
|
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]
|
#[tokio::test]
|
||||||
async fn a_missing_directory_fails_with_the_machine_s_own_message() {
|
async fn a_missing_directory_fails_with_the_machine_s_own_message() {
|
||||||
let dir = tree();
|
let dir = tree();
|
||||||
|
|||||||
+21
-52
@@ -127,7 +127,7 @@ impl OfferedModel {
|
|||||||
/// Only llama.cpp, because only a driver that runs its own agent loop can use
|
/// Only llama.cpp, because only a driver that runs its own agent loop can use
|
||||||
/// one -- the coding CLIs configure MCP themselves and a second answer here
|
/// one -- the coding CLIs configure MCP themselves and a second answer here
|
||||||
/// would quietly disagree with theirs.
|
/// would quietly disagree with theirs.
|
||||||
fn mcp_defaults(kind: DriverKind) -> Vec<crate::config::McpServerConfig> {
|
pub fn mcp_defaults(kind: DriverKind) -> Vec<crate::config::McpServerConfig> {
|
||||||
match kind {
|
match kind {
|
||||||
DriverKind::LlamaCpp => vec![crate::config::McpServerConfig {
|
DriverKind::LlamaCpp => vec![crate::config::McpServerConfig {
|
||||||
name: "exa".to_string(),
|
name: "exa".to_string(),
|
||||||
@@ -270,41 +270,19 @@ pub fn id_from(label: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normalises what a phone keyboard produced: trims, drops blanks, and
|
/// Normalises what a phone keyboard produced: trims, and reads a field left
|
||||||
/// expands a leading `~` the way a shell would.
|
/// blank as absent rather than as an empty answer.
|
||||||
|
///
|
||||||
|
/// It deliberately does **not** touch a leading `~`. A path is stored as it was
|
||||||
|
/// typed, because `~` and `/home/someone` are not two spellings of one path --
|
||||||
|
/// the second is a snapshot of where the first pointed, and it is the snapshot
|
||||||
|
/// that breaks when an account is renamed or the value is read on another
|
||||||
|
/// machine. Expansion belongs where the path is used, against the machine it
|
||||||
|
/// belongs to: `ssh::quote_path` and `files::PATH_PRELUDE` for a remote one,
|
||||||
|
/// `ssh::expand_home` for one on this machine.
|
||||||
pub fn tidy(value: &str) -> Option<String> {
|
pub fn tidy(value: &str) -> Option<String> {
|
||||||
let value = value.trim();
|
let value = value.trim();
|
||||||
if value.is_empty() {
|
(!value.is_empty()).then(|| value.to_string())
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(match value.strip_prefix("~/") {
|
|
||||||
Some(rest) => match std::env::home_dir() {
|
|
||||||
Some(home) => home.join(rest).to_string_lossy().into_owned(),
|
|
||||||
None => value.to_string(),
|
|
||||||
},
|
|
||||||
None => value.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The inverse of [`tidy`]'s expansion: an absolute path under this machine's
|
|
||||||
/// home, written back as `~/…`, so that a working directory reads on a phone the
|
|
||||||
/// way it is written by hand.
|
|
||||||
///
|
|
||||||
/// Applied only to paths on **this** machine. `$HOME` here says nothing about
|
|
||||||
/// the home directory of a machine reached over ssh, so a remote path is stored
|
|
||||||
/// exactly as it was typed and the remote shell is what expands it.
|
|
||||||
pub fn shorten_home(path: &str) -> String {
|
|
||||||
let Some(home) = std::env::home_dir() else {
|
|
||||||
return path.to_string();
|
|
||||||
};
|
|
||||||
let home = home.to_string_lossy();
|
|
||||||
// The separator has to be part of the match, or `/home/bobby` would be read
|
|
||||||
// as a path inside `/home/bob`.
|
|
||||||
match path.strip_prefix(home.as_ref()) {
|
|
||||||
Some("") => "~".to_string(),
|
|
||||||
Some(rest) if rest.starts_with('/') => format!("~{rest}"),
|
|
||||||
_ => path.to_string(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs a launch to completion and returns its stdout as text.
|
/// Runs a launch to completion and returns its stdout as text.
|
||||||
@@ -326,25 +304,16 @@ impl Transport {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// The two halves of a home-relative path, which have to be inverses: what
|
/// A path survives the boundary unchanged, tilde included -- the one thing
|
||||||
/// is stored is what the phone draws, and what the phone sends back is what
|
/// `tidy` must not do is decide where `~` is.
|
||||||
/// a process is started in.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_home_path_shortens_and_expands_back() {
|
fn a_typed_path_is_stored_as_typed() {
|
||||||
let Some(home) = std::env::home_dir() else {
|
assert_eq!(
|
||||||
return;
|
tidy(" ~/repos/ai-app-2 ").as_deref(),
|
||||||
};
|
Some("~/repos/ai-app-2")
|
||||||
let full = home.join("repos/ai-app-2");
|
);
|
||||||
let full = full.to_string_lossy();
|
assert_eq!(tidy("/etc/hosts").as_deref(), Some("/etc/hosts"));
|
||||||
assert_eq!(shorten_home(&full), "~/repos/ai-app-2");
|
assert_eq!(tidy(" "), None);
|
||||||
assert_eq!(shorten_home(&home.to_string_lossy()), "~");
|
|
||||||
assert_eq!(tidy("~/repos/ai-app-2").as_deref(), Some(full.as_ref()));
|
|
||||||
|
|
||||||
// Not a prefix match on the characters: a sibling directory whose name
|
|
||||||
// merely starts with the home directory's is not inside it.
|
|
||||||
let sibling = format!("{}-backup/notes", home.to_string_lossy());
|
|
||||||
assert_eq!(shorten_home(&sibling), sibling);
|
|
||||||
assert_eq!(shorten_home("/etc/hosts"), "/etc/hosts");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+50
-40
@@ -418,40 +418,27 @@ struct SshRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SshRequest {
|
impl SshRequest {
|
||||||
/// Tidied at the boundary rather than stored as typed -- this came from a
|
/// Tidied at the boundary -- this came from a phone keyboard, so a field
|
||||||
/// phone keyboard, so it may have a stray space or a `~`.
|
/// may carry a stray space or have been left blank. Nothing here rewrites
|
||||||
|
/// a path: a `~` is stored as typed and expanded where it is used, by the
|
||||||
|
/// machine it belongs to. See [`crate::machines::tidy`].
|
||||||
fn into_config(self) -> Result<crate::config::SshConfig, ApiError> {
|
fn into_config(self) -> Result<crate::config::SshConfig, ApiError> {
|
||||||
let address = crate::machines::tidy(&self.address)
|
let tidy = crate::machines::tidy;
|
||||||
|
let address = tidy(&self.address)
|
||||||
.ok_or_else(|| ApiError::BadRequest("a machine needs an address".to_string()))?;
|
.ok_or_else(|| ApiError::BadRequest("a machine needs an address".to_string()))?;
|
||||||
|
let path = |value: Option<String>| {
|
||||||
|
value
|
||||||
|
.as_deref()
|
||||||
|
.and_then(tidy)
|
||||||
|
.map(std::path::PathBuf::from)
|
||||||
|
};
|
||||||
Ok(crate::config::SshConfig {
|
Ok(crate::config::SshConfig {
|
||||||
address,
|
address,
|
||||||
port: self.port,
|
port: self.port,
|
||||||
identity_file: self
|
identity_file: path(self.identity_file),
|
||||||
.identity_file
|
options: self.options.iter().filter_map(|o| tidy(o)).collect(),
|
||||||
.as_deref()
|
attachments_dir: path(self.attachments_dir),
|
||||||
.and_then(crate::machines::tidy)
|
models_dir: path(self.models_dir),
|
||||||
.map(std::path::PathBuf::from),
|
|
||||||
options: self
|
|
||||||
.options
|
|
||||||
.iter()
|
|
||||||
.filter_map(|o| crate::machines::tidy(o))
|
|
||||||
.collect(),
|
|
||||||
// Not `tidy`: that expands `~` to *this* machine's home, and this
|
|
||||||
// path is on the other one. The remote shell expands it there.
|
|
||||||
attachments_dir: self
|
|
||||||
.attachments_dir
|
|
||||||
.as_deref()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|dir| !dir.is_empty())
|
|
||||||
.map(std::path::PathBuf::from),
|
|
||||||
// The same rule, and for the same reason: this directory is
|
|
||||||
// on the other machine, so a `~` in it is that machine's home.
|
|
||||||
models_dir: self
|
|
||||||
.models_dir
|
|
||||||
.as_deref()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|dir| !dir.is_empty())
|
|
||||||
.map(std::path::PathBuf::from),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1373,6 +1360,33 @@ async fn spawn(manager: &Arc<SessionManager>, body: SpawnRequest) -> Result<Sess
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The same question `set_cwd` asks, and for the same reason: a typed
|
||||||
|
// directory that is not there becomes a session whose process cannot start,
|
||||||
|
// reported long after the typo and pointing at nothing. Stored as typed --
|
||||||
|
// a `~` is the serving machine's home and stays one. Blank normalised to
|
||||||
|
// absent for the reason the title below is: a form whose directory field
|
||||||
|
// was left alone sends `""`, which is `Some`.
|
||||||
|
let typed_cwd = match body
|
||||||
|
.cwd
|
||||||
|
.as_ref()
|
||||||
|
.map(|cwd| cwd.to_string_lossy())
|
||||||
|
.filter(|cwd| !cwd.trim().is_empty())
|
||||||
|
{
|
||||||
|
Some(cwd) => {
|
||||||
|
let cwd = crate::files::check_path(&cwd).map_err(bad_request)?;
|
||||||
|
let machine = machine_by_id(manager, &body.machine)?;
|
||||||
|
let transport = crate::session::transport::Transport::for_machine(&machine);
|
||||||
|
if !crate::session::import::directory_exists(&transport, &cwd).await {
|
||||||
|
return Err(ApiError::BadRequest(format!(
|
||||||
|
"{} has no directory {cwd}",
|
||||||
|
machine.name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Some(PathBuf::from(cwd))
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let spec = SpawnSpec {
|
let spec = SpawnSpec {
|
||||||
machine: body.machine,
|
machine: body.machine,
|
||||||
provider: body.provider,
|
provider: body.provider,
|
||||||
@@ -1391,7 +1405,7 @@ async fn spawn(manager: &Arc<SessionManager>, body: SpawnRequest) -> Result<Sess
|
|||||||
}),
|
}),
|
||||||
model: body.model,
|
model: body.model,
|
||||||
// Resumed where it was working, so the CLI picks up the same tree.
|
// Resumed where it was working, so the CLI picks up the same tree.
|
||||||
cwd: body.cwd.or_else(|| {
|
cwd: typed_cwd.or_else(|| {
|
||||||
seed.as_ref()
|
seed.as_ref()
|
||||||
.map(|(chosen, _)| PathBuf::from(&chosen.cwd))
|
.map(|(chosen, _)| PathBuf::from(&chosen.cwd))
|
||||||
.filter(|cwd| cwd.as_os_str() != "")
|
.filter(|cwd| cwd.as_os_str() != "")
|
||||||
@@ -1826,17 +1840,13 @@ async fn set_cwd(
|
|||||||
machine.name
|
machine.name
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
// Stored in the short form, so the one path kept is the one the phone will
|
// Stored exactly as it was typed, `~` included. Rewriting it either way is
|
||||||
// draw -- rather than storing `/home/bob/…` and abbreviating it again at
|
// wrong for the same reason: `~` and `/home/someone` are not the same path,
|
||||||
// each place it is shown, which is two representations of one directory.
|
// they are one path and one snapshot of where it was, and the snapshot is
|
||||||
// Only where the machine runs here; see `machines::shorten_home`.
|
// what breaks when an account is renamed. Every place this is used expands
|
||||||
let stored = if machine.ssh.is_none() {
|
// it against the machine it belongs to.
|
||||||
crate::machines::shorten_home(&cwd)
|
|
||||||
} else {
|
|
||||||
cwd.clone()
|
|
||||||
};
|
|
||||||
manager
|
manager
|
||||||
.set_session_cwd(&id, PathBuf::from(&stored))
|
.set_session_cwd(&id, PathBuf::from(&cwd))
|
||||||
.map_err(bad_request)?;
|
.map_err(bad_request)?;
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -378,9 +378,11 @@ struct Shared {
|
|||||||
/// are changeable while the session runs: they ride on the next request,
|
/// are changeable while the session runs: they ride on the next request,
|
||||||
/// so unlike the server's own flags there is nothing to reload.
|
/// so unlike the server's own flags there is nothing to reload.
|
||||||
sampling: Mutex<serde_json::Map<String, Value>>,
|
sampling: Mutex<serde_json::Map<String, Value>>,
|
||||||
/// The session's working directory, which is where its tools act. `None`
|
/// The session's working directory, which is where its tools act, resolved
|
||||||
/// leaves that to `llama-server`, which is the honest answer rather than a
|
/// on the machine serving this session. Absolute, because it is handed to
|
||||||
/// guess at one.
|
/// `llama-server` as a header it `chdir`s to with no shell to expand a
|
||||||
|
/// `~`. `None` leaves the directory to `llama-server`, which is the honest
|
||||||
|
/// answer rather than a guess at one.
|
||||||
cwd: Option<String>,
|
cwd: Option<String>,
|
||||||
/// Set by [`Driver::interrupt`]; the streaming loop and the tool loop both
|
/// Set by [`Driver::interrupt`]; the streaming loop and the tool loop both
|
||||||
/// check it, leaving what was produced in the transcript.
|
/// check it, leaving what was produced in the transcript.
|
||||||
@@ -474,16 +476,30 @@ impl LlamaDriver {
|
|||||||
|
|
||||||
let sampling = sampling_from(&meta.params);
|
let sampling = sampling_from(&meta.params);
|
||||||
|
|
||||||
|
// Resolved on the machine that will serve this session, because it is
|
||||||
|
// the one path here that reaches something with no shell in front of
|
||||||
|
// it: `llama-server` `chdir`s to the header it is given. See
|
||||||
|
// `files::resolve_blocking`. A failure is the launch's, for the same
|
||||||
|
// reason a CLI session whose directory has gone fails at its own `cd`
|
||||||
|
// -- tools running somewhere other than where they were told to is
|
||||||
|
// worse than not starting.
|
||||||
|
let cwd = meta
|
||||||
|
.cwd
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| {
|
||||||
|
crate::files::resolve_blocking(transport, &path.to_string_lossy()).with_context(
|
||||||
|
|| format!("this session's working directory, {}", path.display()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
let driver = Self {
|
let driver = Self {
|
||||||
shared: Arc::new(Shared {
|
shared: Arc::new(Shared {
|
||||||
sink,
|
sink,
|
||||||
transcript: transcript.to_path_buf(),
|
transcript: transcript.to_path_buf(),
|
||||||
session_dir: session_dir.to_path_buf(),
|
session_dir: session_dir.to_path_buf(),
|
||||||
sampling: Mutex::new(sampling),
|
sampling: Mutex::new(sampling),
|
||||||
cwd: meta
|
cwd,
|
||||||
.cwd
|
|
||||||
.as_ref()
|
|
||||||
.map(|path| path.to_string_lossy().into_owned()),
|
|
||||||
cancel: AtomicBool::new(false),
|
cancel: AtomicBool::new(false),
|
||||||
turns: Mutex::new(Turns::default()),
|
turns: Mutex::new(Turns::default()),
|
||||||
serving: Mutex::new(Serving::Loading),
|
serving: Mutex::new(Serving::Loading),
|
||||||
|
|||||||
@@ -172,6 +172,12 @@ impl Tools {
|
|||||||
/// say they use one. A session with no working directory sends none, and
|
/// say they use one. A session with no working directory sends none, and
|
||||||
/// `llama-server` falls back to its own -- which is the honest outcome:
|
/// `llama-server` falls back to its own -- which is the honest outcome:
|
||||||
/// this server has no better answer for where "here" is.
|
/// this server has no better answer for where "here" is.
|
||||||
|
///
|
||||||
|
/// It must already be **absolute**: the header is passed to `chdir` with
|
||||||
|
/// no shell in the way, so a leading `~` is a directory of that name and
|
||||||
|
/// every tool using one fails with "failed to spawn process". The driver
|
||||||
|
/// resolves it on the serving machine at launch; see
|
||||||
|
/// `files::resolve_blocking`.
|
||||||
pub fn execute(
|
pub fn execute(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
|||||||
+5
-1
@@ -107,7 +107,11 @@ pub fn command(
|
|||||||
command.args(["-p", &port.to_string()]);
|
command.args(["-p", &port.to_string()]);
|
||||||
}
|
}
|
||||||
if let Some(identity) = &ssh.identity_file {
|
if let Some(identity) = &ssh.identity_file {
|
||||||
command.arg("-i").arg(identity);
|
// Expanded here rather than when it was typed: this one *is* a path on
|
||||||
|
// this machine, but storing what `~` resolved to on the day it was
|
||||||
|
// entered is what breaks when the account is renamed. There is no shell
|
||||||
|
// between here and `ssh`, so nothing else would expand it.
|
||||||
|
command.arg("-i").arg(expand_home(identity));
|
||||||
// Without this, ssh may offer an agent key first and authenticate as
|
// Without this, ssh may offer an agent key first and authenticate as
|
||||||
// somebody else entirely -- silently, and with different permissions.
|
// somebody else entirely -- silently, and with different permissions.
|
||||||
command.args(["-o", "IdentitiesOnly=yes"]);
|
command.args(["-o", "IdentitiesOnly=yes"]);
|
||||||
|
|||||||
Reference in new issue
Block a user