Prune commentary and stale Rust port notes
This commit is contained in:
1 parent
5428cd75c9
commit
25370731d0
193 files changed
+693
-16219
No files matched your search
@@ -1,48 +1,24 @@
|
||||
//! Building the command a driver actually spawns -- locally, or wrapped in
|
||||
//! `ssh` when the session names a host to run on.
|
||||
//!
|
||||
//! A driver speaks JSONL over a child process's stdio and doesn't care what
|
||||
//! that child is, so a remote session is the identical command with `ssh host …`
|
||||
//! in front.
|
||||
//!
|
||||
//! Uses the system `ssh` client rather than a Rust SSH library, so
|
||||
//! `~/.ssh/config`, agents and jump hosts all keep working and there is only
|
||||
//! one place to configure connections.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::config::SshConfig;
|
||||
|
||||
/// A port on the machine a command runs on, and the port that reaches it from
|
||||
/// the backend.
|
||||
///
|
||||
/// The second half of what a transport is (PLAN.md's SSH section): "run this"
|
||||
/// plus "reach this port". Locally the two numbers are one and nothing is
|
||||
/// forwarded; over ssh the connection carries an `-L` tunnel, so a model server
|
||||
/// binds loopback on the far machine and is never exposed to its network.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Forward {
|
||||
/// What the launched program should listen on, on its own machine.
|
||||
pub there: u16,
|
||||
/// What this machine connects to. The same number as `there` when the
|
||||
/// program runs here.
|
||||
pub here: u16,
|
||||
}
|
||||
|
||||
/// Options forced onto every connection. `BatchMode` makes a missing key fail
|
||||
/// immediately with a readable message instead of hanging on a password prompt
|
||||
/// nothing can answer; the keepalives turn a silently dropped link into a
|
||||
/// process exit, which the session reports as `exited` rather than hanging.
|
||||
const SSH_OPTIONS: [&str; 3] = [
|
||||
"BatchMode=yes",
|
||||
"ServerAliveInterval=30",
|
||||
"ServerAliveCountMax=3",
|
||||
];
|
||||
|
||||
/// Builds the child process for `program args…`, run in `cwd`, either on this
|
||||
/// machine (`ssh` absent) or on the machine it describes.
|
||||
///
|
||||
/// Stdio is left alone: how the streams are connected is the caller's decision
|
||||
/// and differs by more than the transport does -- a probe wants pipes it will
|
||||
/// drain, a session wants files that outlive this server.
|
||||
@@ -61,11 +37,6 @@ pub fn command(
|
||||
let mut command = Command::new(program);
|
||||
command.args(args);
|
||||
if let Some(cwd) = cwd {
|
||||
// Expanded here for the same reason `quote_path` expands it on the
|
||||
// far side: a working directory typed as `~/repos/ai-app` has to
|
||||
// mean the same thing whichever machine runs it. There is no shell
|
||||
// in this branch, so nothing else would -- `current_dir` would be
|
||||
// handed the literal one-character directory `~`.
|
||||
command.current_dir(expand_home(cwd));
|
||||
}
|
||||
return command;
|
||||
@@ -73,17 +44,7 @@ pub fn command(
|
||||
|
||||
let mut command = Command::new("ssh");
|
||||
if let Some(forward) = forward {
|
||||
// A forwarded process is not spoken to over stdio, and that changes how
|
||||
// it is shut down. Everything else here is a CLI reading its stdin, so
|
||||
// killing the ssh client ends it; a `llama-server` never reads its own,
|
||||
// so the same kill left it running on the far machine with the model
|
||||
// loaded -- measured 2026-09-04, an orphan per stopped session. A pty
|
||||
// is what makes sshd hang the far side up. `-tt` because this client
|
||||
// has no terminal to inherit one from. The cost is a log that arrives
|
||||
// through a line discipline, which nothing parses.
|
||||
command.arg("-tt");
|
||||
// Loopback at both ends: the far side binds 127.0.0.1, so what it
|
||||
// serves is reachable only through this connection.
|
||||
command.args([
|
||||
"-L",
|
||||
&format!("127.0.0.1:{}:127.0.0.1:{}", forward.here, forward.there),
|
||||
@@ -93,8 +54,6 @@ pub fn command(
|
||||
// arrive as "the model never became ready".
|
||||
command.args(["-o", "ExitOnForwardFailure=yes"]);
|
||||
} else {
|
||||
// -T: no pty. This carries JSONL, and a pty would rewrite it (echo,
|
||||
// CRLF translation, ^C handling) into something the parser can't read.
|
||||
command.arg("-T");
|
||||
}
|
||||
for option in SSH_OPTIONS {
|
||||
@@ -108,8 +67,6 @@ pub fn command(
|
||||
}
|
||||
if let Some(identity) = &ssh.identity_file {
|
||||
command.arg("-i").arg(identity);
|
||||
// Without this, ssh may offer an agent key first and authenticate as
|
||||
// somebody else entirely -- silently, and with different permissions.
|
||||
command.args(["-o", "IdentitiesOnly=yes"]);
|
||||
}
|
||||
command.arg(&ssh.address);
|
||||
@@ -117,10 +74,6 @@ pub fn command(
|
||||
command
|
||||
}
|
||||
|
||||
/// The single argument handed to the remote login shell. `exec` so the CLI
|
||||
/// replaces that shell: the process the connection is attached to is then the
|
||||
/// CLI itself, and dropping the connection takes it down rather than leaving an
|
||||
/// orphan behind a live wrapper.
|
||||
fn remote_script(program: &str, args: &[String], cwd: Option<&Path>) -> String {
|
||||
let mut script = String::new();
|
||||
if let Some(cwd) = cwd {
|
||||
@@ -137,12 +90,6 @@ fn remote_script(program: &str, args: &[String], cwd: Option<&Path>) -> String {
|
||||
script
|
||||
}
|
||||
|
||||
/// A path with a leading `~` replaced by this machine's home directory.
|
||||
///
|
||||
/// The local half of the rule [`quote_path`] states for the remote one, and
|
||||
/// deliberately the same shape: the tilde is expanded, `~user` is not, and
|
||||
/// nothing else in the path gains a meaning. A machine with no home directory
|
||||
/// leaves the path alone, which fails with the operating system's own message.
|
||||
pub(crate) fn expand_home(path: &Path) -> PathBuf {
|
||||
let Some(rest) = path.to_str().and_then(|p| {
|
||||
if p == "~" {
|
||||
@@ -159,22 +106,11 @@ pub(crate) fn expand_home(path: &Path) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
/// Quotes a path, expanding a leading `~` and nothing else.
|
||||
///
|
||||
/// [`quote`] is right for every other word crossing to the remote side and
|
||||
/// wrong for exactly one character. `~` means "expand me", and single quotes
|
||||
/// are what stop expansion -- so a working directory typed as `~/repos/ai-app`
|
||||
/// arrived as the literal four-character directory `~`, and the remote shell
|
||||
/// said it did not exist, which reads like the path being wrong.
|
||||
///
|
||||
/// `"$HOME"` rather than handing the tilde to the shell unquoted: the variable
|
||||
/// is expanded, the expansion is not re-split or globbed because it is
|
||||
/// double-quoted, and everything after it stays single-quoted and literal.
|
||||
/// `$HOME` is set by every shell this can land in, including the fish login
|
||||
/// shell on the dev VM, so this does not depend on the remote shell being POSIX.
|
||||
///
|
||||
/// `~user` is deliberately not handled: there is no portable expansion for it,
|
||||
/// and inventing one would mean guessing another account's home directory.
|
||||
pub(crate) fn quote_path(path: &str) -> String {
|
||||
if path == "~" {
|
||||
return "\"$HOME\"".to_string();
|
||||
@@ -185,13 +121,7 @@ pub(crate) fn quote_path(path: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Single-quotes one word for a POSIX shell. Everything crossing to the remote
|
||||
/// side goes through here: paths, model names and prompts-as-arguments are all
|
||||
/// attacker-adjacent input in a server whose whole job is running commands, and
|
||||
/// unquoted they would be shell syntax rather than data.
|
||||
pub(crate) fn quote(word: &str) -> String {
|
||||
// Inside single quotes every character is literal except `'` itself, which
|
||||
// is closed, escaped, and reopened.
|
||||
format!("'{}'", word.replace('\'', r"'\''"))
|
||||
}
|
||||
|
||||
@@ -203,7 +133,6 @@ mod tests {
|
||||
args.iter().map(|arg| arg.to_string()).collect()
|
||||
}
|
||||
|
||||
/// The rendered argv, for asserting on what would actually run.
|
||||
fn argv(command: &Command) -> Vec<String> {
|
||||
std::iter::once(command.get_program())
|
||||
.chain(command.get_args())
|
||||
@@ -211,9 +140,6 @@ mod tests {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A host with nothing configured but a name to dial, so `~/.ssh/config`
|
||||
/// decides everything else -- the case that proves this adds no flags of its
|
||||
/// own when it was not told to.
|
||||
fn bare_host() -> SshConfig {
|
||||
SshConfig {
|
||||
address: "vm".to_string(),
|
||||
@@ -263,7 +189,6 @@ mod tests {
|
||||
assert!(rendered.contains(&"IdentitiesOnly=yes".to_string()));
|
||||
assert!(rendered.contains(&"2222".to_string()));
|
||||
assert!(rendered.contains(&"/home/me/.ssh/id_ai".to_string()));
|
||||
// The host, then exactly one argument: the remote script.
|
||||
assert_eq!(rendered[rendered.len() - 2], "bob@10.0.2.15");
|
||||
assert_eq!(
|
||||
rendered[rendered.len() - 1],
|
||||
@@ -276,17 +201,9 @@ mod tests {
|
||||
let ssh = bare_host();
|
||||
let rendered = argv(&command(Some(&ssh), "claude", &args(["-p"]), None, None));
|
||||
assert_eq!(rendered.last().unwrap(), "exec 'claude' '-p'");
|
||||
// No -i means no IdentitiesOnly: ~/.ssh/config decides instead.
|
||||
assert!(!rendered.contains(&"IdentitiesOnly=yes".to_string()));
|
||||
}
|
||||
|
||||
/// The second half of a transport: the connection that runs the command also
|
||||
/// carries the port that reaches it.
|
||||
///
|
||||
/// Both ends are pinned to loopback, which is what keeps a model server off
|
||||
/// the far machine's network -- asserted rather than trusted, because
|
||||
/// dropping the addresses is a one-word edit that still works on a machine
|
||||
/// nobody else can reach.
|
||||
#[test]
|
||||
fn a_forwarded_port_rides_the_same_connection_as_the_command() {
|
||||
let ssh = bare_host();
|
||||
@@ -306,53 +223,33 @@ mod tests {
|
||||
.expect("a forward");
|
||||
assert_eq!(rendered[forward + 1], "127.0.0.1:41000:127.0.0.1:24242");
|
||||
assert!(rendered.contains(&"ExitOnForwardFailure=yes".to_string()));
|
||||
// The half that is easy to lose: without a pty the far process outlives
|
||||
// the connection, because nothing closes a stdin it never reads.
|
||||
assert!(rendered.contains(&"-tt".to_string()));
|
||||
assert!(!rendered.contains(&"-T".to_string()));
|
||||
// Options come before the host, or ssh reads them as part of the
|
||||
// remote command.
|
||||
assert!(forward < rendered.len() - 2);
|
||||
assert_eq!(
|
||||
rendered.last().unwrap(),
|
||||
"exec 'llama-server' '--port' '24242'"
|
||||
);
|
||||
|
||||
// Nothing forwarded is nothing added: every other session is one of
|
||||
// these, and an -L on it would bind a port for no reason.
|
||||
let plain = argv(&command(Some(&ssh), "claude", &args(["-p"]), None, None));
|
||||
assert!(!plain.contains(&"-L".to_string()));
|
||||
// And a session that *is* spoken to over stdio keeps its raw pipe.
|
||||
assert!(plain.contains(&"-T".to_string()));
|
||||
assert!(!plain.contains(&"-tt".to_string()));
|
||||
}
|
||||
|
||||
/// The one character quoting must not swallow. A working directory typed as
|
||||
/// `~/repos/ai-app` was arriving as the literal directory `~`, and the
|
||||
/// remote shell reported it missing -- which reads as the path being wrong
|
||||
/// rather than the quoting being wrong, and cost an evening.
|
||||
#[test]
|
||||
fn a_leading_tilde_expands_and_nothing_else_does() {
|
||||
assert_eq!(quote_path("~"), "\"$HOME\"");
|
||||
assert_eq!(quote_path("~/repos/ai-app"), "\"$HOME\"/'repos/ai-app'");
|
||||
// Only leading, and only its own segment: a tilde anywhere else is an
|
||||
// ordinary character in a filename, and `~user` has no portable
|
||||
// expansion so it stays literal.
|
||||
assert_eq!(quote_path("/tmp/~/x"), "'/tmp/~/x'");
|
||||
assert_eq!(quote_path("~user/x"), "'~user/x'");
|
||||
|
||||
// And it reaches the script the remote shell is handed.
|
||||
assert_eq!(
|
||||
remote_script("claude", &args(["-p"]), Some(Path::new("~/repos/ai-app"))),
|
||||
"cd \"$HOME\"/'repos/ai-app' && exec 'claude' '-p'",
|
||||
);
|
||||
}
|
||||
|
||||
/// The same character, on the transport with no shell to expand it. The
|
||||
/// local branch runs the program directly, so a working directory of
|
||||
/// `~/repos/ai-app` would reach `current_dir` as the literal one-character
|
||||
/// directory `~`. The two transports have to agree about what a tilde means
|
||||
/// or a path is only portable by accident.
|
||||
#[test]
|
||||
fn a_local_cwd_expands_its_tilde_the_same_way() {
|
||||
let Some(home) = std::env::home_dir() else {
|
||||
@@ -363,7 +260,6 @@ mod tests {
|
||||
home.join("repos/ai-app")
|
||||
);
|
||||
assert_eq!(expand_home(Path::new("~")), home);
|
||||
// Leading only, and its own segment only -- `quote_path`'s rule.
|
||||
assert_eq!(expand_home(Path::new("/tmp/~/x")), Path::new("/tmp/~/x"));
|
||||
assert_eq!(expand_home(Path::new("~user/x")), Path::new("~user/x"));
|
||||
|
||||
@@ -379,9 +275,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn shell_metacharacters_cross_as_data_not_syntax() {
|
||||
// Expanding $HOME must not open a door for anything else: the rest stays
|
||||
// single-quoted, so this remains one absurd path rather than three
|
||||
// commands.
|
||||
assert_eq!(
|
||||
quote_path("~/'; touch /tmp/pwned; '"),
|
||||
r#""$HOME"/''\''; touch /tmp/pwned; '\'''"#,
|
||||
@@ -393,8 +286,6 @@ mod tests {
|
||||
assert_eq!(quote("$(whoami)"), "'$(whoami)'");
|
||||
assert_eq!(quote("it's"), r"'it'\''s'");
|
||||
|
||||
// The end-to-end version of the same worry: a working directory that
|
||||
// tries to close the quote and start a new command.
|
||||
let ssh = bare_host();
|
||||
let evil = Path::new("/tmp/'; touch /tmp/pwned; '");
|
||||
let rendered = argv(&command(Some(&ssh), "claude", &[], Some(evil), None));
|
||||
|
||||
Reference in new issue
Block a user