Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/doctoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ The #144 ordinary parent integration preserves the production runner and process

The prior informational review limits remain explicit: descendants are bound to start times read after lineage sampling, so a reused PID in that gap may conservatively fail a trial; root and process-set waits have separate bounded budgets, with one shared deadline only inside the set waiter; and the pre-shutdown exit-count bound is a defensive evidence invariant. None of these controlled local contracts attests cgroup ownership, later processes, OS-wide orphan absence or exact-head pinned-Chromium acceptance. The inherited failure-path regression keeps observer errors unproven and task failures unsuccessful even when temporary-profile cleanup succeeds.

### Late-failure and forced-close evidence limits

The #145 ordinary integration adopts #144 `3c0c5c363c2da0b4e8a621226e2f7766c735b743` without changing the production runner or either child-owned process-set/forced-close regression. Its predecessor `a1341e9c8595c38446d64aaaadeab999062eab57` collected three inherited failure-path contracts rather than the parent's five. The inherited regression now runs alongside the child's separate late-failure contract; no new runtime abstraction or timeout is needed.

Ten controlled probes of the actual browser-pass and outer-trial flows cover observed root/set exit, a surviving root, a surviving sampled set, a process-observation error, and interruption before a complete result in each lane. Session deletion and driver shutdown precede the applicable exit observations. Late ordinary failure retains observed true/false root and captured-set outcomes without becoming a pass; incomplete capture omits set evidence. Forced-close success requires both observations to be true. Successful cleanup commands do not establish that sampled processes are already dead: a surviving identity fails the trial. Forced-close interruption skips post-finally observers and leaves termination unproven, while observation errors produce only the bounded fallback error type. Private exception messages remain absent. These injected probes establish control-flow contracts, not live Linux/Chromium termination, and exact-head hosted compatibility is still required.

## References

Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retrieved August 6, 2026, from https://docs.aws.amazon.com/eks/latest/userguide/pod-id-agent-setup.html
Expand Down
86 changes: 79 additions & 7 deletions scripts/ci/run_mv3_compatibility.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -1281,20 +1281,27 @@ def _run_agent_task_browser_pass(
browser_process_id,
browser_process_start_time_ticks,
)
chromium_process_set_terminated: bool | None = None
if chromium_process_identities is not None:
chromium_process_set_terminated = _wait_for_linux_process_identity_set_exit(
chromium_process_identities
)
if browser_failure_type is not None:
return {
failure_evidence: dict[str, Any] = {
"failure_type": browser_failure_type,
"browser_process_terminated": browser_process_terminated,
}
if chromium_process_set_terminated is not None:
failure_evidence["chromium_process_set_terminated"] = (
chromium_process_set_terminated
)
return failure_evidence
if result is None:
raise RuntimeError("Agent Task browser pass returned no result after shutdown")
if chromium_process_identities is None:
if chromium_process_set_terminated is None:
raise RuntimeError("Agent Task Chromium process identities were not captured")
Comment thread
seonghobae marked this conversation as resolved.
if chromium_process_pre_shutdown_exit_count is None:
raise RuntimeError("Agent Task Chromium pre-shutdown exit count was not captured")
chromium_process_set_terminated = _wait_for_linux_process_identity_set_exit(
chromium_process_identities
)
if not browser_process_terminated:
raise RuntimeError("Agent Task browser process did not terminate")
if not chromium_process_set_terminated:
Expand Down Expand Up @@ -1357,14 +1364,24 @@ def _run_agent_task_trial(
browser_process_terminated = result.get("browser_process_terminated")
if not isinstance(browser_process_terminated, bool):
raise RuntimeError("Agent Task browser pass returned invalid teardown evidence")
return {
failure_evidence: dict[str, Any] = {
"trial_number": trial_number,
"passed": False,
"failure_type": returned_failure_type,
"browser_process_terminated": browser_process_terminated,
"profile_cleaned": True,
"duration_ms": duration_ms,
}
if "chromium_process_set_terminated" in result:
chromium_process_set_terminated = result["chromium_process_set_terminated"]
if not isinstance(chromium_process_set_terminated, bool):
raise RuntimeError(
"Agent Task browser pass returned invalid process-set teardown evidence"
)
failure_evidence["chromium_process_set_terminated"] = (
chromium_process_set_terminated
)
return failure_evidence

return {
"trial_number": trial_number,
Expand Down Expand Up @@ -1462,6 +1479,10 @@ def _run_agent_task_forced_close_browser_pass(

driver_port = _free_loopback_port()
session_id: str | None = None
browser_process_id: int | None = None
browser_process_start_time_ticks: int | None = None
chromium_process_identities: tuple[tuple[int, int], ...] | None = None
result: dict[str, Any] | None = None
driver = subprocess.Popen(
[str(chromedriver_bin), f"--port={driver_port}", "--allowed-ips=127.0.0.1"],
stdout=subprocess.DEVNULL,
Expand Down Expand Up @@ -1506,11 +1527,22 @@ def _run_agent_task_forced_close_browser_pass(
raise RuntimeError("ChromeDriver forced-close capabilities are malformed")
session_id = _path_token(raw_session_id, "session identifier")
browser_version = capabilities.get("browserVersion")
browser_process_id = capabilities.get("goog:processID")
if browser_version != PINNED_CHROME_VERSION:
raise RuntimeError(
f"unexpected forced-close Chrome version: expected {PINNED_CHROME_VERSION}, "
f"got {browser_version!r}"
)
if (
isinstance(browser_process_id, bool)
or not isinstance(browser_process_id, int)
or browser_process_id <= 0
):
raise RuntimeError("ChromeDriver did not return a valid forced-close browser process id")
browser_process_identity = _read_linux_proc_stat_process_identity(browser_process_id)
if browser_process_identity is None:
raise RuntimeError("Agent Task forced-close browser process identity disappeared")
browser_process_start_time_ticks = browser_process_identity[1]

survivor_context = _json_request(
driver_port,
Expand Down Expand Up @@ -1556,6 +1588,21 @@ def _run_agent_task_forced_close_browser_pass(
if loaded_url != fixture_url:
raise RuntimeError("Agent Task forced-close probe did not load its fixture URL")

process_evidence = _snapshot_linux_process_evidence()
chromium_process_ids = _discover_linux_process_tree_ids(
browser_process_id,
process_evidence,
)
chromium_process_identities, _pre_shutdown_exit_count = (
_read_linux_process_identity_set(
chromium_process_ids,
required_root_identity=(
browser_process_id,
browser_process_start_time_ticks,
),
)
)

forced_close_detected = _force_close_agent_task_context(driver_port, session_id)
if not forced_close_detected:
raise RuntimeError("Agent Task forced-close probe did not detect the close")
Expand All @@ -1574,7 +1621,7 @@ def _run_agent_task_forced_close_browser_pass(
if not isinstance(surviving_url, str):
raise RuntimeError("Agent Task survivor context was not usable after forced close")

return {
result = {
"browser_version": browser_version,
"forced_close_detected": forced_close_detected,
"session_survived": True,
Expand All @@ -1595,6 +1642,27 @@ def _run_agent_task_forced_close_browser_pass(
driver.kill()
driver.wait(timeout=5)

if browser_process_id is None or browser_process_start_time_ticks is None:
raise RuntimeError("Agent Task forced-close browser process identity was not captured")
if chromium_process_identities is None:
raise RuntimeError("Agent Task forced-close Chromium process identities were not captured")
browser_process_terminated = _wait_for_linux_process_identity_exit(
browser_process_id,
browser_process_start_time_ticks,
)
chromium_process_set_terminated = _wait_for_linux_process_identity_set_exit(
chromium_process_identities
)
if result is None:
raise RuntimeError("Agent Task forced-close browser pass returned no result after shutdown")
if not browser_process_terminated:
raise RuntimeError("Agent Task forced-close browser process did not terminate")
if not chromium_process_set_terminated:
raise RuntimeError("Agent Task forced-close Chromium process set did not terminate")
result["browser_process_terminated"] = True
result["chromium_process_set_terminated"] = True
return result
Comment thread
seonghobae marked this conversation as resolved.


def _run_agent_task_forced_close_trial(
chrome_bin: pathlib.Path,
Expand Down Expand Up @@ -1651,6 +1719,8 @@ def _run_agent_task_forced_close_trial(
"browser_version": result["browser_version"],
"forced_close_detected": result["forced_close_detected"],
"session_survived": result["session_survived"],
"browser_process_terminated": result["browser_process_terminated"],
"chromium_process_set_terminated": result["chromium_process_set_terminated"],
"profile_cleaned": True,
"duration_ms": duration_ms,
}
Expand Down Expand Up @@ -1856,6 +1926,8 @@ def main() -> int:
forced_close_surfaces_complete = all(
trial.get("forced_close_detected") is True
and trial.get("session_survived") is True
and trial.get("browser_process_terminated") is True
and trial.get("chromium_process_set_terminated") is True
and trial.get("profile_cleaned") is True
for trial in forced_close_trials
if trial.get("passed") is True
Expand Down
94 changes: 94 additions & 0 deletions tests/test_agent_task_failure_process_set_termination_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Contract for Chromium process-set termination evidence after Agent Task failure."""

from __future__ import annotations

import pathlib
import runpy
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py"


class AgentTaskFailureProcessSetTerminationContractTests(unittest.TestCase):
"""Retain sampled descendant teardown evidence when controlled browser work fails."""

def _namespace(self, name: str) -> dict[str, object]:
return runpy.run_path(str(RUNNER), run_name=name)

def test_browser_pass_retains_sampled_process_set_teardown_after_failure(self) -> None:
"""Late failure must not discard identities already captured before shutdown."""

runner = RUNNER.read_text(encoding="utf-8")
start = runner.index("def _run_agent_task_browser_pass(")
end = runner.index("\ndef _run_agent_task_trial(", start)
browser_pass = runner[start:end]
for expected in (
"if chromium_process_identities is not None:",
"chromium_process_set_terminated = _wait_for_linux_process_identity_set_exit(",
'failure_evidence["chromium_process_set_terminated"]',
):
with self.subTest(expected=expected):
self.assertIn(expected, browser_pass)

def test_trial_preserves_failed_process_set_teardown_evidence(self) -> None:
"""Profile cleanup must preserve both root and sampled-set termination outcomes."""

namespace = self._namespace("agent_task_failure_process_set_termination_trial")
run_trial = namespace["_run_agent_task_trial"]

def fail_after_sampled_set_shutdown(
*_args: object, **_kwargs: object
) -> dict[str, object]:
return {
"failure_type": "RuntimeError",
"browser_process_terminated": True,
"chromium_process_set_terminated": False,
}

run_trial.__globals__["_run_agent_task_browser_pass"] = (
fail_after_sampled_set_shutdown
)
result = run_trial(
pathlib.Path("controlled-chrome"),
pathlib.Path("controlled-chromedriver"),
"http://127.0.0.1/controlled-fixture",
13,
)

self.assertEqual(result["trial_number"], 13)
self.assertIs(result["passed"], False)
self.assertEqual(result["failure_type"], "RuntimeError")
self.assertIs(result["browser_process_terminated"], True)
self.assertIs(result["chromium_process_set_terminated"], False)
self.assertIs(result["profile_cleaned"], True)

def test_failure_before_process_set_capture_does_not_invent_set_evidence(self) -> None:
"""A failure without sampled identities must remain explicit rather than fabricated."""

namespace = self._namespace("agent_task_failure_before_process_set_capture")
run_trial = namespace["_run_agent_task_trial"]

def fail_before_process_set_capture(
*_args: object, **_kwargs: object
) -> dict[str, object]:
return {
"failure_type": "RuntimeError",
"browser_process_terminated": True,
}

run_trial.__globals__["_run_agent_task_browser_pass"] = fail_before_process_set_capture
result = run_trial(
pathlib.Path("controlled-chrome"),
pathlib.Path("controlled-chromedriver"),
"http://127.0.0.1/controlled-fixture",
14,
)

self.assertIs(result["passed"], False)
self.assertNotIn("chromium_process_set_terminated", result)
self.assertIs(result["profile_cleaned"], True)


if __name__ == "__main__":
unittest.main()
84 changes: 84 additions & 0 deletions tests/test_agent_task_forced_close_process_termination_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Contract for post-shutdown process termination in the Agent Task forced-close lane."""

from __future__ import annotations

import pathlib
import runpy
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py"


class AgentTaskForcedCloseProcessTerminationContractTests(unittest.TestCase):
"""Require interruption evidence to include bounded Chromium teardown proof."""

def test_forced_close_browser_pass_binds_and_waits_for_process_identities(self) -> None:
"""The forced-close pass must prove its sampled browser process set terminates."""

runner = RUNNER.read_text(encoding="utf-8")
start = runner.index("def _run_agent_task_forced_close_browser_pass(")
end = runner.index("\ndef _run_agent_task_forced_close_trial(", start)
browser_pass = runner[start:end]
for expected in (
'capabilities.get("goog:processID")',
"_read_linux_proc_stat_process_identity",
"_snapshot_linux_process_evidence",
"_read_linux_process_identity_set",
"_wait_for_linux_process_identity_exit",
"_wait_for_linux_process_identity_set_exit",
'"browser_process_terminated"',
'"chromium_process_set_terminated"',
):
with self.subTest(expected=expected):
self.assertIn(expected, browser_pass)

def test_forced_close_trial_preserves_false_teardown_evidence(self) -> None:
"""A failed teardown proof must not be omitted or normalized into success."""

namespace = runpy.run_path(
str(RUNNER), run_name="forced_close_process_termination_trial"
)
trial = namespace["_run_agent_task_forced_close_trial"]

def fake_browser_pass(
_chrome_bin: pathlib.Path,
_chromedriver_bin: pathlib.Path,
_fixture_url: str,
_profile_dir: str,
) -> dict[str, object]:
return {
"browser_version": namespace["PINNED_CHROME_VERSION"],
"forced_close_detected": True,
"session_survived": True,
"browser_process_terminated": False,
"chromium_process_set_terminated": False,
}

trial.__globals__["_run_agent_task_forced_close_browser_pass"] = fake_browser_pass
result = trial(
pathlib.Path("/unused/chrome"),
pathlib.Path("/unused/chromedriver"),
"http://127.0.0.1/fixture",
1,
)
self.assertIs(result["browser_process_terminated"], False)
self.assertIs(result["chromium_process_set_terminated"], False)

def test_main_forced_close_gate_requires_process_termination(self) -> None:
"""Compatibility success must reject a live forced-close browser identity."""

runner = RUNNER.read_text(encoding="utf-8")
start = runner.index("forced_close_surfaces_complete = all(")
end = runner.index("\n\n evidence = {", start)
gate = runner[start:end]
for expected in (
'trial.get("browser_process_terminated") is True',
'trial.get("chromium_process_set_terminated") is True',
):
with self.subTest(expected=expected):
self.assertIn(expected, gate)


if __name__ == "__main__":
unittest.main()
Loading