mirror of
https://github.com/herdrdev/herdr.git
synced 2026-10-01 08:00:20 +00:00
@@ -17,6 +17,7 @@
|
||||
- Bumped the client/server protocol version to 15 for socket API placement mutation event and response compatibility.
|
||||
|
||||
### Fixed
|
||||
- `herdr --remote` now prints clean remote attach failures and SSH authentication guidance instead of Rust Debug-formatted I/O errors when SSH authentication is denied. (#1034)
|
||||
- `herdr server stop` now waits until both server sockets are unreachable before returning, avoiding an immediate first-start failure when restarting right after replacing the binary.
|
||||
- Grok Build agent detection now tracks the current Grok Build UI: panes report working while responses, tools, and subagents run, and blocked on permission prompts and question dialogs, instead of falling back to idle mid-turn. (#1017)
|
||||
- Unix local Herdr clients no longer treat empty bracketed paste as a clipboard-image bridge; `herdr --remote` keeps using it for local-desktop image paste over SSH. (#986)
|
||||
|
||||
@@ -69,6 +69,15 @@ Native Windows `herdr --remote` is not part of the Windows beta. From Windows, S
|
||||
|
||||
By default, `herdr --remote` runs remote setup and the bridge through a temporary SSH config that includes your SSH config first, then adds fallback keepalive settings and a private per-attach control socket for connection reuse. Existing user keepalive settings win. Set `[remote].manage_ssh_config = false` to use plain `ssh` without Herdr's generated config or control socket.
|
||||
|
||||
Remote attach uses your normal OpenSSH authentication. If the target uses a passphrase-protected key in a non-interactive shell, script, CI job, or mobile terminal that cannot show the passphrase prompt, load the key into ssh-agent first:
|
||||
|
||||
```bash
|
||||
ssh-add
|
||||
herdr --remote workbox
|
||||
```
|
||||
|
||||
For any remote authentication failure, verify plain SSH access first with `ssh workbox`, then run `herdr --remote workbox` again.
|
||||
|
||||
By default, remote attach uses the normal restart/stop flow if it needs to replace or restart a running remote server. To opt into experimental live handoff for a supported running remote server, pass `--handoff`:
|
||||
|
||||
```bash
|
||||
|
||||
+7
-1
@@ -661,7 +661,13 @@ fn main() -> io::Result<()> {
|
||||
}
|
||||
|
||||
if let Some(remote_launch) = remote_launch {
|
||||
return remote::run_remote(remote_launch);
|
||||
let remote_target = remote_launch.target.clone();
|
||||
if let Err(err) = remote::run_remote(remote_launch) {
|
||||
eprintln!("error: {err}");
|
||||
remote::print_remote_error_hint(&err, &remote_target);
|
||||
std::process::exit(1);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let loaded_config = config::Config::load();
|
||||
|
||||
@@ -146,3 +146,78 @@ pub(crate) fn run_remote_client_bridge() -> std::io::Result<()> {
|
||||
"remote client bridge is not supported on Windows yet",
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn print_remote_error_hint(err: &std::io::Error, target: &str) {
|
||||
if is_remote_auth_error(err) {
|
||||
eprintln!(
|
||||
"hint: verify SSH access first with `{}`.",
|
||||
ssh_check_command(target)
|
||||
);
|
||||
eprintln!(
|
||||
"hint: if your SSH key has a passphrase, load it into ssh-agent with `ssh-add` before running `herdr --remote`."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_remote_auth_error(err: &std::io::Error) -> bool {
|
||||
let message = err.to_string();
|
||||
message.contains("Permission denied")
|
||||
&& (message.contains("(publickey")
|
||||
|| message.contains("(keyboard-interactive")
|
||||
|| message.contains("(password"))
|
||||
}
|
||||
|
||||
fn ssh_check_command(target: &str) -> String {
|
||||
format!("ssh {}", shell_quote(target))
|
||||
}
|
||||
|
||||
fn shell_quote(value: &str) -> String {
|
||||
if !value.is_empty()
|
||||
&& value.chars().all(|ch| {
|
||||
ch.is_ascii_alphanumeric()
|
||||
|| matches!(
|
||||
ch,
|
||||
'@' | '%' | '_' | '+' | '=' | ':' | ',' | '.' | '/' | '-'
|
||||
)
|
||||
})
|
||||
{
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
format!("'{}'", value.replace('\'', "'\\''"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn remote_auth_error_matches_ssh_auth_denied() {
|
||||
let err = std::io::Error::other(
|
||||
"remote platform detection failed: user@host: Permission denied (publickey).",
|
||||
);
|
||||
|
||||
assert!(is_remote_auth_error(&err));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_auth_error_matches_keyboard_interactive_denied() {
|
||||
let err = std::io::Error::other(
|
||||
"remote server status failed: user@host: Permission denied (keyboard-interactive).",
|
||||
);
|
||||
|
||||
assert!(is_remote_auth_error(&err));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_auth_error_ignores_non_auth_errors() {
|
||||
let err = std::io::Error::other("remote platform detection failed: unsupported platform");
|
||||
|
||||
assert!(!is_remote_auth_error(&err));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_check_command_quotes_remote_target() {
|
||||
assert_eq!(ssh_check_command("host name"), "ssh 'host name'");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user