browserrot: Node stealth sidecar + AI self-heal (M-B1..M-B3, LIVE-VM)
Extends the Phase A-tier-1 site-side rotation engine with an optional Node browser-sidecar and a deterministic self-heal tier, all proven LIVE-VM against the local lab change-password form. go-rod stays the default; the sidecar is a swap-in Driver behind the same Session/Driver/Rotator interfaces — no spine change, backup + verify-new-before-revoke-old + proof gates all apply unchanged. M-B1 sidecar parity: internal/browserrot/sidecar/ (index.js JSON-RPC over stdio, patchright real-fingerprint Chromium, playwright-core fallback) + sidecar.go Driver/Session. A shared test harness runs go-rod and the sidecar through the identical live rotation proof. M-B2 stealth: patchright removes navigator.webdriver; humanized input (Bezier mouse paths + per-key typing jitter, no ML trajectory generation per plan §5). M-B3 self-heal: Healer interface + heal-retry fill/click/text wrappers; on a rotted selector, heal.js relocates the field from DOM structure ONLY (never a value) and the engine rewrites + persists the recipe (recipes.go RecipeBook) so the next run is Tier-1 again. Deterministic heuristic is the proven Tier-2; Stagehand LLM strategy is opt-in + UNPROVEN. Leak-check asserts no secret ever reaches a heal request (redacted IPC transcript). Secret seam documented honestly: fill carries base64 secret bytes across exactly one local stdio boundary, typed and wiped, never logged/persisted/sent to a model. MFA/CAPTCHA still degrade to the human worklist (Hard Rule 5). 14 tests green, -race clean; lab/lab-provision-browserrot.sh proves both drivers. M-B4 (first REAL self-owned site) deliberately NOT started — needs per-credential authorization (safe-candidate ladder). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -217,6 +217,17 @@ func TestIntegration_realBrowserRotation(t *testing.T) {
|
||||
if bin == "" {
|
||||
t.Skip("set CHROME_BIN to a Chromium executable to run the LIVE-VM browser proof")
|
||||
}
|
||||
runLiveRotationProof(t, &RodDriver{Bin: bin, Headless: true, Timeout: 30 * time.Second})
|
||||
}
|
||||
|
||||
// runLiveRotationProof drives one Driver through the full LIVE-VM proof: it stands up a
|
||||
// throwaway change-password + login website in-process, then asserts
|
||||
// discover→inject→submit→verify(new)→old-rejected. Both the go-rod and node-sidecar
|
||||
// drivers run this identical harness, so "parity" is a literal shared test. Optional
|
||||
// mutators tweak the Site before the run (e.g. to rot a selector for a self-heal proof).
|
||||
// It returns the Rotator so callers can assert post-run state (Healed(), HealedSite()).
|
||||
func runLiveRotationProof(t *testing.T, drv Driver, mutators ...func(*Site)) *Rotator {
|
||||
t.Helper()
|
||||
|
||||
const oldPW = "OLD-corr3ct-horse-battery"
|
||||
var mu sync.Mutex
|
||||
@@ -282,6 +293,9 @@ func TestIntegration_realBrowserRotation(t *testing.T) {
|
||||
},
|
||||
Proof: rotate.ProofLiveVM,
|
||||
}
|
||||
for _, m := range mutators {
|
||||
m(&site)
|
||||
}
|
||||
|
||||
v := vault.New()
|
||||
defer v.Purge()
|
||||
@@ -294,7 +308,6 @@ func TestIntegration_realBrowserRotation(t *testing.T) {
|
||||
// Point HostFor at our ephemeral host via the git-style "user @ host" identity.
|
||||
c.Identity = "acct @ " + host
|
||||
|
||||
drv := &RodDriver{Bin: bin, Headless: true, Timeout: 30 * time.Second}
|
||||
r := New(site, drv, pwgen.DefaultPolicy())
|
||||
|
||||
if !r.Detect(c) {
|
||||
@@ -320,6 +333,7 @@ func TestIntegration_realBrowserRotation(t *testing.T) {
|
||||
t.Fatalf("RevokeOld: %v", err)
|
||||
}
|
||||
t.Log("LIVE-VM browser rotation proof passed: discover→inject→submit→verify(new)→old-rejected")
|
||||
return r
|
||||
}
|
||||
|
||||
func hostForDebug(c discover.Credential) string { return links.HostFor(c) }
|
||||
|
||||
Reference in New Issue
Block a user