From f6eec13f7c5d36300c54d9caaec546f435f382b5 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 6 Nov 2026 19:57:59 +0100 Subject: [PATCH] Record native Windows browser workaround for pilot recovery login Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1abf1-7e95-7141-87c9-ee78332bf790 --- ...6-11-06-vergabe-browser-window-repair.json | 105 ++++++++ .../README.md | 48 +++- ...o-company-operating-acceptance.proposed.md | 16 +- tests/test_vergabe_operator_browser_probe.py | 49 ++++ tests/vergabe_operator_windows_fixture.py | 83 ++++++ tools/vergabe_operator_browser_probe.py | 236 ++++++++++++++++++ .../RAPPS-WP-0014-vergabe-invited-pilot.md | 39 ++- 7 files changed, 566 insertions(+), 10 deletions(-) create mode 100644 docs/evidence/2026-11-06-vergabe-browser-window-repair.json create mode 100644 tests/test_vergabe_operator_browser_probe.py create mode 100644 tests/vergabe_operator_windows_fixture.py create mode 100644 tools/vergabe_operator_browser_probe.py diff --git a/docs/evidence/2026-11-06-vergabe-browser-window-repair.json b/docs/evidence/2026-11-06-vergabe-browser-window-repair.json new file mode 100644 index 0000000..341ca17 --- /dev/null +++ b/docs/evidence/2026-11-06-vergabe-browser-window-repair.json @@ -0,0 +1,105 @@ +{ + "schema": "railiance.vergabe-pilot-browser-window-repair.v1", + "recorded_at": "2026-11-06T18:55:09.652746+00:00", + "workplan": "RAPPS-WP-0014", + "task": "RAPPS-WP-0014-T03", + "run_id": "20261106T100825Z-acc2a468a535", + "latest_failed_attempt": { + "status": "owner_login_or_child_not_completed", + "provider_outcome": "unconfirmed", + "warden_exit_code": 10, + "login_category": "callback_timeout", + "before_otp": true, + "observed_hostname": "auth.coulomb.social", + "receipt_file": "/home/worsch/.local/state/vergabe-pilot-recovery/20261106T100825Z-acc2a468a535/provider-receipt-d6a550f9d01d.json", + "session_cleanup_confirmed": null, + "session_revocation_confirmed": null, + "session_storage_cleanup_confirmed": null + }, + "user_observations": [ + "Sign in registers but OTP page never appears before CLI timeout", + "Titlebar X unresponsive", + "Second tab appears, tab switching unresponsive", + "Real-launcher X11/software local probe: random control activation, double click can select another label, unable to open another tab" + ], + "read_only_checks": { + "issuer_readiness": { + "status": "ready", + "authelia": "ok", + "factor_reader": "ok", + "lldap": "ok", + "privacyidea": "ok", + "elapsed_seconds": 0.183 + }, + "authelia_health": { + "http_status": 200, + "elapsed_seconds": 0.047 + }, + "authelia_keycape_lldap_deployments": "Ready, observed generation current", + "app_node_metrics_grant": "not present; no wider access requested" + }, + "stuck_browser_metadata": { + "profile_lock_metadata_used_for_pid_guard": true, + "pid": 1388237, + "main_cpu_percent_over_two_seconds": 0.5, + "memory_available_mib_approx": 5249, + "uninterruptible_wait_observed": false, + "graphics_markers": [ + "Vulkan", + "llvmpipe threads" + ], + "real_page_url_arguments_environment_or_profile_contents_read": false, + "terminated_only_identified_operator_process_after_user_tried_close": true + }, + "comparison": { + "linux_x11_software": "still unreliable; candidate not adopted", + "native_windows_edge": "Everything works normally", + "native_windows_verdict_source": "founder response to mouse/keyboard/new-tab/titlebar-X question", + "causal_limit": "Different platform/browser combination; exact underlying WSLg/Chrome defect not established" + }, + "windows_profile_setup": { + "initial_set_acl_failure": "Windows domain-account translation failed; no domain/account setting changed", + "working_method": "numeric current-user SID icacls grant, inheritance removed, protected owner-only ACL verified" + }, + "platform_fix": { + "commit": "8519459e6fedd1bfb25dc2cdee6372d7252f5d0a", + "launcher": "railiance-platform/scripts/operator-browser/xdg-open", + "worker": "railiance-platform/scripts/operator-browser/windows_browser.py", + "wsl_default": "native Windows Edge, new private disposable profile, no fallback to shared/default browser", + "non_wsl": "pinned Linux Chromium retained", + "issuer_and_callback_allowlist": "unchanged; worker rechecks and refuses credential-bearing query keys", + "url_transport": "URL passed to PowerShell worker on stdin; browser itself receives initial authorization URL as startup argument, as before", + "password_otp_tokens_or_keys_transferred_to_worker": false, + "token_aliases_cleared_in_windows_child_process": true, + "owned_process_lifetime_seconds": 600, + "scope": "Browser opener only; no API role, grant, factor, registration, callback, output allowlist or token-helper change", + "projection": { + "status": "applied", + "instance_role": "primary", + "instance_label": "railiance01", + "expected_commit": "8519459e6fedd1bfb25dc2cdee6372d7252f5d0a", + "derived_commit": "8519459e6fedd1bfb25dc2cdee6372d7252f5d0a", + "refused": 0, + "retired": 0, + "identifier_assignments": [] + } + }, + "validation": { + "operator_tests": 32, + "targeted_opener_cases": 15, + "apps_probe_scope_tests": 3, + "lint": "passed", + "native_lifecycle_fixture": { + "production_guard_rejects_local_fixture_url": true, + "windows_ipv4_to_localhost_redirect": true, + "private_acl_verified_before_launch": true, + "owned_process_closed": true, + "private_profile_removed": true, + "native_identity_request": false + }, + "final_credential_query_guard": "passed in Windows without browser creation or network request" + }, + "next": "Pull railiance-platform, then retry --publish 20261106T100825Z-acc2a468a535 --verify-restore in founder terminal; no recapture/pause", + "native_owner_login_with_new_opener_verified": false, + "fresh_point_provider_or_restore_verified": false +} diff --git a/docs/proposals/2026-11-06-vergabe-attended-backups/README.md b/docs/proposals/2026-11-06-vergabe-attended-backups/README.md index c9045a8..d125315 100644 --- a/docs/proposals/2026-11-06-vergabe-attended-backups/README.md +++ b/docs/proposals/2026-11-06-vergabe-attended-backups/README.md @@ -30,8 +30,8 @@ as historical evidence. No allowlist, token helper or login control is bypassed. Subsequent same-point retries also failed before child handoff: first callback timeout, then authentication_error (both Warden 10). The founder reports slow -browser interaction and an unresponsive feedback/confirmation page. **Pause -further native retries pending diagnosis.** The [investigation receipt](../../evidence/2026-11-06-vergabe-owner-login-investigation.json) +browser interaction and an unresponsive feedback/confirmation page. Further +native retries were paused pending diagnosis. The [investigation receipt](../../evidence/2026-11-06-vergabe-owner-login-investigation.json) records responsive public/tunnel checks and synthetic local callbacks through both hostname and IPv4, in headless and headed fresh browser contexts. Those checks do not reproduce the actual identity flow or human input latency and do @@ -43,6 +43,50 @@ No setting, callback registration, output allowlist or authentication requiremen was changed. The completed capture remains available; native retry/restore proof and possible session revocation remain unconfirmed. +## Current retry after the browser workaround + +The founder subsequently observed a pre-OTP stall on Authelia, unresponsive +titlebar/tab controls, and random control activation in a credential-free Linux +X11/software-rendering test. Native Windows Edge in a new private profile worked +normally for mouse, keyboard, tabs and window close. The +[window-repair receipt](../../evidence/2026-11-06-vergabe-browser-window-repair.json) +records the comparison; it does not establish the precise underlying WSLg/Chrome +defect. The unsuccessful X11/software launcher candidate was not adopted. + +Platform commit `8519459e6fedd1bfb25dc2cdee6372d7252f5d0a` now selects native +Windows Edge for this WSL workstation, with a fresh owner-only disposable +profile rather than the usual browser session. It uses the same issuer/callback +allowlist, rejects credential-bearing URL query keys, hands the URL to its +PowerShell worker through stdin and clears token aliases in that process. +Edge receives the initial authorization URL as a browser startup argument, as +Linux Chromium did. No password, OTP, authorization code or token is passed that +way. The worker closes only its owned PID/start-time/executable and attempts +profile removal after close or its 10-minute limit. Non-WSL Linux retains the +pinned Chromium path. Native launch failure refuses instead of opening a shared +or default browser profile. + +Thirty-two operator tests pass, including 15 opener cases. A local-only native +lifecycle fixture verifies protected profile creation, startup, redirect from +127.0.0.1 to localhost, owned-process close and private-profile removal. The real +issuer guard refuses the synthetic URL; the separate fixture copy admits only +its exact local listener. A final native guard check refuses synthetic credential +query data before browser creation. These checks perform no identity request. +The actual owner login and this fresh point's provider/restore proof remain +pending. No issuer/role, factor, callback-registration, output-allowlist or token- +helper control changed. + +From the Apps checkout, update the owner helper and retry the captured point: + +```bash +git -C ../railiance-platform pull --ff-only +python3 tools/vergabe_pilot_backup.py --publish 20261106T100825Z-acc2a468a535 --verify-restore +``` + +Use the founder identity in the new Windows window, complete OTP and the +diagnosis page's Continue while the CLI is waiting, and close that dedicated +window afterward. Return the sanitized receipts. No recapture or app pause is +needed for this retry. + ## First operating run — founder terminal ```bash diff --git a/docs/vergabe-demo-company-operating-acceptance.proposed.md b/docs/vergabe-demo-company-operating-acceptance.proposed.md index cd8e163..909a054 100644 --- a/docs/vergabe-demo-company-operating-acceptance.proposed.md +++ b/docs/vergabe-demo-company-operating-acceptance.proposed.md @@ -53,12 +53,16 @@ timeout before provider command handoff. Retry `--publish` for that same run wit `--verify-restore` and return the sanitized backup/worker/transport receipts. The [return](evidence/2026-11-06-vergabe-attended-callback-timeout-return.json) does not establish fresh provider publication, restore or session revocation. -Two further retries report callback_timeout and authentication_error before -provider handoff. Further native retries await the founder's stalled-page -hostname/heading details; see the [login investigation](evidence/2026-11-06-vergabe-owner-login-investigation.json). -Public/tunnel and fresh synthetic headed/headless callback checks pass, but do -not reproduce the real identity flow or establish a root cause. The completed -point remains available and no authentication control was changed. +Further retries report callback_timeout/authentication_error before provider +handoff. The founder identifies a pre-OTP Authelia stall and unresponsive browser +tabs/titlebar; the real-launcher X11/software test remains unreliable. The native +Windows Edge private-profile test works normally. The +[window-repair receipt](evidence/2026-11-06-vergabe-browser-window-repair.json) +and [updated operating packet](proposals/2026-11-06-vergabe-attended-backups/README.md#current-retry-after-the-browser-workaround) +record platform commit `8519459` selecting the isolated native Windows opener on +WSL, its 32 passing operator tests and credential-free lifecycle/cleanup proof. +Actual owner login and publication/restore of the completed point remain pending; +no issuer, factor, callback-registration or token-helper control changed. Review provider retention previews and execute only when eligible excess points exist; no artificial deletion is needed to accept an empty retention plan. diff --git a/tests/test_vergabe_operator_browser_probe.py b/tests/test_vergabe_operator_browser_probe.py new file mode 100644 index 0000000..409e71c --- /dev/null +++ b/tests/test_vergabe_operator_browser_probe.py @@ -0,0 +1,49 @@ +"""Scope checks for the credential-free browser UI probe; no GUI launch.""" + +import base64 +import sys +import unittest +from pathlib import Path +from types import SimpleNamespace + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools")) +import vergabe_operator_browser_probe as probe + + +class ProbeBoundaryTests(unittest.TestCase): + def test_windows_probe_accepts_only_fixed_loopback_test_page(self): + for url in ("https://auth.coulomb.social/", "http://127.0.0.1:1234/?code=synthetic", + "http://127.0.0.1:1234/oidc/callback", "http://user:password@127.0.0.1:1234/", + "http://127.0.0.1:1234/#fragment", "http://127.0.0.1:0/"): + with self.subTest(url=url), self.assertRaises(ValueError): + probe.windows_argv(url, 120) + + def test_native_command_uses_private_disposable_profile_and_owned_process_cleanup(self): + argv = probe.windows_argv("http://127.0.0.1:1234/", 120) + self.assertEqual(argv[:4], [probe.POWERSHELL, "-NoProfile", "-NonInteractive", "-EncodedCommand"]) + script = base64.b64decode(argv[4]).decode("utf-16le") + self.assertIn("'/inheritance:r'", script) + self.assertIn("$grant='*'+$sid.Value+':(OI)(CI)F'", script) + self.assertIn("WindowsIdentity]::GetCurrent().User", script) + self.assertIn("--user-data-dir=", script) + self.assertIn("$current.StartTime.ToUniversalTime().Ticks -eq $startTicks", script) + self.assertIn("$current.Path -eq $browser", script) + self.assertNotIn("Stop-Process -Name", script) + for flag in ("--no-sandbox", "--disable-web-security", "--ignore-certificate-errors", "--remote-debugging"): + self.assertNotIn(flag, script) + self.assertNotIn("auth.coulomb.social", script) + self.assertNotIn("kc.coulomb.social", script) + + def test_software_comparison_keeps_real_launcher_profile_and_url(self): + module = SimpleNamespace(launch_argv=lambda browser, profile, url: [browser, "--user-data-dir=" + profile, + "--no-first-run", url]) + argv = probe.probe_argv(module, "/cache/chrome", Path("/tmp/empty-probe"), "http://127.0.0.1:1234/", True) + self.assertIn("--user-data-dir=/tmp/empty-probe", argv) + self.assertEqual(argv[-1], "http://127.0.0.1:1234/") + self.assertIn("--ozone-platform=x11", argv) + self.assertIn("--disable-gpu", argv) + self.assertNotIn("--no-sandbox", argv) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/vergabe_operator_windows_fixture.py b/tests/vergabe_operator_windows_fixture.py new file mode 100644 index 0000000..727a191 --- /dev/null +++ b/tests/vergabe_operator_windows_fixture.py @@ -0,0 +1,83 @@ +"""Manual native lifecycle fixture: disposable local page, no identity request. + +The production worker's issuer/callback guard is first checked to reject the +local URL. A separate in-memory fixture copy admits only the exact disposable +loopback listener. Production source is never modified. No login, real profile +or token helper is used. Run only with explicit GUI approval. +""" + +import argparse +import base64 +import importlib.util +import json +import subprocess +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--module", type=Path, default=Path(__file__).resolve().parents[2] + / "railiance-platform/scripts/operator-browser/windows_browser.py") + args = parser.parse_args() + spec = importlib.util.spec_from_file_location("native_lifecycle_fixture", args.module) + native = importlib.util.module_from_spec(spec) + spec.loader.exec_module(native) + visits = [] + class Handler(BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def do_GET(self): + visits.append(self.path) + if self.path == "/": + self.send_response(302) + self.send_header("Location", "http://localhost:" + str(self.server.server_port) + "/ready") + self.end_headers() + return + self.send_response(200) + self.send_header("Content-Type", "text/html") + self.end_headers() + self.wfile.write(b"

Native browser lifecycle check

No login or credentials. This window closes automatically.

") + + server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + worker = threading.Thread(target=server.serve_forever, daemon=True) + worker.start() + url = "http://127.0.0.1:" + str(server.server_port) + "/" + payload = json.dumps({"url": url}).encode() + try: + refused = subprocess.run(native.command(10), input=payload, capture_output=True, timeout=20, check=False) + assert refused.stdout.strip() == b"failed" + source = native.SCRIPT.replace("__MAXIMUM__", "10") + original = "if($uri.Scheme -ne 'https' -or $uri.Host -ne 'kc.coulomb.social' -or $uri.Port -ne 443 -or $uri.UserInfo -or $uri.Fragment){throw 'invalid origin'}" + assert source.count(original) == 1 + source = source.replace(original, "if($url -ne '" + url + "'){throw 'fixture origin mismatch'}") + original = "if($callbacks.Count -ne 1 -or $callbacks[0] -notin @('http://localhost:8250/oidc/callback','http://127.0.0.1:8250/oidc/callback')){throw 'invalid callback'}" + assert source.count(original) == 1 + source = source.replace(original, "if($uri.Query){throw 'fixture query refused'}") + source = source.rstrip() + assert source.endswith("}") + source = source[:-1] + "\n[Console]::Out.WriteLine((@{fixture_complete=$true;opened=$opened;private_profile_removed=$removed}|ConvertTo-Json -Compress))\n}" + command = [native.POWERSHELL, "-NoProfile", "-NonInteractive", "-EncodedCommand", + base64.b64encode(source.encode("utf-16le")).decode()] + result = subprocess.run(command, input=payload, capture_output=True, timeout=45, check=False) + lines = result.stdout.decode("utf-8", errors="replace").splitlines() + assert lines[0] == "opened" + metadata = json.loads(lines[-1]) + assert metadata == {"fixture_complete": True, "opened": True, "private_profile_removed": True} + assert "/" in visits + assert "/ready" in visits + print(json.dumps({"status": "passed", "native_identity_request": False, + "production_guard_refused_synthetic_url": True, "worker_url_received_on_stdin": True, + "windows_loopback_reached": True, "private_acl_verified_before_launch": True, + "windows_localhost_redirect_reached": True, + "owned_process_closed": True, "private_profile_removed": True})) + finally: + server.shutdown() + server.server_close() + worker.join(timeout=2) + + +if __name__ == "__main__": + main() diff --git a/tools/vergabe_operator_browser_probe.py b/tools/vergabe_operator_browser_probe.py new file mode 100644 index 0000000..53c6f87 --- /dev/null +++ b/tools/vergabe_operator_browser_probe.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Credential-free manual UI probe using the real operator Chromium launcher. + +Default reviews only. --run opens a fresh window and private profile against a +disposable loopback page. --software-x11 selects X11/software rendering for +comparison. No issuer URL, account, credentials, callbacks or real profile. +""" + +import argparse +import base64 +import importlib.machinery +import importlib.util +import json +import os +import subprocess +import tempfile +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from urllib.parse import urlsplit + +LAUNCHER = Path(__file__).resolve().parents[2] / "railiance-platform/scripts/operator-browser/xdg-open" +POWERSHELL = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" +WINDOWS_PROBE = r''' +$ErrorActionPreference='Stop' +$ProgressPreference='SilentlyContinue' +$profile=$null;$process=$null;$startTicks=$null;$closed=$false;$removed=$false;$userClosed=$false;$ok=$false +$stage='browser_detection';$failureStage=$null;$errorKind=$null +try { + $browser=Join-Path ${env:ProgramFiles(x86)} 'Microsoft\Edge\Application\msedge.exe' + if(-not (Test-Path -LiteralPath $browser)){throw 'browser unavailable'} + $stage='profile_creation' + $profile=Join-Path ([IO.Path]::GetTempPath()) ('railiance-browser-probe-'+[guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $profile | Out-Null + $stage='profile_acl' + $stage='profile_acl_identity' + $sid=[Security.Principal.WindowsIdentity]::GetCurrent().User + $stage='profile_acl_apply' + $grant='*'+$sid.Value+':(OI)(CI)F' + & "$env:SystemRoot\System32\icacls.exe" $profile '/grant:r' $grant '/inheritance:r' | Out-Null + if($LASTEXITCODE -ne 0){throw 'private profile ACL command failed'} + $stage='profile_acl_verify' + $verified=Get-Acl -LiteralPath $profile + $rules=$verified.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]) + if(-not $verified.AreAccessRulesProtected -or $rules.Count -ne 1 -or $rules[0].IdentityReference.Value -ne $sid.Value){throw 'private profile ACL verification failed'} + $arguments=@(('--user-data-dir="'+$profile+'"'),'--no-first-run','--no-default-browser-check','--disable-sync','--new-window','__URL__') + $stage='process_start' + $process=Start-Process -FilePath $browser -ArgumentList $arguments -PassThru + $startTicks=$process.StartTime.ToUniversalTime().Ticks + $stage='process_wait' + $deadline=[datetime]::UtcNow.AddSeconds(__MAXIMUM__) + while(-not $process.HasExited -and [datetime]::UtcNow -lt $deadline){Start-Sleep -Milliseconds 250;$process.Refresh()} + $userClosed=$process.HasExited;$ok=$true +} catch { + $ok=$false;$failureStage=$stage;$errorKind=$_.Exception.GetType().Name +} finally { + if($process -and $startTicks){ + $current=Get-Process -Id $process.Id -ErrorAction SilentlyContinue + if($current -and $current.StartTime.ToUniversalTime().Ticks -eq $startTicks -and $current.Path -eq $browser){ + $null=$current.CloseMainWindow() + if(-not $current.WaitForExit(5000)){Stop-Process -Id $current.Id -Force -ErrorAction SilentlyContinue;$null=$current.WaitForExit(5000)} + } + $process.Refresh();$closed=$process.HasExited + } + if($profile -and ($closed -or -not $process)){ + try {Remove-Item -LiteralPath $profile -Recurse -Force -ErrorAction Stop;$removed=$true} catch {$removed=$false} + } + @{status=$(if($ok){'native_probe_finished'}else{'native_probe_failed'});failure_stage=$failureStage;error_kind=$errorKind;browser='Windows Edge';user_closed=$userClosed;owned_process_closed=$closed;private_profile_removed=$removed;native_login=$false} | ConvertTo-Json -Compress +} +''' +HTML = b'''Browser response check - no login + +

Browser response check

This is a local test. No sign-in or credentials.

+ + + +No test button activated yet. +

Click each numbered button and check that the reported number matches. +Then press Tab to focus a button and Enter to activate it. Close this window with its titlebar X when done.

+ +''' + + +def launcher_module(): + loader = importlib.machinery.SourceFileLoader("operator_browser_probe_launcher", str(LAUNCHER)) + spec = importlib.util.spec_from_loader(loader.name, loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +def probe_argv(module, browser, profile, url, software_x11): + command = module.launch_argv(browser, str(profile), url) + if software_x11: + command[-1:-1] = ["--ozone-platform=x11", "--disable-gpu"] + return command + + +def windows_argv(url, maximum): + parsed = urlsplit(url) + if not (parsed.scheme == "http" and parsed.hostname == "127.0.0.1" and parsed.port + and parsed.path == "/" and not parsed.query and not parsed.fragment + and not parsed.username and not parsed.password and 10 <= maximum <= 180): + raise ValueError("synthetic_loopback_only") + script = WINDOWS_PROBE.replace("__URL__", url).replace("__MAXIMUM__", str(maximum - 10)) + encoded = base64.b64encode(script.encode("utf-16le")).decode() + return [POWERSHELL, "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded] + + +def run(software_x11, maximum, windows=False): + if not windows: + module = launcher_module() + browser = module.browser_path() + if not browser: + raise RuntimeError("pinned_browser_unavailable") + events = [] + started = time.monotonic() + class Handler(BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def do_GET(self): + if self.path not in ("/", "/favicon.ico"): + self.send_error(404) + return + if self.path != "/favicon.ico": + event = {"event": self.path, "elapsed_seconds": round(time.monotonic() - started, 2)} + events.append(event) + print(json.dumps(event), flush=True) + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'") + self.end_headers() + self.wfile.write(HTML) + + def do_POST(self): + if self.path != "/event" or not 0 < int(self.headers.get("Content-Length", 0)) <= 1024: + self.send_error(400) + return + try: + body = json.loads(self.rfile.read(int(self.headers["Content-Length"]))) + if set(body) != {"target", "keyboard", "client_x", "client_y", "device_scale", "button_top", "button_bottom"}: + raise ValueError + if body["target"] not in ("one", "two", "three") or type(body["keyboard"]) is not bool: + raise ValueError + if any(type(body[k]) not in (int, float) for k in body if k not in ("target", "keyboard")): + raise ValueError + except (ValueError, KeyError, TypeError): + self.send_error(400) + return + event = {"event": "button_activated", "elapsed_seconds": round(time.monotonic() - started, 2), **body} + events.append(event) + print(json.dumps(event), flush=True) + self.send_response(204) + self.end_headers() + + with tempfile.TemporaryDirectory(prefix="vergabe-browser-probe-") as temporary: + root = Path(temporary) + home, profile = root / "empty-home", root / "empty-profile" + home.mkdir(mode=0o700) + profile.mkdir(mode=0o700) + server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + worker = threading.Thread(target=server.serve_forever, daemon=True) + worker.start() + url = "http://127.0.0.1:" + str(server.server_port) + "/" + command = windows_argv(url, maximum) if windows else probe_argv(module, browser, profile, url, software_x11) + env = {k: v for k, v in os.environ.items() if k not in ("BAO_TOKEN", "VAULT_TOKEN", "OPENBAO_TOKEN")} + env["HOME"] = str(home) + process = subprocess.Popen(command, env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE if windows else subprocess.DEVNULL, + stderr=subprocess.DEVNULL, start_new_session=True) + print(json.dumps({"status": "ui_probe_started", "software_x11": software_x11, + "native_windows": windows, "native_login": False, "maximum_seconds": maximum}), flush=True) + try: + while process.poll() is None and time.monotonic() - started < maximum: + time.sleep(0.25) + finally: + user_closed = process.poll() is not None + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=8) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + server.shutdown() + server.server_close() + worker.join(timeout=2) + if windows: + raw = process.stdout.read(16385) + process.stdout.close() + try: + metadata = json.loads(raw) if len(raw) <= 16384 else {} + print(json.dumps({k: metadata.get(k) for k in ("status", "failure_stage", "error_kind", "browser", "user_closed", "owned_process_closed", "private_profile_removed", "native_login")}), flush=True) + except (ValueError, UnicodeError): + print(json.dumps({"status": "native_probe_metadata_unavailable", "details_suppressed": True}), flush=True) + print(json.dumps({"status": "ui_probe_complete", "software_x11": software_x11, + "window_process_exited_before_deadline": user_closed, "events": events, + "human_wait_included_in_times": True, "native_login": False}), flush=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--run", action="store_true") + parser.add_argument("--software-x11", action="store_true") + parser.add_argument("--windows", action="store_true", help="use native Windows Edge with its own disposable profile") + parser.add_argument("--maximum-seconds", type=int, default=120) + args = parser.parse_args() + if not 10 <= args.maximum_seconds <= 180: + parser.error("maximum must be between 10 and 180 seconds") + if args.windows and args.software_x11: + parser.error("Windows and Linux rendering modes are mutually exclusive") + if not args.run: + print(json.dumps({"status": "prepared", "native_login": False, "fresh_profile": True, + "software_x11": args.software_x11, "native_windows": args.windows, "next": "--run"})) + return 0 + run(args.software_x11, args.maximum_seconds, args.windows) + return 0 + + +if __name__ == "__main__": + os.umask(0o077) + try: + raise SystemExit(main()) + except Exception: # noqa: BLE001 — do not return browser or environment diagnostics + print(json.dumps({"status": "ui_probe_failed", "details_suppressed": True})) + raise SystemExit(1) diff --git a/workplans/RAPPS-WP-0014-vergabe-invited-pilot.md b/workplans/RAPPS-WP-0014-vergabe-invited-pilot.md index 18050f2..97b0fb4 100644 --- a/workplans/RAPPS-WP-0014-vergabe-invited-pilot.md +++ b/workplans/RAPPS-WP-0014-vergabe-invited-pilot.md @@ -196,10 +196,10 @@ id: RAPPS-WP-0014-T03 status: wait needs_human: true decision_id: d06307e5-2737-461a-a2f4-a12c3b492a9a -intervention_note: "Baseline recovery and fresh attended capture/transfer pass. Same-point login retries fail before provider child: callback_timeout, then authentication_error; possible revocation unconfirmed. Founder reports slow/unresponsive confirmation page. Pause retries. Public discovery/tunnel and synthetic headed/headless Continue-to-loopback probes pass; actual stalled page hostname/heading requested without URLs/credentials. Preserve run 20261106T100825Z-acc2a468a535; publication/restore still pending." +intervention_note: "Baseline recovery and fresh capture/transfer pass. WSL Chrome titlebar/tabs/control input unreliable even in X11/software probe; native Windows Edge private-profile probe is founder-confirmed responsive. Platform fix 8519459 selects isolated native Windows opener on WSL; 32 operator tests plus lifecycle/localhost/cleanup checks pass. Pull railiance-platform then retry tools/vergabe_pilot_backup.py --publish 20261106T100825Z-acc2a468a535 --verify-restore. Actual owner login and this point's provider/restore receipts remain pending; no recapture/pause needed." priority: high assignee: the-custodian -blocking_reason: "Baseline recovery and accepted attended model pass; repeatable capture/transfer now pass. Contained owner login fails before provider handoff with callback_timeout/authentication_error; slow confirmation-page cause awaits identification. Same fresh point still needs provider/readback/decryption and restore. Restored native SSO/ingress and broader version rollback remain explicit limits." +blocking_reason: "Baseline recovery and accepted attended model pass; repeatable capture/transfer now pass. Native Windows browser workaround validated and published; await actual contained owner login, provider/readback/decryption and restore for the same fresh point. Restored native SSO/ingress and broader version rollback remain explicit limits." state_hub_task_id: "dd069c6d-fcc1-5bac-b233-976f2f0d5cd1" ``` @@ -594,6 +594,41 @@ issuer, callback, output allowlist or token-helper control was changed. Preserve the completed run; publication/restore and possible session revocation remain unconfirmed. T03 retains the existing wait. +2026-11-06 live browser diagnosis: the founder locates the delay at Authelia +before OTP, then reports titlebar X and tab switching unresponsive. The next +attempt exits callback_timeout before any OTP, receipt `provider-receipt-d6a550f9d01d.json`. +Issuer dependency readiness, Authelia health and read-only deployment status +are healthy; these do not exercise the password request. The identified +operator process is idle/low CPU with ample available memory; only lock/process/ +graphics-library metadata is read, no URL, page, environment or profile content. +The founder-requested frozen window is closed by its guarded PID. A real-launcher +X11/software local probe still has random control activation and unusable tab +controls, so that candidate is not adopted. Native Windows Edge in a fresh +owner-only profile works normally for mouse/keyboard/tabs/titlebar X, confirmed +by the founder. The exact underlying WSLg/Chrome defect is not established. + +Applied and published the owner fix in railiance-platform commit +`8519459e6fedd1bfb25dc2cdee6372d7252f5d0a`: on WSL, open the approved issuer in +native Edge with a new private disposable profile; retain pinned Chromium on +non-WSL Linux. Numeric-SID ACLs avoid the Windows domain-name translation error +encountered while preparing the test; no domain/account setting is changed. +Keep exact issuer/callback checks, reject credential-bearing query keys, pass +the authorization URL to the worker over stdin, clear Windows token aliases, +and bound cleanup to its owned PID/start/executable/profile. The browser still +receives the initial authorization URL as startup argument, as before; no +password/OTP/code/token is passed that way. Thirty-two operator tests pass, +including 15 opener cases, plus a credential-free Windows lifecycle fixture +with IPv4-to-localhost navigation and verified process/profile cleanup. The +production guard refuses the fixture URL, and a separate native negative check +refuses credential query data without opening a browser. No issuer/role/factor, +callback registration, output allowlist or token-helper control is changed. +Repo Manager confirms exact commit applied on primary/railiance01, zero refused +or retired records and no identifier assignments. Receipt: +`docs/evidence/2026-11-06-vergabe-browser-window-repair.json`. +After pulling railiance-platform, retry the same `--publish ... --verify-restore` +command in the founder terminal. Actual owner login, publication and restore for +this fresh point remain pending; T03 stays wait. No recapture/pause is needed. + ## Validate synthetic sizing against the exact release and shared contention