#!/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'''
This is a local test. No sign-in or credentials.
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)