diff --git a/OVERSEER-STATUS.md b/OVERSEER-STATUS.md new file mode 100644 index 0000000..0ab17e7 --- /dev/null +++ b/OVERSEER-STATUS.md @@ -0,0 +1,12 @@ +# OVERSEER-STATUS + +## 2026-07-20 — M-B4 rung-1 (LAN Gitea) safe prep, pre-live-fire +- Backup: `.backups/gitea-rung-2026-07-20.credrot.age` (age-sealed, 2 entries of `gitea/` prefix); **verified decrypts**; restore key custodied in gopass `incredigo/backups/gitea-rung-2026-07-20` (no plaintext on disk). Store confirmed intact. +- Selectors (read-only, via Tailscale `100.117.177.50:3030`; LAN IP dead): login `#user_name` ✅ `#password` ✅ submit **fixed** `.ui.primary.button` ✅ (seed `[type=submit]` MISSED — corrected in sites.go). Change page 303→/user/login ✅ confirms `LoginBeforeChange`. +- Unconfirmed (auth-gated, defer to live-fire; self-heal covers): change-form `#old_password`/`#password`/`#retype`/submit; login SuccessText. +- Zero Gitea writes; no submit performed. **Blocked on Andre:** (1) push local commits `1db4134`+`e11d2f0`(+uncommitted sites.go selector fix); (2) explicit go for the live rotation. + +## 2026-07-21 — M-B4 LIVE-FIRE DONE: first REAL browser password rotation (LIVE-REAL) +- **Rotated** the self-owned Gitea login password (`trill-technician` @ `100.117.177.50:3030`, gopass `gitea/login-creds`) end-to-end via `lab/rung4-gitea-browser` (MODE=live): backup gate 3 gitea entries sealed+verified → pre-change login(OLD) → change-form submit → **site confirmed** → new stored to gopass → verified by browser re-login **and** independent API re-auth (HTTP 200). Old password now dead (confirmed). Metadata lines (username/url/notes) restored on the entry. +- **Engine fixes from the live-fire (all tested green, 11 pass, -race clean):** (1) `sites.go` submit selectors drop `[type=submit]` (Gitea buttons have only the default DOM property, no attribute) — login `.ui.primary.button`, change `form[action$='/user/settings/account'] button`; (2) `RodDriver.Click` waits for post-submit navigation to settle before reading success text (remote round-trip vs instant in-process test); (3) new `RodDriver.Insecure` = `--ignore-certificate-errors` for the self-signed/Tailscale host. +- **Proof-as-data:** `GiteaSite.Proof` promoted UNPROVEN → **ProofLiveReal**; docs (`BROWSER-ROTATION.md` §9, `BROWSERROT-PLAN.md`) updated. Safety held throughout: the two prior failed attempts (submit selector miss) left the password UNCHANGED (API 200 confirmed) — fail-safe worked. Seal passphrase custodied in gopass `incredigo/backups/gitea-rung4-browser-2026-07-20`. **Not yet committed/pushed.** diff --git a/docs/BROWSER-ROTATION.md b/docs/BROWSER-ROTATION.md index 26edaa8..849f675 100644 --- a/docs/BROWSER-ROTATION.md +++ b/docs/BROWSER-ROTATION.md @@ -318,10 +318,21 @@ adapter is pure-exec Go, so it is unit-validated cross-platform via an injected invalidates the old password itself. Proof-as-data per `Site`: the **ENGINE is LIVE-VM** (real headless Chromium against a real local change-password form — `lab-provision-browserrot.sh` / `TestIntegration_realBrowserRotation`); a **specific real-site - selector table stays UNPROVEN** until run against that real site, and no production `rotate` - wiring auto-enables it yet (still gated behind per-credential authorization). Still to do: - build out the curated selector table for real high-frequency no-MFA sites and measure the real - success rate before promising anything. + selector table stays UNPROVEN** until run against that real site. **First real-site promotion + done: `GiteaSite` is LIVE-REAL** — it drove a real password rotation of a self-owned Gitea + account (`trill-technician` @ `100.117.177.50:3030`) on 2026-07-21 via + `lab/rung4-gitea-browser` (M-B4): backup-gated (Hard Rule 1, 3 entries sealed+verified), + pre-change login with the OLD secret, change-form submit, then the NEW secret verified by + browser re-login AND independent API re-auth (HTTP 200). Two real-site selector fixes fell out + of the live-fire and are now in `sites.go`: (1) Gitea's login **and** "Update Password" buttons + carry no `type=submit` *attribute* (only the default DOM property), so CSS `[type=submit]` + misses — select by class / by the account form's sole button instead; (2) `RodDriver.Click` + now waits for the post-submit navigation to settle (a remote round-trip, unlike the instant + in-process test) before the success text is read. `RodDriver.Insecure` was added for the + self-signed/Tailscale host (browser analog of `curl -k`). No production `rotate` wiring + auto-enables browser rotation yet (still gated behind per-credential authorization; the + live-fire is a dedicated harness). Still to do: build out the curated selector table for other + real high-frequency no-MFA sites and measure the real success rate before promising anything. - **Phase A-tier-1.5 — Node stealth sidecar (built + LIVE-VM):** an optional swap-in `Driver` (`internal/browserrot/sidecar.go` + `internal/browserrot/sidecar/`) that speaks newline-delimited JSON-RPC over stdio to a small Node program driving a **patchright** diff --git a/docs/BROWSERROT-PLAN.md b/docs/BROWSERROT-PLAN.md index 454df38..8980dad 100644 --- a/docs/BROWSERROT-PLAN.md +++ b/docs/BROWSERROT-PLAN.md @@ -1,9 +1,13 @@ # browserrot buildout plan — Node stealth sidecar + AI self-heal (Phase A tiers 1-3) -**Status (2026-07-19):** M-B1 ✅ M-B2 ✅ M-B3 ✅ (all LIVE-VM proven against the local lab -form, `internal/browserrot/` suite green + `-race` clean). **M-B4 (first REAL self-owned -site) deliberately NOT started** — it rotates a real credential and needs explicit -per-credential authorization (safe-candidate ladder). M-B5 (vision) later. The Stagehand +**Status (2026-07-21):** M-B1 ✅ M-B2 ✅ M-B3 ✅ **M-B4 ✅** (first REAL self-owned site). +M-B1..B3 are LIVE-VM against the local lab form; **M-B4 is LIVE-REAL** — `GiteaSite` drove a +real password rotation of a self-owned Gitea account (`trill-technician` @ +`100.117.177.50:3030`) on 2026-07-21 via `lab/rung4-gitea-browser`, backup-gated and verified +by browser re-login + API re-auth (HTTP 200). Two real-site selector fixes landed from the +live-fire (no `type=submit` attribute on Gitea buttons; `RodDriver.Click` now waits for the +post-submit navigation), plus `RodDriver.Insecure` for the self-signed/Tailscale host. The +`internal/browserrot/` suite is green + `-race` clean. M-B5 (vision) later. The Stagehand (LLM) heal strategy is wired but UNPROVEN/opt-in; the deterministic DOM-heuristic heal is the LIVE-VM Tier-2. See `docs/BROWSER-ROTATION.md` §9 for the proof-as-data record. **Extends, does not replace:** [`docs/BROWSER-ROTATION.md`](BROWSER-ROTATION.md) (the design @@ -142,10 +146,12 @@ Per-`Site` `Proof`: selector miss → rewrite + persist the recipe → next run is Tier-1. Test: seed a *broken* selector, assert one run heals it and the second run needs no LLM. Assert **no secret** in any heal request (leak-check the IPC transcript). -- **M-B4 — record command + first REAL site.** `incredigo browser record`; then point the - engine at ONE real self-owned account (low-stakes, no MFA), rotate + re-login-verify, promote - that host to LIVE-REAL. New `lab/`/runbook entry, per-cred authorization (safe-candidate - ladder discipline). +- **M-B4 — record command + first REAL site. ✅ DONE (2026-07-21, rotation half).** Pointed the + engine at ONE real self-owned account (self-hosted Gitea, no MFA), rotated + re-login-verified, + promoted `GiteaSite` to LIVE-REAL. Harness: `lab/rung4-gitea-browser` (env-only secrets, + isolated to the real gopass entry `gitea/login-creds`, `MODE=probe|live`, `GITEA_INSECURE=1` + for the self-signed host). Backup-gated (Hard Rule 1). The `incredigo browser record` codegen + command is still TODO — this run used the curated `GiteaSite` recipe, not a recorded one. - **M-B5 (optional/later) — vision fallback.** Tier-3 Computer Use for hostile markup; secrets injected locally, never to the model. diff --git a/internal/browserrot/browserrot_test.go b/internal/browserrot/browserrot_test.go index 2a81ac1..0efd4cd 100644 --- a/internal/browserrot/browserrot_test.go +++ b/internal/browserrot/browserrot_test.go @@ -352,9 +352,10 @@ func TestGiteaSite_recipeShape(t *testing.T) { if s.Login.URL == "" || s.Form.CurrentSel == "" || s.Form.NewSel == "" || s.Form.SubmitSel == "" { t.Fatalf("recipe has empty required selectors: %+v", s) } - // A seed recipe is UNPROVEN until actually run against the real instance (proof-as-data). - if s.Proof != rotate.ProofUnproven { - t.Fatalf("a fresh curated recipe must be ProofUnproven, got %v", s.Proof) + // Promoted to LIVE-REAL: the recipe drove a real self-owned Gitea password + // rotation on 2026-07-21 (lab/rung4-gitea-browser, M-B4). Proof-as-data. + if s.Proof != rotate.ProofLiveReal { + t.Fatalf("GiteaSite recipe should be ProofLiveReal after the M-B4 live-fire, got %v", s.Proof) } } diff --git a/internal/browserrot/rod.go b/internal/browserrot/rod.go index 483676e..740acfa 100644 --- a/internal/browserrot/rod.go +++ b/internal/browserrot/rod.go @@ -22,6 +22,13 @@ type RodDriver struct { Bin string // chromium binary; "" lets go-rod find/download one Headless bool // default true in production; false to watch it locally Timeout time.Duration // per-operation timeout; defaults to 30s + // Insecure accepts self-signed / untrusted TLS certs (Chromium + // --ignore-certificate-errors). Needed for self-hosted targets behind a private + // CA or a Tailscale-fronted service (e.g. a personal Gitea on 100.x). This is the + // browser analog of `curl -k` in the rung harnesses; the connection is still TLS, + // we just don't verify the chain. Off by default — opt in per run for a host you + // own and whose cert you already trust out-of-band. + Insecure bool } // Name identifies the backend in logs/plans. @@ -32,6 +39,11 @@ func (d *RodDriver) Name() string { return "go-rod" } func (d *RodDriver) Open(ctx context.Context) (Session, error) { l := launcher.New().Headless(d.Headless). Set("no-sandbox").Set("disable-dev-shm-usage") + if d.Insecure { + // Accept the self-signed cert of a self-owned host. TLS still encrypts; only + // chain verification is skipped. Scoped to this launched browser instance. + l = l.Set("ignore-certificate-errors") + } if d.Bin != "" { l = l.Bin(d.Bin) } @@ -93,7 +105,21 @@ func (s *rodSession) Click(ctx context.Context, selector string) error { if err != nil { return err } - return el.Click(proto.InputMouseButtonLeft, 1) + // A submit/login click triggers a navigation (login POST -> 302 -> dashboard, + // change-form POST -> flash). Against a remote host that round-trip has real + // latency, so we must wait for the NEW document to commit before returning — + // otherwise the next Text() read resolves against the PRE-submit page (the login + // form still showing) and mis-reports the outcome. Arm the navigation wait BEFORE + // clicking, then block until the network is almost idle. Bounded by the page + // timeout; if a click does not navigate this returns when the timeout elapses. + wait := s.page.Timeout(s.to).WaitNavigation(proto.PageLifecycleEventNameNetworkAlmostIdle) + if err := el.Click(proto.InputMouseButtonLeft, 1); err != nil { + return err + } + wait() + // Belt-and-suspenders: let any post-navigation DOM (flash render) settle. + _ = s.page.Timeout(s.to).WaitStable(300 * time.Millisecond) + return nil } func (s *rodSession) Text(ctx context.Context, selector string) (string, error) { diff --git a/internal/browserrot/sites.go b/internal/browserrot/sites.go index 3a2e204..e1d260e 100644 --- a/internal/browserrot/sites.go +++ b/internal/browserrot/sites.go @@ -19,9 +19,10 @@ import ( // Gitea reaches the change-password controls only inside an authenticated session // (/user/settings/account 302s to /user/login otherwise), so LoginBeforeChange is on. // The password form on the account page carries old_password / password / retype; the -// result renders in a flash message. These selectors match current Gitea markup but are -// UNPROVEN against any specific instance/version — VERIFY on first run (self-heal will -// rewrite any that drift). The SuccessText markers are the one part self-heal cannot fix +// result renders in a flash message. Every selector here was VERIFIED against a live +// Gitea instance on 2026-07-21 (login, the three change fields, and the submit button) +// during the M-B4 live-fire; other instances/versions may drift, and self-heal will +// rewrite any that do. The SuccessText markers are the one part self-heal cannot fix // (they are content, not selectors): a wrong marker only makes Rotate fail SAFE (old // secret kept, human flagged), never a silent bad rotation. func GiteaSite(baseURL, username string) Site { @@ -30,10 +31,15 @@ func GiteaSite(baseURL, username string) Site { Host: strings.TrimPrefix(strings.TrimPrefix(base, "https://"), "http://"), ChangeURL: base + "/user/settings/account", Form: Form{ - CurrentSel: "#old_password", - NewSel: "#password", - ConfirmSel: "#retype", - SubmitSel: "form[action$='/user/settings/account'] button[type=submit]", + CurrentSel: "#old_password", + NewSel: "#password", + ConfirmSel: "#retype", + // Gitea's "Update Password" button carries NO type=submit ATTRIBUTE — it is + // a bare