diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cc6d58d3..c0b130fca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ## [Unreleased] +- Integrated the sandbox-preserving browser-crash parent into ordinary shared-deadline teardown; all four controlled browser lanes retain the sandbox requirement and the existing cleanup-evidence checks. Real pinned-browser startup remains unaccepted until the workflow prerequisite is repaired and verified. +- Integrated the current cleanup-evidence prerequisites into ordinary shared-deadline teardown and kept inherited failure tests on the actual observation path; incomplete process sampling still cannot become complete cleanup evidence. - Browser-crash trials no longer disable Chromium's sandbox. Rejected session startup remains one failed attempt with driver and temporary-profile cleanup, without an unsandboxed retry; live pinned-browser acceptance is still required. - Browser-crash failure evidence now preserves the first causal browser failure when session cleanup or ChromeDriver teardown also fails, retains secondary cleanup only as bounded exception-type fields, and excludes raw exception text from the emitted artifact. Cleanup-only failures remain fail-closed as primary failures. @@ -13,6 +15,8 @@ All notable changes to OriginWeave are documented in this file. The format follo - Controlled browser trials reject malformed pre-shutdown exit counts, retain validated driver and cleanup outcomes after ordinary task failure, and report mid-request protocol failures without remote diagnostic text; failed trials remain failures, startup retries keep their existing narrow scope, and obsolete HTTP-body close signals are no longer accepted. - Failed controlled Agent Task runs now report whether their original browser process ended after shutdown, alongside temporary-profile cleanup; a failed task never becomes a pass merely because cleanup succeeded. If process observation itself fails, termination remains unproven. This covers the original browser process only, not all descendants or arbitrary browser recovery. - Controlled pinned-Chromium Agent Task success now binds the ChromeDriver browser root to its exact Linux `/proc//stat` start-time identity, binds every still-live PID from the already sampled bounded Chromium root-plus-descendant set before shutdown, explicitly records descendants that already exited between the `/proc` lineage snapshot and identity capture, and fails closed unless every retained exact identity terminates after session/driver shutdown; root disappearance or identity change remains an error, PID reuse counts only as termination of the original identity, and this does not attest cgroup/task ownership, processes appearing only after the sample, or OS-wide orphan absence. +- The controlled forced-close Agent Task recovery probe now binds the ChromeDriver browser root and its already sampled bounded Chromium descendant set to exact Linux PID/start-time identities before forcing the disposable context closed, and successful recovery is accepted only after session/driver shutdown proves root and sampled-set termination under one shared bounded monotonic deadline; this remains bounded compatibility evidence and does not attest cgroup ownership, post-snapshot processes, cross-platform supervision, or OS-wide orphan absence. +- Controlled ordinary Agent Task teardown now observes the exact browser root and already sampled Chromium process set under one shared bounded monotonic deadline after session/driver shutdown, preserving separate root/process-set termination evidence and retaining `None` when no full process set was captured; this prevents sequential waiters from extending a nominal teardown budget while making no claim about cgroup ownership, post-snapshot processes, cross-platform supervision, or OS-wide orphan absence. - Failed ordinary and forced-close Agent Task browser trials now retain credential-free temporary-profile cleanup evidence after bounded browser errors, and separate aggregate compatibility gates require cleanup proof from every trial rather than filtering unsuccessful trials out; this does not attest adversarial filesystem erasure, process termination, or arbitrary browser recovery. - Failed Manifest V3 restart trials now retain credential-free temporary-profile cleanup evidence after bounded browser errors, including reviewed ChromeDriver process-teardown `TimeoutExpired` failures; successful trials record the same cleanup fact, and an aggregate compatibility gate requires teardown proof from every MV3 trial before repeatability acceptance without retaining exception messages or command paths; this does not attest adversarial filesystem erasure, browser-process termination, or cleanup outside the controlled temporary profile. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. diff --git a/docs/doctoring.md b/docs/doctoring.md index eedc9795c..097d30f58 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -124,6 +124,12 @@ At #147 `af1b98ba377c73b88baa9633e2232e7f76e76f36`, all six parent review-eviden The existing test now specifies each lane's actual observer, return value and exact arguments. Ordinary trials keep the individual `False` result. Forced-close trials inject `(False, False)` from the shared observer and require `(321, 654, ((321, 654),))`, preserving root-only capture rather than inventing a complete descendant set. Every existing failed-trial, profile-cleanup, live-root, driver-termination, error-type and redaction assertion remains. No production code, deadline, retry, failed denominator or coverage gate changes. This repair does not address the separate ordinary-pass two-deadline finding or prove real Linux/pinned-Chromium acceptance. +### Ordinary shared-deadline parent integration + +PR #150 ordinarily adopts #147 `3dff28d9bf2dd27b72507e39979d51b8bf140fb4` while retaining its existing ordinary shared-deadline production delta and two child-owned tests from `c208668ce3aa079059acc1016646ed7240797ac0`. Before integration, discovery collected none of the six current-parent review-evidence methods. After their restoration, two protocol-fault subcases failed because the inherited ordinary-lane test still injected the individual root waiter; the full 229-test suite found three more cases with the same stale injection in the failure-process test. + +Both existing tests now target the actual shared observer, return paired root/set outcomes and require the exact root-only identity tuple when capture fails early. Every failure, cleanup, error-type and diagnostic-redaction assertion remains. An additional assertion forbids inventing complete sampled-set evidence from root-only capture. Production deadlines, polling, success predicates, retry rules and failed-trial denominators are unchanged by this reconciliation. The parent's four deterministic shared-observer tests and the child's ordinary call-order contract remain independently executable. Passing injected observations does not prove live Chromium teardown, cgroup ownership or OS-wide orphan absence; the #147 ordinary-wait finding remains unresolved on that parent until the #150 delta is actually integrated there. + ### Browser-crash sandbox conformance PR #148 adopts the current #147 parent `3dff28d9bf2dd27b72507e39979d51b8bf140fb4` by ordinary merge, retaining the crash/pidfd delta and all six inherited cleanup-evidence regressions. A new regression executes the actual outer crash trial and intercepts its session-start request: before the repair it fails because that request disables the Chromium sandbox. Removing that one crash-lane argument restores the existing sandbox invariant without introducing a configuration switch or fallback path. @@ -132,6 +138,14 @@ The same regression requires one failed startup attempt, driver reaping and actu Supplemental verification used the existing Colima Linux kernel `6.8.0-117-generic` and Python `3.12.3`, without installing dependencies or changing VM configuration. All 247 Python contracts execute there with no skips, including the three real pidfd cases skipped on macOS: killed-but-unreaped child, non-terminating signal and stale identity. Initial host-path discovery failed because this VM does not expose the host worktree. The first streamed archive then added AppleDouble `._*.rs` files, causing two TLS source-read errors. A fresh export of only Git-tracked paths with `COPYFILE_DISABLE=1 tar --no-xattrs --no-acls --no-fflags` removes that packaging artifact at its producer; no test filter or source exception was added. Source and regression-file SHA-256 hashes match across hosts. This proves the supplemental Linux contracts, not pinned-Chromium execution, hosted approval or release acceptance. +### Ordinary shared-deadline adoption of the sandbox-preserving parent + +On September 6, 2026, #150 predecessor `d3c29359fd10e540ca9b3b2723bde6f94866cdb6` replayed the unchanged four-lane sandbox contract from #148 `0135984f1bc1f68d89d7777f49c4999474105a12`. It failed on three sandbox-disabled launch paths and the absent browser-crash lane. RED commit `f0b2ab90` records that test before the ordinary parent merge. The merge retains every parent production change and adds only #150's existing ordinary shared-deadline production delta; its original child regressions and the reconciled inherited failure injections remain intact. Root-only observation still cannot establish complete sampled-process cleanup. + +This integration supersedes the earlier crash-only scope note above: all four launch lanes now omit the sandbox-disabling argument. It does not supersede the parent's runtime failure. Exact-parent MV3 run `33990522248`, job `101371812631`, artifact `9977680352` (SHA-256 `5430103b6bae59acc0d62d3c21cce559b27a15eadc6f049a857551d9a92ef643`) reports zero successful trials out of three in each of the ordinary MV3, ordinary Agent Task, forced-close and browser-crash lanes. Crash trials fail during session creation; temporary-profile cleanup does not establish successful browser startup. Issue #212 owns the canonical sandbox-helper workflow prerequisite. No workflow, browser pin, retry, deadline, acceptance predicate or denominator is changed here, and #150 remains Draft. Its predecessor's successful unsandboxed run cannot be transferred to this source tree. The #147 ordinary-deadline review finding remains open until this successor delta is actually integrated there. + +The combined source passes all 253 Python contracts on existing Colima Linux `6.8.0-117-generic` / Python `3.12.3`, with no skips or dependency installation. macOS discovery runs the same 253 contracts and skips exactly the three Linux-only pidfd cases, which are covered by that Linux execution rather than counted as macOS passes. Full Rust `1.97.1` formatting, check, workspace tests, strict Clippy and rustdoc pass. Pinned `nightly-2026-08-01` measures 415/415 functions, 3555/3555 lines, 4444/4444 regions and 476/476 branches; the verifier accepts all four metrics at 100%. The separate `warning: --branch option is unstable` is retained. These are local source and contract results, not a replacement for fresh hosted checks or real pinned-browser acceptance. + ## 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 diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index b9b9db81b..1c6653e81 100644 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -1565,15 +1565,22 @@ def _run_agent_task_browser_pass( if browser_process_id is None or browser_process_start_time_ticks is None: raise RuntimeError("Agent Task browser process identity was not captured") - browser_process_terminated = _wait_for_linux_process_identity_exit( - browser_process_id, - browser_process_start_time_ticks, + full_process_set_captured = chromium_process_identities is not None + teardown_identities = ( + chromium_process_identities + if chromium_process_identities is not None + else ((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 + browser_process_terminated, observed_process_set_terminated = ( + _wait_for_linux_process_teardown( + browser_process_id, + browser_process_start_time_ticks, + teardown_identities, ) + ) + chromium_process_set_terminated: bool | None = ( + observed_process_set_terminated if full_process_set_captured else None + ) if ( browser_failure_type is not None or session_cleanup_failure_type is not None diff --git a/tests/test_agent_task_failure_process_set_termination_contract.py b/tests/test_agent_task_failure_process_set_termination_contract.py index 6fe48ea9d..46e055b41 100644 --- a/tests/test_agent_task_failure_process_set_termination_contract.py +++ b/tests/test_agent_task_failure_process_set_termination_contract.py @@ -24,8 +24,9 @@ def test_browser_pass_retains_sampled_process_set_teardown_after_failure(self) - 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(", + "full_process_set_captured = chromium_process_identities is not None", + "_wait_for_linux_process_teardown(", + "observed_process_set_terminated if full_process_set_captured else None", 'failure_evidence["chromium_process_set_terminated"]', ): with self.subTest(expected=expected): @@ -91,4 +92,4 @@ def fail_before_process_set_capture( if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_agent_task_failure_process_termination_contract.py b/tests/test_agent_task_failure_process_termination_contract.py index 910bf0248..83ac10daa 100644 --- a/tests/test_agent_task_failure_process_termination_contract.py +++ b/tests/test_agent_task_failure_process_termination_contract.py @@ -60,7 +60,7 @@ def request(_port, method, target, *_args): raise RuntimeError("private controlled browser failure") return {} - exit_wait = mock.Mock(return_value=exit_observation) + exit_wait = mock.Mock(return_value=(exit_observation, exit_observation)) if isinstance(exit_observation, Exception): exit_wait.side_effect = exit_observation replacements = { @@ -68,7 +68,7 @@ def request(_port, method, target, *_args): "_wait_for_driver": lambda _port: None, "_json_request": request, "_read_linux_proc_stat_process_identity": lambda _pid: (321, 654), - "_wait_for_linux_process_identity_exit": exit_wait, + "_wait_for_linux_process_teardown": exit_wait, } with mock.patch.dict(browser_pass.__globals__, replacements), mock.patch.object( namespace["subprocess"], "Popen", return_value=driver @@ -82,9 +82,10 @@ def request(_port, method, target, *_args): driver.terminate.assert_called_once_with() driver.wait.assert_called_once_with(timeout=5) - exit_wait.assert_called_once_with(321, 654) + exit_wait.assert_called_once_with(321, 654, ((321, 654),)) self.assertIs(result["passed"], False) self.assertIs(result["profile_cleaned"], True) + self.assertNotIn("chromium_process_set_terminated", result) self.assertNotIn("private controlled browser failure", repr(result)) if isinstance(exit_observation, Exception): self.assertEqual(result["failure_type"], "PermissionError") diff --git a/tests/test_agent_task_review_evidence_contract.py b/tests/test_agent_task_review_evidence_contract.py index 035e2fae8..f9d0cd63a 100644 --- a/tests/test_agent_task_review_evidence_contract.py +++ b/tests/test_agent_task_review_evidence_contract.py @@ -75,7 +75,10 @@ def test_protocol_faults_keep_real_browser_cleanup_evidence(self) -> None: """Mid-pass protocol faults stay terminal and preserve observed root cleanup.""" for lane, waiter_name, observed_exit, expected_args in ( - ("agent_task", "_wait_for_linux_process_identity_exit", False, (321, 654)), + ( + "agent_task", "_wait_for_linux_process_teardown", + (False, False), (321, 654, ((321, 654),)), + ), ( "agent_task_forced_close", "_wait_for_linux_process_teardown", (False, False), (321, 654, ((321, 654),)), diff --git a/tests/test_agent_task_shared_teardown_deadline.py b/tests/test_agent_task_shared_teardown_deadline.py new file mode 100644 index 000000000..2a2930231 --- /dev/null +++ b/tests/test_agent_task_shared_teardown_deadline.py @@ -0,0 +1,37 @@ +"""Contract for one total post-shutdown teardown deadline in the ordinary Agent Task lane.""" + +from __future__ import annotations + +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py" + + +class AgentTaskSharedTeardownDeadlineContractTests(unittest.TestCase): + """Prevent ordinary Agent Task teardown polling from multiplying the budget.""" + + def test_browser_pass_uses_only_the_combined_teardown_waiter(self) -> None: + """Root and sampled-set evidence must share one timeout authority after 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] + + self.assertIn("_wait_for_linux_process_teardown(", browser_pass) + self.assertNotIn("_wait_for_linux_process_identity_exit(", browser_pass) + self.assertNotIn("_wait_for_linux_process_identity_set_exit(", browser_pass) + + shutdown = browser_pass.index("_terminate_owned_process_bounded(") + teardown_wait = browser_pass.index("_wait_for_linux_process_teardown(") + failure_return = browser_pass.index( + "or driver_cleanup_failure_type is not None" + ) + self.assertLess(shutdown, teardown_wait) + self.assertLess(teardown_wait, failure_return) + + +if __name__ == "__main__": + unittest.main()