From d2cd3fa9405f0766eef67a8b0e897239d8817fb7 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 15 Oct 2026 04:25:05 +0200 Subject: [PATCH] fix: contain attended login diagnostics and OpenBao recovery Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a137ca-5c77-7a71-b9ae-23ad9b0b5684 --- ...-cust81-attended-dependency-repair-r1.json | 78 +++++++++++++++++++ src/warden/proxy.py | 24 +++++- tests/test_proxy.py | 50 +++++++++++- .../playbooks/openbao-platform-admin-login.md | 19 +++++ ...N-WP-0036-attended-login-openbao-output.md | 20 ++++- 5 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json diff --git a/docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json b/docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json new file mode 100644 index 0000000..aa49ca2 --- /dev/null +++ b/docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json @@ -0,0 +1,78 @@ +{ + "id": "WARDEN-CUST81-ATTENDED-DEPENDENCY-REPAIR-R1", + "revision": 1, + "status": "source_preparation_verified_not_live", + "reviewed_at": "2026-10-15T02:18:02.246091+00:00", + "base_commit": "1baeb0821cb6304464fab78932d8c6501f8c4cc6", + "scope": "Existing WARDEN-WP-0036 contained attended login; CUST-WP-0081-T02 dependency. No credential scope or live change approval expansion.", + "failed_attempts": [ + { + "at": "2026-10-15T02:07:41.774194+00:00", + "exit_code": 10, + "outcome": "login_failed" + }, + { + "at": "2026-10-15T02:10:58.943441+00:00", + "exit_code": 10, + "outcome": "login_failed" + } + ], + "browser_evidence": { + "profile_count": 2, + "initialization_mtimes_utc": [ + "2026-10-15T02:08:19.576345+00:00", + "2026-10-15T02:09:28.109549+00:00" + ], + "prefs_file_presence_only": true, + "profile_contents_read": false + }, + "cleanup_evidence": { + "custody_receipt_exists": false, + "bootstrap_receipt_exists": false, + "new_session_directory_present": false, + "preexisting_session_directory_mtime": "2026-10-14T22:41:38.098938+00:00", + "private_helper_read": false, + "server_revocation": "unconfirmed" + }, + "findings": [ + "The reviewed owner child never ran: Warden exit10 and absent child receipts.", + "Actual failure cause is unknown because captured login bytes were discarded. A 121-second duration is consistent with the Bao callback timeout, not proof.", + "Bao 2.5.5 internal helper uses .vault-token; Warden helper name is correct.", + "Standard flag parsing stops at auth data, but Bao setupEnv scans global format across argv. Trailing -format=json already chooses JSON output. Moving before role is defensive argv normalization, not a proven timeout fix.", + "Warden recovery matcher omitted s. tokens supported by this estate. This fix affects only existing in-memory emergency self-revocation after helper persistence failure." + ], + "changes": [ + "Insert synthesized -format=json immediately after bao login, before auth data.", + "Recognize s. in existing recovery-token matcher; preserve exact revoke-self-only use.", + "Expose only fixed diagnostic categories, numeric returncode, helper-present and output-shape booleans. Captured content is never surfaced." + ], + "strict_output_allowlist_unchanged": true, + "focused_tests": { + "passed": 84, + "command": "python -m pytest -q tests/test_proxy.py", + "ruff": "passed" + }, + "preserved_fixture_failures": [ + "Initial task fixture omitted registry:50failed31passed; fixed by copying public registry.", + "Whole-suite attempt omitted scripts:5 collection errors; actual owner full suite required after integration." + ], + "knowledge_orientation": { + "package_version": "0.2.6", + "digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02", + "status": "withheld; reviewed current NetKingdom SECURITY-COMPANION, gate-house INTENT, ops-warden AGENTS/INTENT and current Custodian environment source directly" + }, + "source_references": [ + "https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/main.go", + "https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/base.go", + "https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/login.go", + "https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/token/helper_internal.go" + ], + "no_login_or_credentials": true, + "whole_suite": { + "status": "unverified_terminal_output_unavailable", + "handle": 39705, + "last_observed": "54 percent with six failures; no terminal result consumed by owner worker", + "terminal_handle_after_user_abort": "root poll reported Unknown process id", + "claim": "No full-suite pass is claimed; 84 focused proxy tests and Ruff passed." + } +} diff --git a/src/warden/proxy.py b/src/warden/proxy.py index 1747487..b4eca41 100644 --- a/src/warden/proxy.py +++ b/src/warden/proxy.py @@ -41,7 +41,7 @@ from typing import List, Optional from warden.routing.models import RouteEntry _PLACEHOLDER = re.compile(r"<[^>]+>") -_OPENBAO_TOKEN = re.compile(rb"\b(?:hvs|hvb|hvr)\.[A-Za-z0-9_-]{8,}\b") +_OPENBAO_TOKEN = re.compile(rb"\b(?:hvs|hvb|hvr|s)\.[A-Za-z0-9_-]{8,}\b") _ATTENDED_LOGIN_ROOT = ".warden-attended-login" _TOKEN_HELPER_NAME = ".vault-token" @@ -544,7 +544,7 @@ def proxy_attended_login_exec( login_argv = list(resolved.argv) if not any(arg == "-format" or arg.startswith("-format=") for arg in login_argv): - login_argv.append("-format=json") + login_argv.insert(2, "-format=json") try: try: @@ -569,9 +569,27 @@ def proxy_attended_login_exec( "" if output_allowed else " (login client produced non-allowlisted output)" ) + # Fixed categories and booleans only; captured bytes stay discarded. + if login.returncode != 0: + if b"timed out waiting for response from provider" in login_output: + category = "callback_timeout" + elif b"Error handling callback" in login_output: + category = "callback_error" + elif b"Error authenticating:" in login_output: + category = "authentication_error" + else: + category = "unknown_client_failure" + elif not helper_valid: + category = "helper_persistence_missing" + else: + category = "output_shape_rejected" raise AttendedLoginError( "attended login failed closed before command handoff" - f"{reason}; any possible issued session was contained and {status}", + f"{reason}; category={category}, client_exit={login.returncode}, " + f"helper_present={helper_valid}, " + f"stdout_allowed={_login_stdout_allowed(_stream_bytes(login.stdout))}, " + f"stderr_allowed={_login_stderr_allowed(_stream_bytes(login.stderr))}; " + f"any possible issued session was contained and {status}", exit_code=10, outcome="login_failed", ) diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 35cbcee..3078bbf 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -446,7 +446,7 @@ def test_attended_login_persistence_failure_revokes_and_cleans(monkeypatch, tmp_ child_argv=["should-not-run"], ) assert calls == [ - ["bao", "login", "-no-print", "-format=json"], + ["bao", "login", "-format=json", "-no-print"], ["bao", "token", "revoke", "-self"], ] assert not (tmp_path / ".warden-attended-login").exists() @@ -716,7 +716,7 @@ def test_attended_login_non_allowlisted_output_direct_raises_value_free( assert (exc.value.exit_code, exc.value.outcome) == (10, "login_failed") assert _LOGIN_SENTINEL not in str(exc.value) assert calls == [ - ["bao", "login", "-no-print", "-format=json"], + ["bao", "login", "-format=json", "-no-print"], ["bao", "token", "revoke", "-self"], ] @@ -948,3 +948,49 @@ def test_access_agent_high_risk_fingerprint_allowed(tmp_path, monkeypatch): ) assert r.exit_code == 0, r.output assert "should-not-appear" not in r.output + + +def test_attended_json_format_precedes_auth_data(monkeypatch, tmp_path): + monkeypatch.setattr(Path, "home", lambda: tmp_path) + calls = [] + def run(argv, **kwargs): + calls.append(argv) + return subprocess.CompletedProcess(argv, 1, b"", b"") + monkeypatch.setattr("warden.proxy.subprocess.run", run) + with pytest.raises(AttendedLoginError): + proxy_attended_login_exec( + ResolvedFetch(argv=["bao", "login", "-no-print", "-method=oidc", "role=platform-admin"]), + child_argv=["must-not-run"], + ) + assert calls[0].index("-format=json") < calls[0].index("role=platform-admin") + + +@pytest.mark.parametrize("shape", ["s", "hvs", "hvb", "hvr"]) +def test_contained_recovery_supports_openbao_token_shapes(monkeypatch, shape): + from warden.proxy import _revoke_contained + calls = [] + sentinel = shape + ".FAKE_RECOVERY_SENTINEL_12345678" + def run(argv, **kwargs): + calls.append((argv, dict(kwargs["env"]))) + return subprocess.CompletedProcess(argv, 1 if len(calls) == 1 else 0, b"", b"") + monkeypatch.setattr("warden.proxy.subprocess.run", run) + assert _revoke_contained("bao", env={}, possible_output=sentinel.encode()) + assert calls[1][1]["BAO_TOKEN"] == sentinel + assert all(sentinel not in " ".join(argv) for argv, _ in calls) + + +@pytest.mark.parametrize("prefix,category", [ + ("timed out waiting for response from provider", "callback_timeout"), + ("Error handling callback", "callback_error"), + ("Error authenticating:", "authentication_error"), + ("unrecognized backend error", "unknown_client_failure"), +]) +def test_attended_failure_is_fixed_category_without_client_bytes(monkeypatch, tmp_path, prefix, category): + monkeypatch.setattr(Path, "home", lambda: tmp_path) + sentinel = "SECRET_CLIENT_OUTPUT_SENTINEL" + def run(argv, **kwargs): + return subprocess.CompletedProcess(argv, 1, b"", (prefix + " " + sentinel).encode()) + monkeypatch.setattr("warden.proxy.subprocess.run", run) + with pytest.raises(AttendedLoginError, match="category=" + category) as exc: + proxy_attended_login_exec(ResolvedFetch(argv=["bao", "login", "-no-print"]), child_argv=["must-not-run"]) + assert sentinel not in str(exc.value) diff --git a/wiki/playbooks/openbao-platform-admin-login.md b/wiki/playbooks/openbao-platform-admin-login.md index fbe547b..0cd809d 100644 --- a/wiki/playbooks/openbao-platform-admin-login.md +++ b/wiki/playbooks/openbao-platform-admin-login.md @@ -89,3 +89,22 @@ messages; it should write only approved, value-free evidence to its own receipt. - Human identity and MFA provider: key-cape / Keycloak - Database-engine procedure owner: `rapp-postgres` - Routing decision and founder-act surface: WARDEN-WP-0029 + +## 2026-10-15 contained failure diagnostics + +A failed login now reports only fixed categories (`callback_timeout`, +`callback_error`, `authentication_error`, `unknown_client_failure`, +`helper_persistence_missing`, `output_shape_rejected`), the numeric client exit +code, helper-present and allowed-output booleans. These are observations, not +authorization or proof of an issued session. The original captured bytes remain +discarded, and the existing strict output allowlist is unchanged. + +Synthesized JSON-format flags precede positional auth data. OpenBao 2.5.5 also +scans global format flags across argv; this normalization is not a proven fix +for the October 15 timeout. Emergency self-revocation recognizes the estate's +`s.` service-token shape as well as `hvs.`, `hvb.` and `hvr.`. Recovery still uses +a value only in memory for `bao token revoke -self`, never a new handoff. + +The two CUST81 failed attempts remain failed and revocation-unconfirmed. Fresh +Firefox profiles prove browser initialization, not callback completion. Source +receipt: `docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json`. diff --git a/workplans/WARDEN-WP-0036-attended-login-openbao-output.md b/workplans/WARDEN-WP-0036-attended-login-openbao-output.md index 84b1440..452eb41 100644 --- a/workplans/WARDEN-WP-0036-attended-login-openbao-output.md +++ b/workplans/WARDEN-WP-0036-attended-login-openbao-output.md @@ -8,7 +8,7 @@ status: finished owner: codex topic_slug: attended-login-openbao-output created: "2026-09-01" -updated: "2026-10-14" +updated: "2026-10-15" state_hub_workstream_id: "d844c96e-152d-53fa-bff6-e072125ef66c" --- @@ -111,3 +111,21 @@ Tightened the same day after an independent re-review (containment gap): stdout the stderr OIDC prompt must start at the first byte (no leading whitespace; trailing newlines remain fine). The fail-closed and hand-off tests now run through the CLI and assert sentinel absence from both `access-audit.log` and `audit.jsonl`. + +### 2026-10-15 CUST81 dependency correction (existing T01/T02) + +Prepared source correction after the two CUST81 attended platform-admin attempts +failed before child handoff. The strict login-output allowlist remains unchanged. +Synthesized format flags now precede auth data; the recovery-only matcher handles +OpenBao `s.` tokens; failure messages expose only fixed categories, client exit +code and helper/output-shape booleans. No credential bytes or URLs are surfaced. + +Do not attribute the observed 121-second failure to the argv order: Bao 2.5.5 +scans global format throughout argv. Actual callback/auth failure remains unknown. +Browser profile initialization was verified from metadata only; server revocation +remains unconfirmed and the failed attempts are retained. No login, admin token +read, policy application or provisioning was performed by this correction. + +Source verification is distinct from live verification. The already-approved +CUST81 execution scope and its open attended gate remain with CUST-WP-0081-T02. +Owner evidence: `docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json`.