fix: contain attended login diagnostics and OpenBao recovery
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a137ca-5c77-7a71-b9ae-23ad9b0b5684
This commit is contained in:
tegwick 2026-10-15 04:25:05 +02:00
parent 1baeb0821c
commit d2cd3fa940
5 changed files with 185 additions and 6 deletions

View file

@ -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."
}
}

View file

@ -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",
)

View file

@ -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)

View file

@ -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`.

View file

@ -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`.