Skip to content

fix(browser): keep generated sessions reachable after scratch cleanup - #6083

Merged
kyleseaman merged 1 commit into
kirodotdev:mainfrom
Tiger-0512:fix/reap-orphan-browser-daemons-5986
Aug 27, 2026
Merged

kyleseaman merged 1 commit into
kirodotdev:mainfrom
Tiger-0512:fix/reap-orphan-browser-daemons-5986

Conversation

@Tiger-0512

@Tiger-0512 Tiger-0512 commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Problem / Motivation

A playwright-cli daemon deliberately outlives the individual command that opened it. Kiro Crew points each agent process's TMPDIR at a per-process scratch directory, and Playwright derives its Unix control socket from that temporary root. When scratch is reclaimed, the socket disappears while the daemon and Chromium tree remain alive. The CLI then answers "browser is not open" and the surviving daemon is no longer reachable through ordinary lifecycle commands.

Why it matters

On one long-running host this produced 19 detached cliDaemon processes, with roughly 10.7 GB belonging to daemons no live session claimed. Preserving the control socket does not automatically kill crash orphans, but it removes the permanent-unreachability root cause: generated sessions remain visible to playwright-cli list and can be closed through the CLI's own protocol after their originating scratch directory is gone.

What changed (motivation → approach → change)

The earlier revisions of this PR attempted automatic process-table reaping. Review correctly established that unattended SIGKILL could not prove cross-process ownership safely on every supported platform: scratch metadata is agent-writable, PID/group identities can be reused, and macOS has no pidfd equivalent. That design is fully removed.

This revision implements the smaller safe direction from #5986: keep generated browser sessions reachable, without adding automatic close authority.

  1. PR fix(browser): give each agent process its own playwright-cli browser session #5960 already gives each agent process a generated PLAYWRIGHT_CLI_SESSION (kc-<8hex>). For a newly generated name only, both ACP spawn paths now inject short, per-session lifecycle roots:
    • PWTEST_SOCKETS_DIR=<base>/<8hex>/s
    • PWTEST_DAEMON_SESSION_DIR=<base>/<8hex>/d
      The default base is <data-home>/pw; absolute operator-configured roots are treated as bases and namespaced identically. A session's ordinary playwright-cli list therefore does not enumerate peer chat families.
  2. These roots sit outside per-process scratch, so both the daemon socket and <workspace-hash>/<session>.session metadata survive scratch reclamation. This does not create a new same-UID security boundary: latest main already documents that Playwright's user-global default registry lets list show every browser on the machine. The diff reduces ambient visibility and adds no close/read/signal authority.
  3. Operator-provided non-kc- session names preserve their complete existing Playwright location environment. They are never redirected. For generated sessions, lifecycle bases are selected only from the trusted gateway os.environ plus the generated session name; caller/app/cron extra_env is excluded, and returned trusted paths override caller PWTEST values in the final child env. Relative configured lifecycle roots are rejected because gateway and agent working directories can differ.
  4. Before injecting either test-prefixed upstream hook, Kiro Crew resolves the active @playwright/cli package through the existing package anchor and verifies exact playwright-core execution snippets for both variables. Missing/renamed hooks fail back with a warning; a stale standalone package can never satisfy a different active installation. The source verdict is bounded and cached by path/mtime/size.
  5. Kiro-Crew-owned lifecycle directories are created owner-only and then checked through the fail-loud platform restriction helper before their env vars are exported. The final namespaced AF_UNIX path is checked against Playwright's 103-byte budget; overlong roots fail back to existing behavior.
  6. Filesystem preparation runs through each spawn path's cancellation-safe sandbox guard. No new event-loop blocking window is introduced.

This PR deliberately performs no registry read, socket connection, external CLI execution, automatic close, or numeric PID/process-group signal. Crash-orphan reclamation remains an open design problem under #5986; this change enables operator-controlled cleanup and future graceful lifecycle work without creating cross-session kill authority.

Tests

  • test_browser_cli_launch.py
    • generated session names are unique and regenerate inherited kc- values;
    • both ACP spawn paths inject the generated session and stable lifecycle env;
    • operator session overrides receive no PWTEST_* location variables;
    • socket/registry bases are namespaced per generated session, relative roots are refused, owner restrictions are applied, preparation is all-or-nothing, and overlong AF_UNIX roots fail closed.
  • test_browser_cli_install.py
    • the active serving Playwright package is accepted only when both lifecycle hooks are present on their execution paths;
    • a missing hook fails closed and a stale fallback package cannot satisfy the active installation.
  • test_acp_client.py
    • an operator PLAYWRIGHT_CLI_SESSION=chrome remains unchanged and does not enter Kiro Crew lifecycle locations.
  • Local validation after the final rebase: 882 ACP/browser tests passed; black gate, isort, flake8, mypy (--platform linux), docs-lint, subprocess-encoding gate, spawn audit, and portability regex all passed. One unrelated liveness timing test failed only in a parallel aggregate run and passed standalone on both this branch and clean latest main.
  • Independent GPT review found one cancellation-guard issue; it was fixed by routing browser_socket_env through AcpClient._to_thread_guarding_sandbox. Later focused review confirmed the same-UID peer-control concern is pre-existing on main, while the diff reduces default visibility; the real relative-root and stale-package probe issues were fixed. No new blocker remained.

Manual verification

N/A — this revision changes only inherited environment placement. Unit tests pin the exact env passed by both spawn paths, and the upstream Playwright source contract for PWTEST_SOCKETS_DIR / PWTEST_DAEMON_SESSION_DIR is exercised through those paths. No user-visible UI changes.

Related Issues

Related: #5986 — partial fix only; this PR intentionally does not close the crash-orphan issue.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

Why no screenshot: backend-only environment and lifecycle-path change; no rendered surface is touched.

@github-actions github-actions Bot added the fork Pull request from a fork (external contributor) label Aug 26, 2026
@Tiger-0512
Tiger-0512 marked this pull request as ready for review August 26, 2026 15:55
@Tiger-0512
Tiger-0512 requested review from a team and buluoray August 26, 2026 15:55
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 26, 2026
@Tiger-0512
Tiger-0512 force-pushed the fix/reap-orphan-browser-daemons-5986 branch from fef9877 to 3fa1d31 Compare August 27, 2026 00:29
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 27, 2026
@github-actions

github-actions Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Design Review (Fable 5, fork) — 🟡 CONCERNS

Design-level review of 09dd326dfc5c4f643070f84dd1fb11e152c4d432 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: CONCERNS

Sound partial fix, but it trades unbounded orphan daemons for unbounded per-spawn lifecycle directories in the persistent data home, with pruning deferred to the same open issue.

Watch

  • Every agent-process spawn mints a fresh kc-<8hex> session and creates <data-home>/pw/<8hex>/{s,d} before any browser command runs; nothing ever deletes them ("safe connect-test-then-prune remains part of Agent browser daemons are never reclaimed: socket dies with the per-process scratch dir (~10.7 GB stranded, oldest 4 days) #5986"). Unlike scratch, the data home has no reclamation, so a busy host accumulates dir pairs forever — the fix for a leak ships its own leak, just cheaper. At minimum, create the dirs lazily-per-spawn only if this is bounded, or land a trivial empty-namespace sweep with it.
  • The enable gate greps exact byte snippets (process.env.PWTEST_SOCKETS_DIR ||) in two named playwright-core files. A routine Playwright upgrade that refactors or re-bundles those files silently reverts sockets to scratch — the 10.7 GB failure mode returns with only a log warning, and no test goes red because the fixture fakes the tree. The fail-back is correct; the detection needs a canary against the real installed package or a surfaced (not just logged) signal.

Suggestions

  • A false-positive grep costs only two unused dirs (behavior identical to fail-back), while a false-negative reinstates the leak — the asymmetry argues for a looser presence check (or unconditional injection) over exact-snippet matching in pinned files.

[DESIGN-REVIEWED] 09dd326

@github-actions

github-actions Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

GPT 5.6 Review (fork) — ✅ no blocking findings

Reviewed 09dd326dfc5c4f643070f84dd1fb11e152c4d432 via the fork AI-review pipeline; updated in place on each push.

Review details

FINDING -- src/kiro_crew/acp/client.py:2805, src/kiro_crew/acp/runtime.py:1061 -- when lifecycle preparation returns {}, caller-supplied PWTEST_* values remain in the spawned environment -> Fix: remove both keys before applying trusted lifecycle additions.

[GPT-REVIEWED] 09dd326

@github-actions

github-actions Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5, fork) — 🟡 CONCERNS

Premise-level review of 09dd326dfc5c4f643070f84dd1fb11e152c4d432 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All the reused mechanisms check out in the base tree (_cli_package_dirs, _manifest_for_cli_package, make_owner_only_dir/restrict_dir_to_owner, _to_thread_guarding_sandbox in both spawn paths), and PWTEST appears nowhere in base, so nothing is duplicated. The module's two existing operator-override siblings (cli_env_overrides, browser_session_env) both practice a hands-off doctrine the new code diverges from. Here is the review.

First-Principles-Verdict: CONCERNS

The reachability fix earns its place; the operator-root rewriting and the upstream source-scan are extra surface whose zero options cost nearly nothing.

What this change ships

Intent: keep an agent's browser daemon closeable after its scratch directory is reclaimed — a FIX.

  1. Generated browser sessions survive scratch cleanup and stay closeable via the CLI — justified (the fix)
  2. One agent's playwright-cli list no longer shows peers' browsers — declared, justified
  3. Operator-set PWTEST roots are silently rewritten to a per-session subfolder — contradicts the module's own override doctrine
  4. Relative operator roots refuse the whole feature — exists only because of item 3
  5. Every spawn scans installed playwright-core sources to verify the env hooks — oversized
  6. Two owner-only dirs created under data-home per agent spawn — justified (untrusted-agent boundary)
  7. New public socket_dir/daemon_dir helpers — one consumer each
  8. Spec section in browser.md — mandated same-commit

Watch

  • Item 3 inverts a doctrine with 2 counted siblings in the same module: cli_env_overrides (launch.py:147) and browser_session_env (launch.py:188) both return {} when the operator set the variable. Rewriting an operator's PWTEST_SOCKETS_DIR to <root>/<8hex>/s changes what their setting means, and item 4's validation/rejection surface exists solely to support that rewrite.
  • Item 5's zero option is byte-identical behavior today: current playwright honors the vars, and after a hypothetical upstream rename the fail-back outcome (sockets back under scratch) is the same with or without the check — the ~60 lines plus lru_cache buy one warning line.

Subtractions

  • Replace the base-namespacing branch in browser_socket_env with the sibling doctrine: operator-set SOCKETS_ENV/DAEMON_DIR_ENV → return {}. Deletes the absolute-path validation, the relative-root rejection, and 2 tests; reachability is still fixed (the operator's root is outside scratch by their own choice).
  • Defer cli_lifecycle_env_supported + _source_contains (install.py, ~60 lines, 3 tests) to when an upstream rename is observed; inject unconditionally.
  • Underscore-prefix socket_dir/daemon_dir — 1 consumer each (browser_socket_env), no external caller.

[FIRST-PRINCIPLES-REVIEWED] 09dd326

@github-actions

github-actions Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Opus 4.8 Review (fork) — ✅ no blocking findings

Reviewed 09dd326dfc5c4f643070f84dd1fb11e152c4d432 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 09dd326

@Tiger-0512
Tiger-0512 force-pushed the fix/reap-orphan-browser-daemons-5986 branch from 3fa1d31 to 029d2c7 Compare August 27, 2026 01:11
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 27, 2026
@Tiger-0512

Copy link
Copy Markdown
Contributor Author

Both findings on 3fa1d31 addressed in 029d2c7.

  • platform_compat.py — PID identity released before the group signal — FIXED (Linux), residual documented (macOS).

    Daemon group exits and its ID is reused after token verification -> orphan sweep signals the unrelated replacement group.

    The window was real, and on Linux it is now closed rather than narrowed: kill_self_led_group_pinned opens a pidfd on the target before validation and holds it across killpg. A pidfd holds a reference to the process object, so the kernel cannot recycle that PID — and therefore cannot create a new group under that id — until the fd is released after the signal returns. A process that vanished before the pin is refused at pidfd_open without signalling anything. Three new tests pin the pinned path, the vanished-process refusal, and the no-pidfd fallback.

    Fix: Revert the group-kill reaper until signaling remains tied to validated identity.

    Pushing back on the revert specifically, for the macOS / pre-5.3-kernel fallback that remains token-guarded: POSIX offers no stronger primitive there — there is no pidfd equivalent and no handle to hold, which is precisely the residual kill_process_tree_pinned's own contract documents ("POSIX is deliberately untouched … the residual probe-to-signal window there is the pre-existing one"). Every existing kill path in this module and in session_pid's orphan sweeps carries the same in-process check-to-signal residual; the fallback here re-reads the start token immediately before the signal, which is the tightest guard the platform admits. Reverting would trade a microsecond-scale, platform-irreducible window (already narrower than the shipped sweeps') for reintroducing a measured ~10.7 GB standing leak.

  • session_pid.py:1500/:1623, browser_cli/reap.py:192 — function-local imports — FIXED. All three hoisted to module scope. Verified first that no cycle exists: browser_cli/__init__.py is docstring-only, and sel's only package imports are platform_compat / atomic_write / config.paths, none of which reach session_pid or acp.

@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 27, 2026
@Tiger-0512
Tiger-0512 force-pushed the fix/reap-orphan-browser-daemons-5986 branch from 029d2c7 to 8c7de92 Compare August 27, 2026 02:19
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention labels Aug 27, 2026
@Tiger-0512

Copy link
Copy Markdown
Contributor Author

All findings on 029d2c7 addressed in 8c7de92.

GPT — BLOCKING reap.py:171 — token captured after the argv/age snapshot — FIXED.

Old daemon exits after the caller reads argv/age and its PID is reused by a detached marked process with a dead scratch owner -> stale daemon data validates against the replacement token.

Correct: the mixed-generation snapshot was the gap. Fixed along the suggested shape — the kill phase in kill_orphan_mcps now captures the start token BEFORE its cmdline read, and kill_browser_daemon(pid, cmdline, age, start_token) takes that pre-read token as a required parameter instead of deriving its own. Every identity read now postdates the token, so either the data describes the pinned process, or a pre-token recycle means every read consistently describes the replacement — which must then satisfy the full predicate (including its own fresh age) on its own merits; a post-token recycle flips the pinned re-check in kill_self_led_group_pinned and refuses. The wiring test asserts the exact pre-read token reaches the kill, and a missing-token call refuses before any read.

First Principles — Watch: description claims a lazy import the diff does not have — FIXED. The import was hoisted to module scope in the previous round at GPT's own advisory; the PR body's "What changed" is updated in place to describe the module-level import.

First Principles — Subtraction 1 (drop the sig parameter) — FIXED. kill_self_led_group_pinned(pid, expected_start_time) now hardcodes SIGKILL; the one call site and tests updated.

First Principles — Subtraction 2 (drop the double predicate evaluation) — FIXED. The kill phase routes on the read-nothing cmdline_is_browser_daemon shape check; the full environment-reading predicate runs once, inside kill_browser_daemon. This halves the per-kill macOS ps spawns as suggested.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention labels Aug 27, 2026
@github-actions github-actions Bot added the readiness: action required A blocking check or review needs attention label Aug 27, 2026
@Tiger-0512
Tiger-0512 marked this pull request as draft August 27, 2026 11:48
@Tiger-0512
Tiger-0512 force-pushed the fix/reap-orphan-browser-daemons-5986 branch from 79563d9 to f03ae2d Compare August 27, 2026 12:21
@iamwhatever

Copy link
Copy Markdown
Contributor

👋 Hi! This PR is currently in draft status. Workflow runs won't be auto-approved until it's marked as ready for review.

When you're ready, click "Ready for review" and the workflows will be approved on the next cycle automatically.

@Tiger-0512

Copy link
Copy Markdown
Contributor Author

All concerns on 79563d94 are dispositioned in f03ae2d0.

GPT BLOCKING — shared roots expose browsers across chat families — REBUTTED as pre-existing, with a narrowing change retained.

Two generated sessions in one workspace -> approved playwright-cli list reveals the peer name -> -s=<peer> reads or controls its authenticated browser.

That authority exists on origin/main before this diff. Playwright's default registry is user-global (~/Library/Caches/ms-playwright/daemon on macOS / ~/.cache/ms-playwright/daemon on Linux), and current Kiro Crew explicitly documents it: config/prompt.md:144 says playwright-cli list shows other sessions' browsers; builtin_skills/web-browse/SKILL.md:157-158 says it shows every browser on the machine. A same-UID agent can already learn a peer kc-* name and pass -s=<peer>, or supply arbitrary PWTEST roots. This diff adds no close/signal/socket-read authority.

The implementation nonetheless narrows ambient visibility: generated sessions now receive separate <base>/<8hex>/s and <base>/<8hex>/d roots, so an ordinary list in one family does not enumerate peers. Focused independent review confirmed that scanning the common parent or peer environment uses the same pre-existing same-UID authority and is not a diff-introduced regression blocker.

Design Watch — upstream test hooks may disappear silently — FIXED. The active @playwright/cli package (first/most-specific package resolved through the existing launcher anchor) is now checked before injection. Its serving playwright-core must contain the exact execution snippets process.env.PWTEST_DAEMON_SESSION_DIR and process.env.PWTEST_SOCKETS_DIR ||. Reads are size-bounded and cached by path/mtime/size. Missing/renamed hooks warn and fail back. A stale standalone package cannot satisfy a different active installation; positive, missing-hook, and stale-fallback tests pin this.

Design Watch — permanent small-file growth — ACCEPTED-AND-DEFERRED to #5986. Per-session namespace directories can remain after a crash. Safe connect-test-then-prune requires the same cross-process ownership decision this PR deliberately avoids; adding unattended deletion here would reintroduce the withdrawn authority problem. The limitation and follow-up are now explicit in browser.md and #5986 remains open.

Additional correctness finding from focused review — relative configured PWTEST roots — FIXED. Relative roots are rejected before directory creation because gateway and agent CWDs can differ. Absolute configured roots are treated as bases and namespaced per generated session.

@Tiger-0512
Tiger-0512 marked this pull request as ready for review August 27, 2026 12:25
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 27, 2026
@Tiger-0512
Tiger-0512 force-pushed the fix/reap-orphan-browser-daemons-5986 branch from f03ae2d to 4639708 Compare August 27, 2026 12:58
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 27, 2026
@Tiger-0512
Tiger-0512 marked this pull request as draft August 27, 2026 13:38
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 27, 2026
Move Playwright daemon sockets and session metadata for generated per-process browser sessions out of agent scratch and into fixed owner-restricted data-home directories. This preserves ordinary playwright-cli reachability after the agent or scratch directory is gone, while operator-provided session locations remain untouched.

The change deliberately adds no automatic close, external CLI execution, registry read, or process signal. Crash-orphan reclamation remains open under kirodotdev#5986 until cross-process ownership can be proven safely across supported platforms.

Related: kirodotdev#5986
@Tiger-0512
Tiger-0512 force-pushed the fix/reap-orphan-browser-daemons-5986 branch from 4639708 to 09dd326 Compare August 27, 2026 13:47
@Tiger-0512

Copy link
Copy Markdown
Contributor Author

The blocking caller-controlled-root finding is fixed in 09dd326d.

App cron PWTEST_*=/home/user/.ssh -> extra_env -> ACP spawn -> gateway creates/chmods directories under .ssh.

Correct. The prior code passed the fully merged child environment — including caller/app/cron extra_env — into browser_socket_env, giving caller values authority over gateway filesystem preparation.

Both ACP spawn paths now construct lifecycle input as exactly {**os.environ, **browser_env} and call the helper only when browser_session_env generated a kc-* name. Caller extra_env is excluded from directory selection. The trusted lifecycle paths returned by the helper are then applied after caller env merging, overriding caller PWTEST values in the generated-session child environment. Non-generated operator sessions skip gateway lifecycle preparation entirely and retain their explicit environment.

A behavioral test constructs AcpClient with caller-controlled PWTEST roots and asserts neither reaches the lifecycle helper, while the generated session name does. The shared source-wiring test pins the trusted merge in both AcpClient and AcpRuntime. Both paths remain under _to_thread_guarding_sandbox for cancellation cleanup.

Focused independent re-review: blocker CLOSED, no new blocker found.

@Tiger-0512
Tiger-0512 marked this pull request as ready for review August 27, 2026 13:49
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 27, 2026
@kyleseaman
kyleseaman merged commit 353b52b into kirodotdev:main Aug 27, 2026
79 of 80 checks passed
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants