Repository navigation
feat(herdr-core): show the core's other machines as PCs of their own - #2955
Conversation
Each machine in hc's machine.list (not temporary) is a PC, reached through this PC's core: a server per machine on loopback over a connection scoped to that machine's terminals and documents. Nothing of Ovis is installed there. Fixes #2497
|
Caution Review failedThe pull request is closed.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
- Each call first checks hc still names the machine by its id: after a rename, the old name no longer reaches whichever machine took it. - The machine's server refuses the routes that read or write this disk for its panes (files, diffs, pasted images, folder picker), and passes a new workspace's folder to that machine unchecked. - Its adapter socket goes in a short folder of its own, and an event line past the adapter's limit is no longer held whole.
hc takes a machine's permanent id wherever it takes its name (`<id>/t3`, `on`): a call made under the id cannot reach another machine that took the name between the name check and the call. The machine's server also refuses the pull request routes, which run gh and git on this disk.
|
Review rounds (omo + Codex Terra xhigh) Round 1 (head acc527a): two HIGHs and one MEDIUM from Codex, the same file problem from omo. Each was reproduced with three isolated hc cores, one of them in its own mount namespace, then fixed with a test that fails without the fix:
Final review at da4c017: Codex found no HIGH. omo found no HIGH and blocks on MEDIUMs, which go here as agreed after one fix round:
|
|
Local merge gate (owner-approved): |
#3044) ## Change Closes #3043, the follow-up #2814 left open ("Starting an agent from a pane stays a follow-up"). The issue is new: this PR is here so the code can be read alongside it, and it can wait for a maintainer's yes there. With `OVIS_RUNTIME=herdr-core` on Windows, **New workspace** and **New tab with agent** now start the agent in the pane's shell. Until now `server.agent_manifests` offered no kind there, because `agent.start` took only a POSIX shell: every pane Ovis opens on Windows runs hc's own PowerShell, a quoted name alone is a string in PowerShell, Ctrl-U types into its line, and hc names only the shell in front of a pane, whatever runs in it. On win32 only (`adapter.ts`, `shell-agent.ts`): - **Which panes.** hc's own PowerShell (`-NoExit -Command` with its prompt hook), a PowerShell with interactive switches only, and cmd without `/c`, `/k`, `/r` or `/v` (delayed expansion reads `!`, which nothing here quotes for). The app's program terminal (`powershell.exe -NoExit -Command "& 'claude'"`) stays `agent_pane_unsupported`, and so does a POSIX shell: Git Bash's MSYS rewrites an argument that looks like a path (`/review the diff` arrives as `C:/Program Files/Git/review the diff`), the variable that stops it would stay set for every command the agent runs, and what runs in WSL is in no Windows process table. - **The line.** PowerShell gets `& 'claude' ...`, cmd `claude "..."`, neither with Ctrl-U. cmd's name goes unquoted: a `.cmd` shim found on the PATH under a quoted name reads the current folder as its own (`%~dp0`). npm's own shim guards against this; one that reads `%~dp0` directly failed with `Cannot find module` in the real-shell check below. The name is only letters, digits and `-`. - **Arguments.** `powershellNativeArgs` encodes them for Windows PowerShell 5.1 and throws for what 5.1 would split (`"fix the bug"`, `x="a b"`) or PowerShell 7 might change. `cmdQuote` refuses `"` and `%`. A refusal answers `invalid_params` before anything is typed. - **Busy and started.** These come from the shell's descendants in a fresh process table (`windows-processes.ts`), console hosts aside. A table that cannot be read is busy before typing and says nothing while waiting. `AdapterOptions` gains `platform` and `processTable` (defaults `process.platform` and `windowsProcessTable`), so all of this is tested on any host. The input turn around the check and the typing stays. - **Another machine of the core (#2955, #2986).** Its terminals are not in this PC's table, so a Windows PC keeps answering no manifests for it, and `agent.start` refuses with `agent_pane_unsupported` before typing and without reading the table. A Linux or macOS PC starts there as before, by what hc reports. - **Command Code stays off the list.** Its kind and command are both `cmd`, which on Windows is cmd.exe: it would read as installed, and picking it would open a nested cmd.exe, so it is not offered and `agent.start` refuses it. `session-history.ts` resumes it as `cmd --session <id>`, which is left alone here. `server.agent_manifests` offers the other kinds on win32 again. `agents-routes.ts` keeps its logic; a comment and a test title that said Windows offers none are corrected. **Linux and macOS are unchanged.** `plainShell` is untouched and is what `platform !== "win32"` runs. The typed line with its Ctrl-U, the busy and started checks and their error codes are the same; see the differential run and the macOS runs below. Docs: `docs/guide.md` no longer says this runtime is refused on Windows, which #2814 made false, and the "Starting an agent" bullet describes the Windows shells, the POSIX exclusion, the busy rule and the refused arguments. Known limits: - An agent PowerShell starts through a `.cmd` file alone has its arguments read by cmd as well, and the quoting cannot know that: `%NAME%` and `^` change, and an `&` that cmd reads outside quotes, where a `"` in the argument can put it, ends the agent's line and runs the rest (`fix "a&echo ran&b" now` ran `echo ran`). The caller can type into that pane anyway, so this is no escalation, and the only free-text argument the UI sends is Codex's first prompt. An npm install also has a `.ps1`, which PowerShell prefers and which passed everything intact. Where the execution policy blocks scripts (`Restricted`, the Windows client default), PowerShell refuses that `.ps1` and does not fall back to the `.cmd`: the start fails, as typing it by hand does, and the pane says why. - cmd looks in the pane's folder before the `PATH`, as it does for what a person types: a `claude.cmd` in a repository runs in place of the agent. hc's default shell is PowerShell, so a cmd pane is rare. - A command PowerShell runs in itself (`Read-Host`) has no program below the shell for the table to show: the pane reads as idle, and the command takes the line as its input. Text left at a Windows prompt runs with the line, as no one key clears it in every editing mode. A new tab's shell has neither, so this takes a start into a pane someone is using. - The 10 s wait for the agent starts when the line is typed. A shell whose prompt takes longer than that (a slow profile) can answer `agent_start_failed` and run the agent afterwards, as a slow POSIX shell can. - The started check reads the first two words of a program's command line, as on POSIX, so an agent run as `node --flag script.js` can answer `agent_start_failed` while it runs. ## Validation Red first: with the tests and without the fix, 6 new Windows adapter tests failed and `shell-agent.test.ts` failed to load (`powershellNativeArgs` not exported). The POSIX pins passed on the unchanged code. End to end with a real hc: a real core in a scratch session, the real `server/index.ts` and a browser, with a stand-in agent binary (`.github/platform-check/e2e.ts` on my fork, not part of this PR). It opens a workspace, types through the API and through the page's terminal, starts the agent twice, reads back the arguments it received, closes everything and checks nothing is left: | Host | hc | Shell | Result | | --- | --- | --- | --- | | Windows 11 (real PC) | 0.1.18 | Windows PowerShell 5.1 | 23 ok, 0 failed; `say "hi"`, an empty argument, `C:\dir with space\`, `it's`, `$env:PATH` and `한글 인자` arrive unchanged; `"fix the bug"` is refused with nothing typed | | GitHub `windows-2025` | 0.1.17 | PowerShell 7 | 23 ok, 0 failed; `a b`, `it's`, `$env:PATH` and `한글 인자` arrive unchanged; `"fix the bug"` is refused with nothing typed | | GitHub `macos-15-intel` | 0.1.17 | zsh/bash | 19 ok, 0 failed | | GitHub `macos-14` and `macos-26` (arm64) | 0.1.17 | zsh/bash | 19 ok, 0 failed on each | | GitHub `ubuntu-24.04` | 0.1.17 | bash | 19 ok, 0 failed | A real Claude Code 2.1.318 too, on the real PC with the Herdr app's own core (hc 0.1.18), through this PR's code merged in my fork's main: **New workspace** with Claude answered `agent_started` in 1.2 s, the pane showed in the snapshot as `claude`, `idle`, its chat read `claude-transcript`, and closing it left the app's other terminals as they were. On upstream's code, without this PR, the same script on Windows gives `{"agents":[]}` from `/api/agents` and `agent_pane_unsupported`. Everything together on GitHub's hosted machines, this branch merged with the other open Windows herdr-core branches on `f2bbee9e`, on my fork: upstream's `ci.yml` ([run](https://github.com/photojupi/ovis/actions/runs/42078075843)): `bun run check fast` passes (unit: 12253 pass, 0 fail, 39 skip), and so do Integration tests, every Browser regression shard, Rust PTY host, the herdr-core live contract and Native Windows install. That job's `Release guards` step fails on a fork with no `v*` tags; with the tags it says this is no release. `Native macOS session identity` got no `macos-15` runner, twice; its test files ran in the whole unit suite below, with no failure among them. Linux, WSL Ubuntu, Bun 1.4.2, clean checkout of this commit on `5b51cbe3`: - `bun run typecheck` passes. `server/herdr-core/adapter.test.ts`, `server/shell-agent.test.ts`, `server/agents-routes.test.ts`, `server/agent-launch.test.ts`, `server/conventions.test.ts` and `scripts/changelog-fold.test.ts`: 135 pass, 0 fail. - Differential against `0011d8fd`, each version's real adapter over a real unix socket with the same scripted core (a scratch script, not committed): 24,440 combinations of own argv, foreground, pid, kind, arguments and what comes up after typing, on `linux` and again on `darwin`, gave the same answer (code and message), the same typed bytes and the same hc calls: 0 mismatches. `server.agent_manifests` is the same too. As a control, 1,504 of 2,000 of them differ under `platform: "win32"`. An independent fuzz with injected faults, 12,000 cases each on `linux`, `darwin` and `freebsd`: 0 mismatches. Windows 11 (5.1.26100.9711, Bun 1.4.2): - `bun run typecheck`: pass. - `server/herdr-core/adapter.test.ts` cannot reach the adapter on Windows at the base commit (5 pass, 64 fail: `setup()` gives a plain path, which the adapter listens on and the client maps to a pipe). With a temporary uncommitted patch of `setup()` to `adapterAddress`: the agent tests alone 24 pass, 0 fail. - `powershellNativeArgs`' output through real `powershell.exe` 5.1 (`-File`) into `node.exe`, no hc. Every string of length 0 to 5 over `a`, space, `"`, `\` and a no-break space (3,906): the 3,174 accepted arrive intact; of the 732 refused, the unrefused encoding would split or change 525, and the other 207 hold only a no-break space and are refused on 5.1's own test. - cmd, which hc does not open by default: the real adapter and process table with a real `cmd.exe` in a ConPTY of its own, behind a stand-in for the core. A Bun binary, npm's `.cmd` shim and one reading `%~dp0` each got 9 arguments back unchanged, among them `a&b` and `^caret`. macOS: a real hc 0.1.17 and the real server on GitHub's `macos-14` and `macos-26` (arm64) and `macos-15-intel`, with a stand-in agent: `.github/platform-check/e2e.ts` on my fork gives 19 ok, 0 failed on each ([arm64 run](https://github.com/photojupi/ovis/actions/runs/42084111704), [Intel run](https://github.com/photojupi/ovis/actions/runs/42078075706)): a workspace opens, the terminal takes typing through the API and the browser, the agent starts with `it's`, `$HOME`, `a\b`, `"q" r`, an empty argument and `한글 인자` intact, and closing leaves nothing. `server/herdr-core/live.test.ts` on that hc: 7 pass, 1 fail, the schema test, which pins hc 0.1.14 as `ci.yml` does and fails without these branches too. The whole unit suite on `macos-15-intel`, all these branches merged against their base: the base's 41 failures are the same on both sides (`tar --transform`, workflow files and the like, none in a file these branches change but `server/session-history.test.ts`, whose failures match by name); a first, slower run's two extra timeouts, in files these branches do not touch (`server/pi-delivery.test.ts`, `server/windows-native.test.ts`), did not come back on the rerun. No real MacBook: the Herdr app's own window on macOS was not checked. Not run: - A real Codex or any other real agent: only Claude Code ran for real, as below. - A cmd pane with a real hc, and the real Herdr app's window on macOS. - `bun run check full` as one command, and the Windows VM and `eamac` lanes, which are the maintainer's. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: devswha <devswha@gmail.com>




Fixes #2497
What
With
OVIS_RUNTIME=herdr-core, every other machine the core reaches (hc machine add vps, or added in the Herdr app) is now a PC of its own in the sidebar, reached through this PC's core. Nothing of Ovis is installed on that machine. Before, those machines did not show at all (since #2795 their terminals were only kept out of this PC).A PC of the core shows:
herdr-appdocument lays them out (doc.get { on }), or as Ovis's labels andovis-layoutdocument there;terminal.run { on }).How
The server already shows a remote PC through an endpoint
{ url, token }that serves herdr's API (an SSH bridge, or the relay forward since #2829). A machine of the core gets the same thing, run locally:server/herdr-core/machine-scope.ts:MachineScopedConnection, anHcConnectionthat shows the core as that machine sees itself. Onlylab/…terminals are listed, without the prefix (parenttoo). Every address goes back with the prefix.terminal.run,doc.getanddoc.setgeton: lab. The event stream keeps only that machine's events, including itsdocevents (machine: "lab"). The adapter, the attach and theherdr.seenmarker therefore run unchanged.server/herdr-core/machine-entry.ts: a server per machine (createServer({ bridgeOnly: true })) on loopback with a random token, over that connection.OVIS_HC_MACHINEselects the scoped connection inruntime.ts.server/herdr-core/machine-bridge.ts:listHcMachines(machine.list, temporary machines left out) andstartMachineBridge, which starts that server as a child. The child runs with an empty home folder of its own and only the program and core-finding variables. This PC's agent transcripts and processes are not that machine's, so none can be taken for one of its panes. For the same reason,pane.process_infogives no pids for another machine, andworktree.*is refused there, because git would run here. The child ends when this server's stdin pipe to it closes.server/machines.ts: a new PC kind,hc, read frommachine.listevery 5 s. Its id ishc-<machine id>, which is stable across renames. A rename restarts its server under the new name, because addresses change. A machine that is not connected shows hc's reason. hc PCs are never saved tomachines.json, and rename, remove and setup are refused ("add, rename or remove it withhc machineor in the Herdr app"). In the sidebar, the PC's manage panel says the same instead of offering those buttons.Answers to the issue's open questions:
Safety (from the review round, each reproduced first with three isolated cores, one in its own mount namespace):
hc machine rename). Every scoped call goes to hc under the machine's permanent id (<id>/t3,on: <id>), which hc takes wherever it takes the name, so no call can reach another machine. The current name, looked up by id first, is used only to filter what hc lists and reports by name. Before this, input to the oldlablanded onvpsafterlab→lab2,vps→lab; after it, the call is refused and nothing reachesvps.localFiles: false) refuses the routes that read or write this disk for its panes:/api/fs/*,pane/files,pane/image,workspace/diff/*,workspace/directories,workspace/pr*(gh and git run there). Before this, it served a file only this PC has and could not see a file only that machine has. A new workspace's folder goes to that machine unchecked: a folder that exists only there now works.Known limits (in docs/guide.md):
Validation
bun run check fast: pass on the merged head (11959 pass, 0 fail; typecheck, build, generated types fresh).bun test server/herdr-core/ server/hc-machines.test.ts: pass. The new scoped-adapter tests and the manager tests go red when their owner is broken:ondropped, another machine's terminals kept, pids kept, hc PCs saved, no restart on rename, no id check, disk routes served.OVIS_TEST_HC_BIN=<hc 0.1.14> bun test ./server/herdr-core/live.test.tsand the same with hc 0.1.17: pass. The new live test starts two isolated cores, adds the second withhc machine add --via, then starts, types into and reads a terminal there through the scoped adapter.lab(another isolated core).labappears as a connected PC with its own workspaces. Its live terminal attaches with history, and typing in the browser reachedlab's shell (hc readthere showed it). Workspace create, input and read through/api/machines/hc-…/all work. The local PC shows only its own terminal. Stopping the server ended the per-machine child.