Record native Windows browser workaround for pilot recovery login
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1abf1-7e95-7141-87c9-ee78332bf790
This commit is contained in:
parent
00739c22a6
commit
f6eec13f7c
7 changed files with 566 additions and 10 deletions
105
docs/evidence/2026-11-06-vergabe-browser-window-repair.json
Normal file
105
docs/evidence/2026-11-06-vergabe-browser-window-repair.json
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
49
tests/test_vergabe_operator_browser_probe.py
Normal file
49
tests/test_vergabe_operator_browser_probe.py
Normal file
|
|
@ -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()
|
||||
83
tests/vergabe_operator_windows_fixture.py
Normal file
83
tests/vergabe_operator_windows_fixture.py
Normal file
|
|
@ -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"<h1>Native browser lifecycle check</h1><p>No login or credentials. This window closes automatically.</p>")
|
||||
|
||||
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()
|
||||
236
tools/vergabe_operator_browser_probe.py
Normal file
236
tools/vergabe_operator_browser_probe.py
Normal file
|
|
@ -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'''<!doctype html><meta charset="utf-8"><title>Browser response check - no login</title>
|
||||
<style>body{font:20px system-ui;max-width:40rem;margin:2rem auto}button{display:block;margin:1rem;padding:1rem;background:#def;color:#123;width:32rem;text-align:left}output{display:block;padding:1rem;background:#eee}</style>
|
||||
<h1>Browser response check</h1><p>This is a local test. No sign-in or credentials.</p>
|
||||
<button id="one">1. First test button</button>
|
||||
<button id="two">2. Second test button</button>
|
||||
<button id="three">3. Third test button</button>
|
||||
<output id="result">No test button activated yet.</output>
|
||||
<p>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.</p>
|
||||
<script>
|
||||
for(const button of document.querySelectorAll('button'))button.addEventListener('click',event=>{
|
||||
document.getElementById('result').textContent='Activated: '+button.textContent;
|
||||
const r=button.getBoundingClientRect();
|
||||
const observation={target:button.id,keyboard:event.detail===0,client_x:event.clientX,client_y:event.clientY,
|
||||
device_scale:window.devicePixelRatio,button_top:r.top,button_bottom:r.bottom};
|
||||
fetch('/event',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(observation)});
|
||||
});
|
||||
</script>
|
||||
'''
|
||||
|
||||
|
||||
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)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue