CB-161: match a pid's ancestry against a pane, not just direct shell/foreground pid #202

Closed
agent wants to merge 1 commits from worker/cb-161-pane-ancestry-293510-1 into main
Member

Fixes fleetd issue #161: a member's grandchild process (e.g. a python3/curl helper opening its own MCP connection) matched no pane in PaneLocator.paneOwnsPid, so ConnectionIdentity.resolve fell through to loopback-trust and resolved that connection as the primary — a worker->primary privilege escalation.

terminalForPid now walks the caller's ancestry once (via a new injectable ParentResolver seam, bounded at 32 generations with a cycle guard) and matches any ancestor against a pane's shell_pid / foreground pids, instead of comparing the raw pid directly. The ancestor set is computed once per call and reused across both herdr clients in the CB-185 two-daemon path.

Scope: only PaneLocator, the new ParentResolver seam, and their tests. Authz, ConnectionIdentity's fallthrough, and loopback-trust policy are untouched, as scoped.

Tests: 6 new tests in PaneLocatorTest against a fake ParentResolver (shell_pid match, foreground match, grandchild match [the bug], no-match->null, cycle termination, ancestor-set-computed-once across the two-daemon constructor). Proved tests 3 (grandchild) and 5 (cycle) fail without the fix — details and full build output (mvn clean install: 1043 tests, 0 failures, BUILD SUCCESS) in REPORT-cb161.md at the repo root of this branch.

Could not verify against a live herdr daemon (no daemon access from this worker) — see "What I could NOT check" in REPORT-cb161.md.

Fixes fleetd issue #161: a member's grandchild process (e.g. a python3/curl helper opening its own MCP connection) matched no pane in `PaneLocator.paneOwnsPid`, so `ConnectionIdentity.resolve` fell through to loopback-trust and resolved that connection as the primary — a worker->primary privilege escalation. `terminalForPid` now walks the caller's ancestry once (via a new injectable `ParentResolver` seam, bounded at 32 generations with a cycle guard) and matches any ancestor against a pane's `shell_pid` / foreground pids, instead of comparing the raw pid directly. The ancestor set is computed once per call and reused across both herdr clients in the CB-185 two-daemon path. Scope: only `PaneLocator`, the new `ParentResolver` seam, and their tests. `Authz`, `ConnectionIdentity`'s fallthrough, and loopback-trust policy are untouched, as scoped. Tests: 6 new tests in `PaneLocatorTest` against a fake `ParentResolver` (shell_pid match, foreground match, grandchild match [the bug], no-match->null, cycle termination, ancestor-set-computed-once across the two-daemon constructor). Proved tests 3 (grandchild) and 5 (cycle) fail without the fix — details and full build output (`mvn clean install`: 1043 tests, 0 failures, BUILD SUCCESS) in `REPORT-cb161.md` at the repo root of this branch. Could not verify against a live herdr daemon (no daemon access from this worker) — see "What I could NOT check" in REPORT-cb161.md.
agent added 1 commit 2026-08-31 09:04:27 +02:00
CB-161: match a pid's ancestry against a pane, not just its shell/foreground pid
CI / contract (pull_request) Successful in 56s
CI / build (pull_request) Successful in 1m39s
7954a4d399
paneOwnsPid only compared a pid directly against a pane's shell_pid and
foreground_processes pids, so a grandchild process a member spawns (a python3
or curl helper opening its own MCP connection) matched no pane. Connection
identity then fell through to loopback-trust and resolved that connection as
the primary -- a worker->primary privilege escalation.

terminalForPid now walks the caller's ancestry once (bounded at 32
generations, with a cycle guard) via an injectable ParentResolver seam, and
paneOwnsPid checks pane pids against that ancestor set instead of the raw
pid. Production wiring defaults to ProcessHandle; tests drive a fake
pid->parent map.
Owner

Already on main — closing as superseded, not rejected. The change landed in 08ce9ae ("#161: resolve a pane by process ancestry, closing a worker->primary escalation"): ParentResolver with the PROCESS_HANDLE default, the ancestry walk in PaneLocator, and the FakeParentResolver test seam.

This one was also verified live: a member's curl/python3 grandchild used to resolve as primary and get full orchestration rights. The probe was an opencode member (no safety classifier in the way) calling read-only fleet_whoami over curl, and it now correctly answers worker.

Already on `main` — closing as superseded, not rejected. The change landed in `08ce9ae` ("#161: resolve a pane by process ancestry, closing a worker->primary escalation"): `ParentResolver` with the `PROCESS_HANDLE` default, the ancestry walk in `PaneLocator`, and the `FakeParentResolver` test seam. This one was also verified live: a member's `curl`/`python3` grandchild used to resolve as **primary** and get full orchestration rights. The probe was an opencode member (no safety classifier in the way) calling read-only `fleet_whoami` over curl, and it now correctly answers `worker`.
ltms closed this pull request 2026-08-31 17:08:08 +02:00
Some checks are pending
CI / contract (pull_request) Successful in 56s
CI / build (pull_request) Successful in 1m39s

Pull request closed

Sign in to join this conversation.