Repository navigation
fix(terminal): isolate tmux sockets to prevent cross-conversation handle reuse - #5485
Conversation
…dle reuse Co-authored-by: openhands <openhands@all-hands.dev>
|
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
Coverage Report •
|
|||||||||||||||||||||||||||||||||||
|
@all-hands-bot Please review this terminal isolation fix, including stale-handle safety, lifecycle cleanup, and the real-tmux regression coverage. This review request was posted by an AI agent (OpenHands) on behalf of the user. |
|
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Review summary
Scope. This change is correctly scoped to this repository: it modifies the agent-server's tmux startup cleanup (openhands-agent-server/openhands/agent_server/api.py) and the two terminal owners in openhands-tools. Linked issue #5484 is open, ready-for-dev, priority:medium, and its acceptance criteria are addressed.
Verified on the current head 8972e85 (detached checkout, no tracked files modified):
uv run pytest tests/tools/terminal/test_tmux_pane_pool.py tests/agent_server/test_api.py→ 72 passed, matching the PR's claim.- I ran the seven new regressions against the base revision (
1b2b5c8) with the same tests: all 7 fail (read/write/close× pooled/standalone, plus the cleanup test) and all pass on the head, so the new tests are meaningful before/after evidence rather than tautologies. - Current-head check runs are green. The only non-success run,
Validate PR description, failed at 21:21:38 and was superseded by a success at 21:22:19 on the same head.
Implementation assessment. Giving each terminal owner a fresh openhands-<uuid4().hex> socket removes the numeric-ID aliasing between owners, and cleanup globbing openhands- + 32 hex characters matches only the new socket format while leaving unrelated sockets alone. Recovery generations (_initialize_pool) get new sockets too. Startup cleanup only runs in the agent-server, while TmuxPanePool/TmuxTerminal are also constructed directly in SDK embeddings; I considered whether that leaks servers there, but tmux has no auto-start daemon and no code path creates a socket for cleanup to orphan, so that risk is inherent to tmux rather than introduced here.
Non-blocking design notes (no change requested):
- Socket accumulation.
TmuxPanePool.initialize()/TmuxTerminal.initialize()never remove their socket files; the single session they kill is gone, but the dead socket file remains underTMUX_TMPDIR/tmux-<uid>/until the next agent-server startup.TMUX_TMPDIRalready defaults to a per-pid directory that the agent-server lifespan removes, so the default deployment is bounded; this only matters whenTMUX_TMPDIRis set explicitly and the server runs long with many pools. The head's own test asserts the stale handle keeps addressing the dead UUID socket (''), which is the intended fix, not a regression. - The cleanup path adds
os.name/os.getuid/Path.is_socketthat the base did not use. These are guarded (os.getuidis only reached on POSIX, andPath.is_socketis available on Windows), and CI passes on Windows and macOS, so I see no demonstrated defect; worth a maintainer glance only because the agent-server runs on those platforms.
No material correctness, security, or compatibility defect found on the current head.
✅ APPROVED
Review: terminal tmux socket isolationI reviewed the diff, reproduced the isolation tests against the pre-fix source, and ran the broader suites in a tmux 3.5a environment. Summary: the core fix is correct and the regression coverage is genuine. A few lifecycle/cleanup issues are worth addressing before this leaves draft. What I verified
Findings1. (Medium) Each unique socket leaks a socket file; the recommended override path never reclaims it. tmux does not remove the socket file when the last session is killed — it lingers after both That is fine for the default per-process Suggestion: unlink the socket file after killing the session in 2. (Low) Stale-handle read fails silently, so "isolated" is not the same as "detected". After 3. (Low) Cleanup glob is narrower than the invariant.
4. (Info) Startup warning noise on the failure path. For any leftover socket file with no running server, 5. (Info) Test coupling. The VerdictThe isolation fix and its regression tests look sound; I'd merge once the socket-file lifecycle (finding 1) is addressed, since it's the one item that can degrade a long-running deployment. Findings 2–5 are optional hardening. Generated by OpenHands AI on behalf of the user. |
HUMAN:
I confirmed the bug before and ran the code after with no issue.
AGENT:
This PR was created by an AI agent (OpenHands) on behalf of the user.
Why
Terminal tools share a tmux socket. After server death/restart, numeric pane/window/session IDs are reused; retained libtmux handles can read, write to, or kill another conversation. Local saved observations showed a sleep command returning a different workspace and later another shell command. No private conversation data is included here.
Summary
Issue Number
Fixes #5484
How to Test
Requires tmux and the repository development dependencies.
Result: 72 passed. These are real shell/tmux integration paths, not mocked terminal operations. Tests use a temporary TMUX_TMPDIR for server destruction. Six isolation cases fail against the original terminal source; all pass after this change. The cleanup test also failed before its implementation update and passes afterward.
Full run:
uv run pytest tests/tools/terminal tests/agent_server/test_api.py-> 379 passed, 15 failed, 15 skipped. Existing concurrency/timeout/prompt tests failed in this local environment; a clean upstream worktree also failed concurrency and tmux timeout tests (2 failed, 9 passed on the executor/exit-code subset). Not every full-suite failure has been proven pre-existing. Keep draft pending CI/investigation.Ruff format/lint, pycodestyle, pyright, dynamic-attribute, import-rule, and tool-registration hooks pass when invoked individually. The all-hooks invocation was blocked by a broken local virtualenv pip seed cache while initializing the YAML formatter; no YAML changed. Existing compatible development dependencies were reused via UV_PROJECT_ENVIRONMENT and PYTHONPATH.
Video/Screenshots
Not applicable: backend change. Reproduction uses real tmux restart, output marker, command-created file, and stale-owner close assertions.
Design Doc
Each terminal owner gets
openhands-<uuid hex>rather than the globalopenhandssocket. Panes within one pool still share that pool server. This removes cross-owner numeric-ID reuse without racy ownership check-then-use logic. Trade-off: one tmux server process per owner rather than one shared process. Normal close kills the sole session; startup cleanup recognizes both formats.This is accidental session isolation, not a security boundary between same-UID processes. Startup cleanup retains its existing assumption that the configured TMUX_TMPDIR is owned by the Agent Server; independently running servers should not deliberately share it.
Type
Notes
The running local Agent Server was not modified or restarted. A release/dependency update and runtime restart are needed to deploy the fix. The human contributor must fill the HUMAN section before marking ready.
🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdkpython-node-runtimepython-node-runtimepython-node-runtimegolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:8972e85-pythonRun
All tags pushed for this build
About Multi-Architecture Support
8972e85-python) is a multi-arch manifest supporting both amd64 and arm648972e85-python-amd64) are also available if needed