diff --git a/docs/backup-credential-recovery.md b/docs/backup-credential-recovery.md index 92c8ce5..dc7045b 100644 --- a/docs/backup-credential-recovery.md +++ b/docs/backup-credential-recovery.md @@ -141,8 +141,11 @@ python3 scripts/openbao-attended-exec.py -- /absolute/path/to/reviewed-silent-co The wrapper keeps Warden's `openbao-platform-admin-login --exec` envelope and puts the repository launcher ahead of the default `xdg-open`. The launcher allows only the Keycloak HTTPS host and exact local OIDC callback. It opens -that page in a new empty Edge or Chrome profile, not the signed-in Windows -session, and it does not record the address. +that page in a new empty profile of the headed Chromium pinned by +coulomb-social's Playwright install. It does not open Firefox or another +Windows default browser, and it does not record the address. The remote +headless rapp-playwright service is a different facility and cannot complete +this localhost callback. No token helper or captured Warden output may be inspected. A receipt written by the owner command proves handoff; session cleanup is assessed separately from the final Warden exit status. diff --git a/docs/credential-lane-designs/mas-evidence-backup-grant-2026-11-02.md b/docs/credential-lane-designs/mas-evidence-backup-grant-2026-11-02.md index 2a47a78..2cefff9 100644 --- a/docs/credential-lane-designs/mas-evidence-backup-grant-2026-11-02.md +++ b/docs/credential-lane-designs/mas-evidence-backup-grant-2026-11-02.md @@ -34,8 +34,9 @@ and is not created here. 1. Founder review is recorded: platform-owner and mas-owner, source `founder-approval-2026-11-02`. The live account matches the binding. 2. Apply the policy and role with the attended platform-admin login: - `BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200 warden access openbao-platform-admin-login --exec -- /home/worsch/railiance-platform/scripts/install-mas-backup-primary-reader.sh` - Sign in as platform-root. The helper is silent and writes only a receipt. + `python3 scripts/openbao-attended-exec.py -- /home/worsch/railiance-platform/scripts/install-mas-backup-primary-reader.sh` + The window is a new empty Playwright Chromium profile. Sign in as + platform-root. The helper is silent and writes only a receipt. 3. In the Scaleway console, create a new IAM application named `mas-evidence-backup-primary`. Do not add a key to email-connect or to a broad application. Then say that the application exists. This workstation diff --git a/scripts/operator-browser/xdg-open b/scripts/operator-browser/xdg-open index eb5277f..e8a0706 100755 --- a/scripts/operator-browser/xdg-open +++ b/scripts/operator-browser/xdg-open @@ -1,9 +1,11 @@ #!/usr/bin/env python3 -"""Open the approved OpenBao login in a new empty browser profile. +"""Open the approved OpenBao login in a new empty Playwright Chromium profile. -The default Windows session is not used. A previous login stayed signed in -there as another account, so this launcher creates a fresh profile each time. +The Windows default browser is not used. A previous login opened Firefox, +which was already signed in as another account. This launcher uses the +headed Chromium binary pinned by coulomb-social's Playwright install. """ +import json import os import subprocess import sys @@ -12,8 +14,9 @@ from pathlib import Path from urllib.parse import parse_qs, urlsplit -EDGE = "/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" -CHROME = "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" +BROWSERS_JSON = Path( + "/home/worsch/coulomb-social/scripts/node_modules/playwright-core/browsers.json" +) def allowed(url): @@ -38,19 +41,38 @@ def allowed(url): return False -def browser_path(): - for candidate in (EDGE, CHROME): - if Path(candidate).is_file(): - return candidate +def chromium_executable(browsers_json, cache_root): + try: + spec = json.loads(Path(browsers_json).read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError): + return "" + browsers = spec.get("browsers") if isinstance(spec, dict) else None + if not isinstance(browsers, list): + return "" + revision = "" + for item in browsers: + if isinstance(item, dict) and item.get("name") == "chromium": + revision = str(item.get("revision") or "") + break + if not revision.isdecimal(): + return "" + candidate = Path(cache_root) / f"chromium-{revision}" / "chrome-linux64" / "chrome" + if candidate.is_file() and os.access(candidate, os.X_OK): + return str(candidate) return "" +def browser_path(): + return chromium_executable(BROWSERS_JSON, Path.home() / ".cache/ms-playwright") + + def launch_argv(browser, profile_dir, url): return [ browser, f"--user-data-dir={profile_dir}", "--no-first-run", "--no-default-browser-check", + "--disable-sync", "--new-window", url, ] @@ -71,51 +93,29 @@ def main(): if len(sys.argv) != 2 or not allowed(sys.argv[1]): stamp("rejected") return 1 + if not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"): + stamp("no-display") + return 1 browser = browser_path() if not browser: stamp("no-browser") return 1 profile = Path(tempfile.mkdtemp(prefix="operator-browser-")) os.chmod(profile, 0o700) + command = launch_argv(browser, str(profile), sys.argv[1]) try: - windows_profile = subprocess.run( - ["wslpath", "-w", str(profile)], - capture_output=True, - check=False, - timeout=10, - ) - if windows_profile.returncode != 0: - return 1 - profile_dir = windows_profile.stdout.decode().strip() - if not profile_dir or any(char in profile_dir for char in "\r\n"): - return 1 - command = launch_argv(browser, profile_dir, sys.argv[1]) - payload = "\n".join(command) + "\n" - opened = subprocess.run( - [ - "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe", - "-NoProfile", - "-NonInteractive", - "-Command", - "$ErrorActionPreference='Stop'; " - "$lines=@([Console]::In.ReadToEnd().Split(\"`n\") | " - "Where-Object { $_ -ne '' }); " - "if ($lines.Count -lt 2) { exit 1 }; " - "Start-Process -FilePath $lines[0] -ArgumentList $lines[1..($lines.Count-1)]", - ], - input=payload.encode(), + subprocess.Popen( + command, + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - timeout=20, + start_new_session=True, ) - if opened.returncode != 0: - stamp("open-failed") - return 1 - stamp("opened") - return 0 - except (OSError, subprocess.SubprocessError): + except OSError: stamp("open-failed") return 1 + stamp("opened") + return 0 if __name__ == "__main__": diff --git a/tests/test_operator_browser.py b/tests/test_operator_browser.py index 4ed9f19..c577bda 100644 --- a/tests/test_operator_browser.py +++ b/tests/test_operator_browser.py @@ -17,8 +17,26 @@ class BrowserBoundary(unittest.TestCase): self.assertFalse(allowed('https://kc.coulomb.social/?redirect_uri=http://localhost:8250/oidc/callback&redirect_uri=https://evil.example')) def test_launch_uses_empty_profile(self): url='https://kc.coulomb.social/authorize?'+urlencode({'redirect_uri':'http://localhost:8250/oidc/callback'}) - argv=launch_argv(r'C:\Edge\msedge.exe', r'C:\fresh\profile', url) - self.assertEqual(argv[0], r'C:\Edge\msedge.exe') - self.assertIn(r'--user-data-dir=C:\fresh\profile', argv) + browser='/cache/ms-playwright/chromium-1234/chrome-linux64/chrome' + argv=launch_argv(browser, '/tmp/fresh-profile', url) + self.assertEqual(argv[0], browser) + self.assertIn('--user-data-dir=/tmp/fresh-profile', argv) self.assertEqual(argv[-1], url) - self.assertNotIn('explorer.exe', ' '.join(argv)) + self.assertNotIn('--headless', argv) + joined=' '.join(argv) + for banned in ('explorer.exe', 'powershell', 'msedge', 'firefox', '/mnt/c/'): + self.assertNotIn(banned, joined) + def test_chromium_comes_from_the_playwright_pin(self): + import json + import tempfile + from pathlib import Path + with tempfile.TemporaryDirectory() as raw: + root=Path(raw) + spec=root/'browsers.json' + spec.write_text(json.dumps({'browsers':[{'name':'chromium','revision':'1234'},{'name':'firefox','revision':'1538'}]})) + binary=root/'chromium-1234'/'chrome-linux64'/'chrome' + binary.parent.mkdir(parents=True) + binary.write_text('') + binary.chmod(0o755) + self.assertEqual(launcher['chromium_executable'](spec, root), str(binary)) + self.assertEqual(launcher['chromium_executable'](root/'missing.json', root), '') diff --git a/workplans/RPF-WP-0070-mas-evidence-recovery.md b/workplans/RPF-WP-0070-mas-evidence-recovery.md index 0cdab4e..017821e 100644 --- a/workplans/RPF-WP-0070-mas-evidence-recovery.md +++ b/workplans/RPF-WP-0070-mas-evidence-recovery.md @@ -40,7 +40,7 @@ status: progress priority: medium needs_human: true depends_on: [RPF-WP-0053-T05] -intervention_note: "Founder approved CCR-2026-0062. The delivery account exists. OpenBao policy/role install still needs the attended platform-admin login, and the IAM application is not created. Age-identity escrow remains separate." +intervention_note: "Founder approved CCR-2026-0062. The delivery account exists. OpenBao policy/role install still needs the attended platform-admin login in the empty Playwright Chromium profile. The IAM application is not created. Age-identity escrow remains separate." state_hub_task_id: "4f6484fb-8303-5dd5-94cb-b2622e1322c8" ```