diff --git a/Cargo.lock b/Cargo.lock index 6594bcb6dc..c05d760ba3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -699,6 +699,7 @@ dependencies = [ "tracing-subscriber", "unicode-width", "widestring", + "windows", "windows-sys", "wmi", ] diff --git a/Cargo.toml b/Cargo.toml index 02edde19b8..180fbc039c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,12 @@ portable-pty = { path = "vendor/portable-pty" } # UTF-16 SDDL input for interprocess security descriptors. widestring = "1.2" wmi = { version = "0.18.4", default-features = false } +windows = { version = "0.62.2", features = [ + "Win32_System_Com", + "Win32_System_Ole", + "Win32_System_TaskScheduler", + "Win32_System_Variant", +] } windows-sys = { version = "0.61.2", features = [ "Wdk_System_Threading", "Win32_Foundation", @@ -69,6 +75,7 @@ windows-sys = { version = "0.61.2", features = [ "Win32_System_Memory", "Win32_System_Ole", "Win32_System_Pipes", + "Win32_System_RemoteDesktop", "Win32_System_Threading", "Win32_UI_Input_Ime", "Win32_UI_Input_KeyboardAndMouse", diff --git a/docs/next/website/src/content/docs/persistence-remote.mdx b/docs/next/website/src/content/docs/persistence-remote.mdx index e876445778..05eca35988 100644 --- a/docs/next/website/src/content/docs/persistence-remote.mdx +++ b/docs/next/website/src/content/docs/persistence-remote.mdx @@ -42,6 +42,7 @@ Save repeat targets to work across several machines from one Herdr client: ```bash herdr machine add workbox --label "Build machine" --remote-session agents herdr machine add ssh://you@server:2222 --label "Production" --remote-session operations +herdr machine add windows-workbox --label "Desktop agents" --remote-desktop herdr machine list ``` @@ -68,7 +69,7 @@ herdr machine remove Renaming updates the label without reconnecting. Removing or disabling a machine disconnects only that machine from the client; its remote sessions keep running, even if the machine is unreachable. If you remove or disable the machine you are viewing, the client returns to Local. If Local is unavailable, it shows that and reconnects rather than selecting another remote. An unreadable or invalid saved-machine file leaves current connections unchanged and shows a notice; the client retries reading it automatically. -Saved profiles contain only the opaque ID, label, SSH target, explicit remote session, and enabled state. Passwords, private keys, agent tickets, and SSH control sockets are never stored in the machine catalog. +Saved profiles contain only the opaque ID, label, SSH target, explicit remote session, enabled state, and whether Windows desktop hosting is required. Passwords, private keys, agent tickets, and SSH control sockets are never stored in the machine catalog. The client uses local keybindings by default. Custom commands advertised by the selected machine still execute on that machine. Herdr does not copy local command plugins, configuration, executables, or secrets to SSH hosts; a missing remote command or plugin fails visibly. @@ -83,6 +84,14 @@ herdr --remote ssh://you@server:2222 In this mode, your local Herdr is a thin client. It connects over SSH, starts or attaches to the remote Herdr server, and streams the UI back to your local terminal. Because the client runs locally, Herdr can bridge local desktop features such as image clipboard paste into the remote session by copying the image to a remote temp file and pasting that path. +Windows SSH normally starts processes outside the signed-in desktop. To let agents on a Windows target interact with desktop apps, opt in explicitly: + +```bash +herdr --remote windows-workbox --remote-desktop +``` + +To start a desktop server, Herdr requires exactly one active desktop login for the SSH account. It asks for confirmation, creates a one-time Windows task to launch the server in that login, and removes the task after the server is ready. Automatic reconnects only attach; they do not create tasks or start a server. A server with the same Herdr session name in Windows Session 0 or another login is left running and reported as a conflict. Use a different `--session` or stop that server explicitly. This option does not sign in, unlock Windows, or move existing panes between Windows sessions. + By default, `herdr --remote` uses your local Herdr keybindings for that attach. This keeps local muscle memory even when the remote server has different config. The local keybindings are a snapshot from attach time; detach and reattach after editing local keybindings. Use `--remote-keybindings server` when you want the remote server config instead. Local custom command keybindings are not sent, because those commands would run on the remote host. For repeat targets, use your SSH config: @@ -100,7 +109,7 @@ Then attach with: herdr --remote workbox ``` -Remote attach supports Linux, macOS, and Windows local clients connecting to Linux or macOS hosts on x86_64 and aarch64. Herdr checks the remote platform, prefers a compatible `herdr` already on the remote `PATH`, then checks common direct, Homebrew, mise, and Nix profile install paths. Local and remote versions do not need to match once both support the stable endpoint generation. If no compatible binary exists, interactive runs prompt to install one to `~/.local/bin/herdr`; non-interactive runs fail instead of modifying the host. If `~/.local/bin` is not on the remote `PATH`, Herdr warns after install. Windows is not supported as the remote host. +Remote attach and saved SSH machines support Linux, macOS, and Windows local clients connecting to Linux or macOS hosts on x86_64 and aarch64, or Windows hosts on x86_64. Local and remote versions do not need to match once both support the stable endpoint generation. On Linux and macOS hosts, Herdr prefers a compatible `herdr` already on the remote `PATH`, then checks common direct, Homebrew, mise, and Nix profile install paths. On Windows hosts, Herdr checks `PATH` and the active managed release. If no compatible binary exists, interactive runs prompt to install or update one; non-interactive runs fail instead of modifying the host. Linux and macOS installs use `~/.local/bin/herdr`; Windows installs use the complete Windows package and its app-local ConPTY runtime. If `~/.local/bin` is not on the remote `PATH`, Herdr warns after install. 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. Existing user keepalive settings win. Linux and macOS clients also use a private per-attach control socket for connection reuse; Windows OpenSSH does not. Set `[remote].manage_ssh_config = false` to use plain `ssh` without Herdr's generated config or control socket. @@ -121,9 +130,9 @@ herdr --remote workbox --handoff If you SSH into the server first and run `herdr` there, Herdr runs entirely on the server and cannot access your local desktop clipboard beyond normal terminal text paste. -When your local and remote platforms match, Herdr can copy the current local binary for direct installs. For Homebrew, mise, and Nix installs, or when the platforms differ, it downloads the matching release asset for the current client version from `https://herdr.dev/latest.json`. +For Linux and macOS hosts, Herdr can copy the current local binary when the platforms match. For Homebrew, mise, and Nix installs, when the platforms differ, or for Windows hosts, it downloads the matching release asset for the current client version from `https://herdr.dev/latest.json`. -For local builds or custom binaries, set `HERDR_REMOTE_BINARY` to a local file path before running remote attach. +For local builds or custom packages, set `HERDR_REMOTE_BINARY` to a local file path before running remote attach. Use a bare Herdr executable for Linux or macOS targets and a complete `herdr-windows-x86_64.zip` package for Windows targets. ```bash HERDR_REMOTE_BINARY=target/release/herdr herdr --remote workbox diff --git a/docs/next/website/src/content/docs/windows-beta.mdx b/docs/next/website/src/content/docs/windows-beta.mdx index 255727a20e..15ff1fb745 100644 --- a/docs/next/website/src/content/docs/windows-beta.mdx +++ b/docs/next/website/src/content/docs/windows-beta.mdx @@ -32,7 +32,8 @@ For internal testing, `HERDR_MANIFEST_URL` can point the installer at a custom m | Local persistent sessions | supported | | Native panes through ConPTY | supported | | Windows Terminal / PowerShell app attach | supported | -| `herdr --remote` to Linux/macOS hosts | supported | +| `herdr --remote` and saved SSH machines to Linux/macOS/Windows hosts | supported; interactive attach and saved-machine setup can install or update Windows packages after confirmation; background reconnect only discovers installed packages | +| Remote agents in the signed-in Windows desktop | supported with `--remote-desktop` | | Remote clipboard images and image-file drops | supported | | `cmd.exe` panes | supported | | Native keyboard and mouse input | supported | @@ -49,6 +50,8 @@ For internal testing, `HERDR_MANIFEST_URL` can point the installer at a custom m Local persistent sessions continue running after the client detaches or its terminal window closes. Servers and pane processes launched through Windows OpenSSH also survive logout; run `herdr` again to reconnect. +Windows OpenSSH starts its normal server in Windows Session 0. Add `--remote-desktop` to a direct remote attach or `machine add` command when agents must interact with apps in the SSH account's active desktop login. Herdr asks before it uses a one-time Windows task for the initial start, removes the task after readiness, and leaves an existing server in another Windows session untouched. + Windows agent process detection scans descendants of the pane shell and recognizes direct agents plus common command wrappers, including npm/Node and Git Bash process chains. It follows Git Bash-launched agents across emulated `exec` boundaries, but it is not the same as Unix foreground process-group detection. Windows integration installation currently supports Pi, OMP, Claude Code, Codex, GitHub Copilot CLI, Devin CLI, OpenCode, Kilo Code CLI, Droid, Kimi Code CLI, Qoder CLI, and Antigravity CLI. Availability is narrower than on Unix; Herdr hides or rejects integrations whose install format is not supported on Windows. @@ -80,7 +83,7 @@ switch_ascii_input_source_in_prefix = true Other Windows IMEs are not supported by this option yet. See [Configuration](/docs/configuration/#prefix-input-source-switching). -Some Windows agents can receive `ctrl+v` and read clipboard images directly. Herdr's own clipboard-image reader is not wired into local native Windows panes, so agent-native image paste remains dependent on the terminal and agent. Agent image-paste shortcuts such as `alt+v` do not add a Herdr-managed local clipboard bridge. Remote clipboard image bridging is supported separately through `herdr --remote` to Linux and macOS hosts. +Some Windows agents can receive `ctrl+v` and read clipboard images directly. Herdr's own clipboard-image reader is not wired into local native Windows panes, so agent-native image paste remains dependent on the terminal and agent. Agent image-paste shortcuts such as `alt+v` do not add a Herdr-managed local clipboard bridge. Remote clipboard image bridging is supported separately through `herdr --remote` to Linux, macOS, and Windows hosts. Kitty graphics is enabled by default and depends on the outer terminal. Herdr emits Kitty graphics protocol output on Windows as it does on other platforms. This path has been exercised with Windows WezTerm hosting Herdr through WSL, but native Windows terminal and ConPTY combinations are not all verified. Windows Terminal does not expose the Kitty graphics path Herdr uses. Set `[terminal].kitty_graphics = false` if the outer terminal mishandles graphics output. @@ -118,7 +121,6 @@ For text paste, use `ctrl+shift+v` in Windows Terminal. Multiline text paste is | Capability | Status | | --- | --- | | Direct terminal attach (`herdr terminal attach`) | unsupported | -| Windows as a `herdr --remote` target host | unsupported | | Live server handoff | unsupported | | Unix file-descriptor handoff | unsupported | | Unix foreground process groups | unsupported | @@ -131,7 +133,7 @@ From Windows Terminal, use the same remote command as Linux and macOS: herdr --remote workbox ``` -The target host must run Linux or macOS. Herdr uses the installed Windows OpenSSH client and your SSH configuration. Windows OpenSSH does not use Herdr's Unix control-socket reuse, so key authentication through Windows `ssh-agent` is recommended to avoid repeated prompts during remote setup. +The target host can run Linux, macOS, or Windows. On Windows hosts, remote attach reuses a compatible package from `PATH` or the active managed release. Interactive direct attach and saved-machine setup prompt before installing or updating the complete package when needed. Background saved reconnect only discovers installed packages and cannot prompt for installation or updates. Herdr uses the installed Windows OpenSSH client and your SSH configuration. Windows OpenSSH does not use Herdr's Unix control-socket reuse, so key authentication through Windows `ssh-agent` is recommended to avoid repeated prompts during remote setup. Windows updates run through the Windows installer and update the active versioned release path. New terminals and reconnected SSH sessions receive that path; start Herdr there to use the updated client. Compatible running servers keep their panes alive. Restart a server later only when you need server-side changes from the release. Live handoff is Unix-only. diff --git a/nix/package.nix b/nix/package.nix index 87bf39f096..bc23c0602f 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -40,6 +40,7 @@ rustPlatform.buildRustPackage { fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ./..)) ( lib.fileset.unions [ ../assets + ../distribution/install.ps1 ../docs/next/api/herdr-api.schema.json ../src ../vendor/libghostty-vt diff --git a/src/checksum.rs b/src/checksum.rs index 4f61a33cb2..ed84169b07 100644 --- a/src/checksum.rs +++ b/src/checksum.rs @@ -25,7 +25,7 @@ pub(crate) fn verify_sha256(path: &Path, expected: &str) -> io::Result<()> { Ok(()) } -fn file_sha256(path: &Path) -> io::Result { +pub(crate) fn file_sha256(path: &Path) -> io::Result { let mut file = File::open(path)?; let mut hasher = Sha256::new(); let mut buffer = [0u8; 64 * 1024]; diff --git a/src/cli/machine.rs b/src/cli/machine.rs index a55e4611a7..b74e22583f 100644 --- a/src/cli/machine.rs +++ b/src/cli/machine.rs @@ -4,7 +4,7 @@ use crate::client::endpoint::{EndpointCatalog, ProfileId}; const HELP: &str = "Usage: herdr machine list [--json] - herdr machine add --label