From 473187c1141a87c8e05ca01a4cf3daae22a81b10 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 6 Nov 2026 01:42:42 +0100 Subject: [PATCH] Fix founder publication login handoff and session reporting Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1abf1-7e95-7141-87c9-ee78332bf790 --- ...rgabe-provider-custody-failure-return.json | 30 ++++++++ .../README.md | 33 +++++++- .../plan.json | 25 +++++- tests/test_vergabe_pilot_publish.py | 65 +++++++++++++++- tests/vergabe_pilot_warden_handoff_fixture.py | 77 +++++++++++++++++++ tools/vergabe_pilot_publish.py | 59 ++++++++++++-- .../RAPPS-WP-0014-vergabe-invited-pilot.md | 23 +++++- 7 files changed, 295 insertions(+), 17 deletions(-) create mode 100644 docs/evidence/2026-11-06-vergabe-provider-custody-failure-return.json create mode 100644 tests/vergabe_pilot_warden_handoff_fixture.py diff --git a/docs/evidence/2026-11-06-vergabe-provider-custody-failure-return.json b/docs/evidence/2026-11-06-vergabe-provider-custody-failure-return.json new file mode 100644 index 0000000..bb3e21a --- /dev/null +++ b/docs/evidence/2026-11-06-vergabe-provider-custody-failure-return.json @@ -0,0 +1,30 @@ +{ + "schema": "railiance.vergabe-pilot-provider-recovery.v1", + "run_id": "20261105T233543Z-89bfbe04dece", + "status": "failed", + "provider_publication_verified": false, + "decryption_verified": false, + "restore_verified": false, + "application_changed": false, + "stage": "owner_custody", + "failure": "contained_owner_login_required", + "elapsed_seconds": 0.01, + "warden_exit_code": 11, + "receipt_file": "/home/worsch/.local/state/vergabe-pilot-recovery/20261105T233543Z-89bfbe04dece/provider-receipt-48cc9fb0018d.json", + "session_cleanup_confirmed": false, + "evidence_kind": "user_reported_terminal_output", + "reported_on": "2026-11-06", + "source_revision": "412787515c5ad662b8014347f9eb1bb3b8b8da0f", + "workplan": "RAPPS-WP-0014", + "task": "RAPPS-WP-0014-T03", + "review": { + "founder_login_succeeded": true, + "session_revoked_user_reported": true, + "provider_custody_reads_reached": false, + "provider_calls_reached": false, + "session_cleanup_false_is_publisher_reporting_defect": true, + "original_guard_did_not_identify_which_condition_failed": true, + "source_mismatch": "Publisher rejects OPENBAO_TOKEN, but the wrapped login only clears BAO_TOKEN and VAULT_TOKEN; an inherited alias reproduces this failure in the real Warden envelope with an offline fake Bao client.", + "fix": "Clear all three token aliases before the fresh owner login; retain explicit isolated-home/tunnel/policy checks and safe per-condition diagnostics. Keep session lifecycle reporting distinct from child exit status." + } +} diff --git a/docs/proposals/2026-11-06-vergabe-provider-publication/README.md b/docs/proposals/2026-11-06-vergabe-provider-publication/README.md index 4342a1a..6009edb 100644 --- a/docs/proposals/2026-11-06-vergabe-provider-publication/README.md +++ b/docs/proposals/2026-11-06-vergabe-provider-publication/README.md @@ -5,6 +5,15 @@ verifies ciphertext SHA256 `49152883…6bef76f`, 182521 bytes, for run `20261105T233543Z-89bfbe04dece`. The app remains running. The private encrypted kit is already on the founder's workstation. [Exact scope and source pins](plan.json). +First native attempt: [the founder return](../../evidence/2026-11-06-vergabe-provider-custody-failure-return.json) +reports `contained_owner_login_required` before credential reads/provider calls. +Warden logged in and reported session revocation; the publisher's old aggregate +cleanup flag was misleading. Revision 2 clears all three token aliases before +the fresh login and verifies Warden's private session-directory context. An +offline test through the installed Warden envelope reproduces the inherited +alias refusal and passes with the corrected preparation. Retry the same command +below; the previous session was revoked. Provider scope and ciphertext are unchanged. + ## Founder workstation command ```bash @@ -46,6 +55,14 @@ boundary policies and ambient token variables refuse. The OpenBao tunnel remains `http://127.0.0.1:18200`, as in the existing owner wrapper. No fallback identity or changed control is introduced. +Before launching the wrapper, the publisher clears `BAO_TOKEN`, `VAULT_TOKEN` +and `OPENBAO_TOKEN` and pins both address aliases to the tunnel. Warden's wrapper +already clears the first two; leaving the third inherited conflicted with the +publisher's own guard. The child verifies directory ownership/permissions and +the `.warden-attended-login/session-*` HOME shape, without inspecting the token +helper. Its receipt includes only boolean handoff checks. Self-lookup uses the +owner's `auth/token/lookup-self` API form and combines token/identity policies. + ## Exact provider change One object in the existing `r01-platform-pg` backup folder: @@ -84,6 +101,13 @@ the same deterministic object name. Warden output/cleanup errors remain failures even if data verification finished. Inspect only the allowed receipt, never Warden's captured output or token helper. +For failed children, exit 11 alone cannot distinguish confirmed revocation from +an unconfirmed revocation. The new receipt records `null` for that uncertainty, +with storage cleanup separately reported from Warden's documented exit contract; +13 means revocation unconfirmed and 14 means private-storage cleanup failed. +Only exit 0 confirms the entire envelope. The original failure receipt is kept +unchanged as historical evidence, alongside its review of the reporting defect. + A new `provider-recovery-*` directory inside the private workstation kit retains the retrieved ciphertext, decrypted ZIP and unpacked recovery point for the next isolated restore. The directory is 0700 and files 0600, outside Git; do not open @@ -97,10 +121,13 @@ No second application pause or recapture is needed for this point. ## Validation -Eleven synthetic checks cover real age encrypt/decrypt and complete packet/media/ +Fifteen publisher checks cover real age encrypt/decrypt and complete packet/media/ SQLite verification with mocked custody/provider; separate upload/readback credentials; identical-object retry; different-object preservation; wrong escrow, quota and tampered-download refusal; credential-safe silent failure; ambient token refusal; pinned owner helpers; foreign-origin denial and bounded readback. -Native provider publication/decryption has not executed until its founder receipt -returns. +The additional cases test all token aliases, API/effective-policy handling, +separate session outcomes and the installed Warden envelope using an offline +fake Bao client. They issue no native session and inspect no real helper or +credential. All 45 pilot helper checks pass. Native provider publication and +decryption remain unverified until a successful founder receipt returns. diff --git a/docs/proposals/2026-11-06-vergabe-provider-publication/plan.json b/docs/proposals/2026-11-06-vergabe-provider-publication/plan.json index 044f460..25773eb 100644 --- a/docs/proposals/2026-11-06-vergabe-provider-publication/plan.json +++ b/docs/proposals/2026-11-06-vergabe-provider-publication/plan.json @@ -6,7 +6,7 @@ "run_id": "20261105T233543Z-89bfbe04dece", "ciphertext_sha256": "4915288384e3ba8a87f1427b8e10106ee7241c04184adb97f439e57846bef76f", "ciphertext_bytes": 182521, - "publisher_source_sha256": "845294c574692333b35e627f5d3194e6334136f14f4f699e27a680f9cd4604db", + "publisher_source_sha256": "8815272ed09ffce91e2a1551c646285d8aea844f318fc188c986d15f4093f807", "platform_helper_pins": { "openbao-attended-exec.py": "53fb5fb38ad17c4b06d7969a4d8968d976c8ebc8f1abfeb9a191aa43dc54a5c3", "migrate_nextcloud_backup_account.py": "d3285f403f3c947f75c3288d989152e59ab9a8f5542bcf901bd0a06b0f3e050c", @@ -53,10 +53,29 @@ "recovered_data": "Private provider-recovery-* directory inside the existing workstation kit, outside Git, retained for the isolated restore rehearsal", "failure_handling": "Safe receipt with phase flags; different object bytes refuse without overwrite. Retry reads the deterministic object name. Warden exit and cleanup confirmation are recorded separately from data verification.", "validation": { - "publisher_cases": 11, + "publisher_cases": 15, "synthetic_only": true, "actual_age_round_trip": true, "provider_and_custody_mocked": true, - "native_provider_publication_executed": false + "native_provider_publication_executed": false, + "total_pilot_cases": 45, + "real_installed_warden_offline_fixture": true, + "synthetic_bao_only": true, + "native_login_used_by_tests": false + }, + "previous_publisher_source_sha256": "845294c574692333b35e627f5d3194e6334136f14f4f699e27a680f9cd4604db", + "revision": "r2-owner-handoff", + "handoff_fix": { + "clear_all_token_aliases_before_login": [ + "BAO_TOKEN", + "VAULT_TOKEN", + "OPENBAO_TOKEN" + ], + "isolation": "Verify expected private Warden session HOME by directory metadata only; never inspect token-helper contents", + "safe_handoff_booleans_in_receipt": true, + "lookup_api": "auth/token/lookup-self", + "effective_policies": "Union token and identity policies; root/agent boundary still refuse", + "nonzero_child_exit_does_not_imply_revocation_failure": true, + "same_provider_scope": true } } diff --git a/tests/test_vergabe_pilot_publish.py b/tests/test_vergabe_pilot_publish.py index cc539dc..f358068 100644 --- a/tests/test_vergabe_pilot_publish.py +++ b/tests/test_vergabe_pilot_publish.py @@ -160,6 +160,66 @@ class PublisherTests(unittest.TestCase): class BoundaryTests(unittest.TestCase): + def test_owner_environment_discards_all_token_aliases_before_fresh_login(self): + environment = {name: "synthetic" for name in publish.TOKEN_ENV} + environment.update(HOME="/synthetic", PATH="/usr/bin", BAO_ADDR="wrong", VAULT_ADDR="wrong") + result = publish.owner_login_environment(environment) + self.assertFalse(set(result) & set(publish.TOKEN_ENV)) + self.assertEqual(result["BAO_ADDR"], publish.TUNNEL) + self.assertEqual(result["VAULT_ADDR"], publish.TUNNEL) + self.assertEqual(result["HOME"], "/synthetic") + self.assertTrue(set(environment) & set(publish.TOKEN_ENV)) + + def test_real_warden_envelope_with_offline_synthetic_login(self): + executable = shutil.which("warden") + if not executable: + self.skipTest("installed Warden is required for its real envelope fixture") + first = Path(executable).read_text().splitlines()[0] + if not first.startswith("#!/") or " " in first: + self.skipTest("Warden Python interpreter could not be identified") + result = subprocess.run([first[2:], str(Path(__file__).with_name("vergabe_pilot_warden_handoff_fixture.py"))], + capture_output=True, timeout=30) + self.assertEqual(result.returncode, 0, "offline Warden handoff fixture failed") + evidence = json.loads(result.stdout) + self.assertTrue(evidence["sanitized_handoff_passed"]) + self.assertFalse(evidence["real_credentials_used"]) + + def test_failed_child_exit_does_not_prove_revocation_failure(self): + outcome = publish.session_outcome(11) + self.assertIsNone(outcome["session_cleanup_confirmed"]) + self.assertIsNone(outcome["session_revocation_confirmed"]) + self.assertTrue(outcome["session_storage_cleanup_confirmed"]) + self.assertFalse(publish.session_outcome(13)["session_revocation_confirmed"]) + self.assertFalse(publish.session_outcome(14)["session_storage_cleanup_confirmed"]) + self.assertTrue(publish.session_outcome(0)["session_cleanup_confirmed"]) + + def test_owner_custody_uses_lookup_api_and_effective_identity_policies(self): + from types import SimpleNamespace + calls = [] + def read(args): + calls.append(args) + if args[2] == "auth/token/lookup-self": + return {"data": {"policies": ["default", "platform-admin"], + "identity_policies": ["operator-custody"]}} + if args[2] == publish.LANE: + return {"data": {"metadata": {"version": 1}, "data": { + "NC_WEBDAV_TOKEN": "synthetic-upload", + "NC_WEBDAV_URL": publish.HOST + "/public.php/dav/filesdrop/synthetic-upload", + "AGE_PRIVATE_KEY": "synthetic-not-a-key"}}} + if args[2] == publish.OPERATOR: + return {"data": {"data": {"BACKUP_USERNAME": "Backup", "BACKUP_PASSWORD": "synthetic"}}} + raise AssertionError("unexpected custody read") + with tempfile.TemporaryDirectory(prefix="rapps-owner-context-") as temporary: + root = Path(temporary) / ".warden-attended-login" + root.mkdir(mode=0o700) + home = root / "session-fixture" + home.mkdir(mode=0o700) + with patch.dict(os.environ, {"HOME": str(home), "WARDEN_ROUTING_CATALOG": "synthetic", + "BAO_ADDR": publish.TUNNEL, "VAULT_ADDR": publish.TUNNEL}, clear=True): + result = publish.custody(SimpleNamespace(bao=read, data=lambda value: value)) + self.assertEqual(result["kv_version"], 1) + self.assertEqual([c[2] for c in calls], ["auth/token/lookup-self", publish.LANE, publish.OPERATOR]) + def provider(self): from types import SimpleNamespace return publish.Provider(SimpleNamespace(HOST=publish.HOST, LANE=publish.LANE, @@ -189,8 +249,9 @@ class BoundaryTests(unittest.TestCase): def test_ambient_token_refuses_without_a_secret_read(self): from types import SimpleNamespace - with patch.dict(os.environ, {"WARDEN_ROUTING_CATALOG": "synthetic", "BAO_ADDR": "http://127.0.0.1:18200", - "BAO_TOKEN": "synthetic-not-real"}), self.assertRaises(stores.Refused): + with patch.dict(os.environ, {"WARDEN_ROUTING_CATALOG": "synthetic", "BAO_ADDR": publish.TUNNEL, + "VAULT_ADDR": publish.TUNNEL, "BAO_TOKEN": "synthetic-not-real"}), \ + self.assertRaisesRegex(stores.Refused, "ambient_owner_token_refused"): publish.custody(SimpleNamespace()) def test_owner_helper_pins_validate_without_native_access(self): diff --git a/tests/vergabe_pilot_warden_handoff_fixture.py b/tests/vergabe_pilot_warden_handoff_fixture.py new file mode 100644 index 0000000..abcae94 --- /dev/null +++ b/tests/vergabe_pilot_warden_handoff_fixture.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Exercise installed Warden's real envelope with a fake, offline Bao client. + +Only synthetic files beneath a temporary HOME. No real token helper, login, +OpenBao, provider, backup or operator credential is inspected or used. +""" + +import json +import os +import sys +import tempfile +from pathlib import Path +from unittest.mock import patch + +from warden.proxy import AttendedLoginError, ResolvedFetch, proxy_attended_login_exec + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools")) +import vergabe_pilot_publish as publisher + + +def main(): + with tempfile.TemporaryDirectory(prefix="rapps-warden-offline-", dir="/tmp") as temporary: + root = Path(temporary) + caller = root / "caller" + caller.mkdir(mode=0o700) + client = root / "bao" + client.write_text("#!" + sys.executable + "\n" + ''' +import os,sys +from pathlib import Path +if sys.argv[1] == 'login': + helper=Path.home()/'.vault-token' + helper.write_text('SYNTHETIC-NO-AUTH') + helper.chmod(0o600) +elif sys.argv[1:] != ['token','revoke','-self']: + raise SystemExit(2) +''') + client.chmod(0o700) + proof = root / "handoff-proof.json" + code = ''' +import json,sys +from pathlib import Path +sys.path.insert(0,sys.argv[1]) +import vergabe_pilot_publish as p +try: + p.require_owner_handoff() +except p.Refused: + raise SystemExit(1) +Path(sys.argv[2]).write_text(json.dumps(p.owner_handoff_checks())) +''' + child = [sys.executable, "-c", code, str(Path(publisher.__file__).parent), str(proof)] + environment = {"HOME": str(caller), "PATH": os.environ.get("PATH", "/usr/bin:/bin"), + "WARDEN_ROUTING_CATALOG": str(root / "synthetic-catalog"), + "BAO_ADDR": publisher.TUNNEL, "VAULT_ADDR": publisher.TUNNEL, + "BAO_TOKEN": "synthetic-parent", "VAULT_TOKEN": "synthetic-parent", + "OPENBAO_TOKEN": "synthetic-inherited-alias"} + command = ResolvedFetch(argv=[str(client), "login"]) + with patch.dict(os.environ, environment, clear=True): + try: + proxy_attended_login_exec(command, child_argv=child) + except AttendedLoginError as error: + assert error.exit_code == 11 and error.outcome == "child_failed" + else: + raise AssertionError("unsanitized alias must refuse before custody") + assert not proof.exists() + assert not (caller / ".warden-attended-login").exists() + with patch.dict(os.environ, publisher.owner_login_environment(environment), clear=True): + assert proxy_attended_login_exec(command, child_argv=child) == 0 + assert all(json.loads(proof.read_text()).values()) + assert not (caller / ".warden-attended-login").exists() + print(json.dumps({"status": "passed", "installed_warden_envelope": True, + "unsanitized_alias_refused": True, "sanitized_handoff_passed": True, + "synthetic_session_storage_removed": True, "native_login": False, + "provider_access": False, "real_credentials_used": False})) + + +if __name__ == "__main__": + main() diff --git a/tools/vergabe_pilot_publish.py b/tools/vergabe_pilot_publish.py index 957ec9c..c971d4a 100644 --- a/tools/vergabe_pilot_publish.py +++ b/tools/vergabe_pilot_publish.py @@ -39,6 +39,8 @@ CIPHER_SHA256 = "4915288384e3ba8a87f1427b8e10106ee7241c04184adb97f439e57846bef76 CIPHER_BYTES = 182521 LANE = "platform/data/workloads/railiance/backup/offsite-lane" OPERATOR = "operators/data/nextcloud/backup" +TUNNEL = "http://127.0.0.1:18200" +TOKEN_ENV = ("BAO_TOKEN", "VAULT_TOKEN", "OPENBAO_TOKEN") PINS = { "openbao-attended-exec.py": "53fb5fb38ad17c4b06d7969a4d8968d976c8ebc8f1abfeb9a191aa43dc54a5c3", "migrate_nextcloud_backup_account.py": "d3285f403f3c947f75c3288d989152e59ab9a8f5542bcf901bd0a06b0f3e050c", @@ -71,20 +73,52 @@ def source(directory): return directory / OBJECT +def owner_login_environment(environ=None): + env = dict(os.environ if environ is None else environ) + for key in TOKEN_ENV: + env.pop(key, None) + env["BAO_ADDR"] = env["VAULT_ADDR"] = TUNNEL + return env + + +def owner_handoff_checks(): + home = Path.home() + shape = home.parent.name == ".warden-attended-login" and home.name.startswith("session-") + private = False + if shape: + try: + private_directory(home) + private_directory(home.parent) + private = True + except (OSError, Refused): + pass + return {"routing_marker_present": bool(os.environ.get("WARDEN_ROUTING_CATALOG")), + "operator_tunnel_matches": os.environ.get("BAO_ADDR") == os.environ.get("VAULT_ADDR") == TUNNEL, + "no_ambient_token_variables": not any(os.environ.get(k) for k in TOKEN_ENV), + "isolated_home_shape": shape, "isolated_home_private": private} + + +def require_owner_handoff(): + checks = owner_handoff_checks() + for key, reason in [("routing_marker_present", "owner_routing_marker_missing"), + ("operator_tunnel_matches", "owner_tunnel_required"), + ("no_ambient_token_variables", "ambient_owner_token_refused"), + ("isolated_home_shape", "isolated_owner_home_required"), + ("isolated_home_private", "private_owner_home_required")]: + require(checks[key], reason) + + def custody(helper): # Reuse the owner's contained OIDC session, never a token environment/file # handoff. Warden revokes and removes its private helper after the child. - require(os.environ.get("WARDEN_ROUTING_CATALOG") - and os.environ.get("BAO_ADDR") == "http://127.0.0.1:18200" - and not any(os.environ.get(k) for k in ("BAO_TOKEN", "VAULT_TOKEN", "OPENBAO_TOKEN")), - "contained_owner_login_required") + require_owner_handoff() def read(args, reason): try: return helper.data(helper.bao(args)) except Exception: raise Refused(reason) from None - identity = read(["token", "lookup", "-format=json"], "owner_identity_lookup_failed")["data"] - policies = set(identity.get("policies", [])) + identity = read(["read", "-format=json", "auth/token/lookup-self"], "owner_identity_lookup_failed")["data"] + policies = set(identity.get("policies") or []) | set(identity.get("identity_policies") or []) require({"platform-admin", "operator-custody"} <= policies and not policies & {"root", "agent-high-risk-boundary"}, "owner_recovery_identity_required") lane = read(["read", "-format=json", LANE], "offsite_custody_read_failed")["data"] @@ -269,6 +303,7 @@ def child(directory, output): "stage": "owner_custody"} try: helper = owner_helpers() + receipt["owner_handoff_checks"] = owner_handoff_checks() publish(directory, custody(helper), Provider(helper), receipt) except BaseException as error: receipt["failure"] = str(error) if isinstance(error, Refused) else "provider_recovery_failed" @@ -281,6 +316,14 @@ def child(directory, output): return 0 if receipt["status"] == "published_read_back_and_decrypted" else 1 +def session_outcome(exit_code): + # Warden 11/12 can mean a revoked or revocation-unconfirmed failed child. + # Only 0 confirms the whole envelope; 14 overrides any earlier phase. + return {"session_cleanup_confirmed": True if exit_code == 0 else False if exit_code in {13, 14} else None, + "session_revocation_confirmed": True if exit_code == 0 else False if exit_code == 13 else None, + "session_storage_cleanup_confirmed": True if exit_code in {0, 11, 12, 13} else False if exit_code == 14 else None} + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--execute", action="store_true") @@ -310,14 +353,14 @@ def main(): command = [sys.executable, str(PLATFORM / "scripts/openbao-attended-exec.py"), "--", sys.executable, str(Path(__file__).resolve()), "--contained", "--directory", str(directory), "--receipt", str(output)] - result = subprocess.run(command, check=False) + result = subprocess.run(command, check=False, env=owner_login_environment()) if output.exists(): export.regular(output, os.geteuid()) receipt = json.loads(output.read_text()) else: receipt = {"status": "owner_login_or_child_not_completed", "provider_outcome": "unconfirmed"} receipt.update(warden_exit_code=result.returncode, receipt_file=str(output), - session_cleanup_confirmed=result.returncode == 0) + **session_outcome(result.returncode)) flags = os.O_WRONLY | os.O_NOFOLLOW | (os.O_TRUNC if output.exists() else os.O_CREAT | os.O_EXCL) fd = os.open(output, flags, 0o600) with os.fdopen(fd, "w") as stream: diff --git a/workplans/RAPPS-WP-0014-vergabe-invited-pilot.md b/workplans/RAPPS-WP-0014-vergabe-invited-pilot.md index d10a3a6..4c73dd2 100644 --- a/workplans/RAPPS-WP-0014-vergabe-invited-pilot.md +++ b/workplans/RAPPS-WP-0014-vergabe-invited-pilot.md @@ -196,7 +196,7 @@ id: RAPPS-WP-0014-T03 status: wait needs_human: true decision_id: d06307e5-2737-461a-a2f4-a12c3b492a9a -intervention_note: "Matched populated capture, resume and encrypted workstation copy pass. Both accounts work after restart, with an Übersicht omission under UI review. Next: founder-run tools/vergabe_pilot_publish.py --execute for the prepared contained owner publication/readback/decryption ceremony. Isolated restore, access/rollback and operating ownership remain open." +intervention_note: "Capture/restart and encrypted workstation copy pass. First provider attempt logged in but the publisher handoff guard refused before credential/provider access; Warden reported revocation. Environment preparation and lifecycle reporting fixed and tested through the installed Warden envelope offline. Retry the same publisher --execute command with fresh founder login. Provider evidence, isolated restore, access/rollback and operating ownership remain open." priority: high assignee: the-custodian blocking_reason: "Matched populated capture, restart and encrypted workstation copy pass with overview UI follow-up. Await provider publication/independent readback, escrow decryption, isolated restored workflow, access checks, compatible rollback and operating ownership." @@ -451,6 +451,27 @@ pass, including real age roundtrip with mocked provider/custody. Native executio awaits the founder; Warden cleanup is distinct from data verification. T03 stays waiting for provider evidence and the remaining restore/rollback/access/owner gates. +2026-11-06 first provider attempt: Warden login succeeded, but the publisher +returned `contained_owner_login_required` at `owner_custody` (0.01 seconds, +Warden exit 11). Source ordering places this before token metadata, credential +reads or provider calls. Warden's terminal message confirms revocation. The +publisher's old `session_cleanup_confirmed: false` conflated a failed child with +cleanup failure; preserve the original receipt and this correction: +`docs/evidence/2026-11-06-vergabe-provider-custody-failure-return.json`. + +Revision 2 clears BAO_TOKEN, VAULT_TOKEN and OPENBAO_TOKEN before the existing +wrapper, checks private Warden HOME-directory metadata (no helper inspection), +records boolean handoff diagnostics, uses the owner's API self-lookup form and +combines effective token/identity policies. Exit 11 no longer claims revocation +failure from its code alone. The first receipt did not identify which guard +condition failed; the source mismatch for the third inherited token alias is +reproduced by an offline test through the installed real Warden envelope with a +fake Bao client. The normalized handoff and session cleanup pass that test. +Fifteen publisher / 45 total pilot checks pass; no native auth or provider call +occurs in those tests. Retry the same reviewed provider command; the earlier +go persists, while fresh login is needed because that session was revoked. No +provider scope, role, policy, credential grant or live app change is introduced. + ## Validate synthetic sizing against the exact release and shared contention