# v0.0.25 Release Plan
_Written: 2026-05-17 — Coordinator Phase 0_

## Scope Summary

Three targeted anti-detection fixes. No architecture changes. No browser swap.
Stays Camoufox only.

---

## Fix 1 — WebRTC mDNS obfuscation

**Acceptance test:** `browserleaks.com/webrtc` local IP field shows `<uuid>.local`,
not `192.168.x.x`.

**File touch surface:**
- `identity/profile.go` → `BuildCamoufoxConfig()`: add one key
  `"media.peerconnection.ice.obfuscate_host_addresses": true`
- `identity/identity_test.go`: assert the key is present in the returned map

**Why this works:** Modern Firefox defaults this pref to `true`. Camoufox's
curated preset evidently omits or resets it. Adding it to CAMOU_CONFIG ensures
Camoufox's Firefox instance reads it from the env-var-injected config at
startup. The CAMOU_CONFIG is the correct vehicle — this is exactly the pattern
used for `timezone`, `navigator.*`, `locale:*` etc.

**Caution from gotchas.md:**
- gotchas entry "CAMOU_CONFIG format was completely wrong" (2026-04-07): do NOT
  add individual env vars; use the flat JSON blob via `BuildCamoufoxConfig()`.
  Already the right approach here.
- gotchas entry "CAMOU_CONFIG over-specified" (2026-04-07): only set what we
  control. WebRTC obfuscation is something we WANT to force; this is appropriate.

**Implementation:** Trivial. One line in `BuildCamoufoxConfig()`, one assert in
tests. ~30 min.

---

## Fix 2 — Locale-policy decoupling (`WithLocalePolicy`)

**Acceptance test:** When `identity.Generate(identity.WithLocalePolicy(identity.LocalePolicyEnglishDefault), ...)` is called, the resulting profile has `Languages = ["en-US", "en"]` and `Locale = "en-US"` regardless of any `WithCountry("RU")` or `WithProxy("ru-ip")`. Default behavior (no `WithLocalePolicy`) is **unchanged**.

**Design (informed by gotchas.md:99-114 and gotchas.md:110-114 azuretls lesson):**

Do NOT expose `LocalePolicyProxyGeo` and `LocalePolicyEnglishDefault` as
independent setters that might interact poorly with the existing geo-resolution
path. Instead, the policy is a single enum that gates the final locale-assignment
block in `Generate()`, after `applyGeoToConfig()` has already resolved timezone
and geo coords. This means:

- `WithCountry("RU")` + `WithLocalePolicy(LocalePolicyEnglishDefault)` →
  locale=en-US, timezone=Europe/Moscow (kept), geo=RU coords (kept). The proxy
  geo is still consistent with the physical location; only the language is forced.
- This is the correct tradeoff: real user on a Russian IP who speaks English
  and is searching English content. Language preference != physical location.

**New types:**
```go
// LocalePolicy controls how the locale/languages fields are set relative to proxy geo.
type LocalePolicy int

const (
    LocalePolicyProxyGeo      LocalePolicy = iota // default: locale matches proxy IP geo
    LocalePolicyEnglishDefault                     // always en-US, regardless of proxy geo
)
```

**`WithLocalePolicy` option:** adds `localePolicy LocalePolicy` to `generateConfig`.

**`Generate()` change:** after the existing locale-assignment block, if
`cfg.localePolicy == LocalePolicyEnglishDefault && cfg.locale == ""`:
  - override `p.Locale` to `"en-US"`
  - override `p.Languages` to `["en-US", "en"]`

The `cfg.locale != ""` guard ensures that an explicit `WithLocale(...)` call still
wins — locale policy is the fallback, not an override of explicit user intent.

**File touch surface:**
- `identity/profile.go`: new `LocalePolicy` type + constants, add field to
  `generateConfig`, implement `WithLocalePolicy`, modify `Generate()` locale block
- `identity/identity_test.go`: tests for `LocalePolicyEnglishDefault` with
  `WithCountry("RU")`, without geo constraint, and with explicit `WithLocale`
  (last one should ignore the policy)

**ADR:** `.dev-squad/v0.0.25-adr-locale-policy.md`

**No public API breakage:** `LocalePolicy` is additive. Default
`LocalePolicyProxyGeo` (zero value) preserves current behavior exactly.

---

## Fix 3 — PerimeterX press-and-hold solver

**Acceptance test:** When Walmart's `px-captcha` element is detected in a page
response, foxhound logs "behavior: PressAndHold initiated" and the press-hold
gesture fires. Full bypass of PX depends on PX's pre-challenge ML score, but the
GESTURE must fire correctly — confirmed by inspecting the playwright interaction
log.

**Audit note:** Phase 3 log shows Camoufox gets `px-captcha` (17KB page = challenge
served). Phase 4 confirms the same. CloakBrowser bypasses the challenge entirely
(1.35MB real page content) — likely because its fingerprint scores high enough on
PX's pre-challenge filter. Solving the press-hold gesture is the best foxhound can
do natively; whether PX accepts the solution depends on fingerprint scoring which
is orthogonal to the gesture.

**File touch surface:**
- `captcha/detect.go`: add `CaptchaPerimeterX CaptchaType = "perimeter_x"`, add
  `isPerimeterX()` detector, wire into `Detect()` before the soft-block fallback
- `behavior/press_hold.go` (NEW): `PressAndHold(page playwright.Page, selector string, dur time.Duration, mouse *Mouse)` function. Uses `WeibullClamped` from `distributions.go` for hold duration (k=2.0, lambda=1.5, lo=0.8, hi=4.0). Uses `Mouse.MoveTo` for approach trajectory.
- `captcha/press_hold_solver.go` (NEW): `SolvePerimeterX(ctx context.Context, page playwright.Page)` — detects the press-hold button selector (several known variants: `#px-captcha-wrapper`, `#px-captcha`, `div.px-captcha-error-button`), calls `behavior.PressAndHold`. Build tag: `playwright`.
- `captcha/detect_test.go` / `captcha/detect_enhanced_test.go`: add tests for `isPerimeterX()` with known HTML snippets

**NopeCHA constraint:** NopeCHA path in `camoufox_playwright.go` is NOT touched.
The press-hold solver fires first (before NopeCHA token request). If press-hold
succeeds, the captcha clears and NopeCHA never fires. If press-hold fails, NopeCHA
fires as normal fallback.

**Wire-up:** The press-hold solver is invoked from `fetch/camoufox_playwright.go`
in the post-navigation captcha detection block — when `Detect()` returns
`CaptchaPerimeterX`, call `captcha.SolvePerimeterX(ctx, page)` before attempting
any token-based solver.

**Build tag consideration:** `behavior/press_hold.go` must NOT import playwright
(so it stays buildable without `playwright` tag). The playwright dependency lives
entirely in `captcha/press_hold_solver.go` (build tag: playwright). `behavior.PressAndHold`
takes abstract `x, y float64` target coordinates and returns `[]behavior.Point` +
timing slices — the actual playwright mouse API calls happen in
`captcha/press_hold_solver.go`.

---

## Sequencing Decision

Ship all three in v0.0.25 as one PR. Rationale:

1. Fix 1 is trivial (minutes), Fix 2 is small (hours), Fix 3 is the main work.
2. All three are self-contained — no interdependencies.
3. Splitting into two PRs adds reviewer latency with no quality benefit.

If Fix 3 takes >2 days of actual work (which is unlikely given the outline above),
escalate and consider splitting.

**Implementation order within the branch:**
1. Fix 1 (trivial, baseline for branch)
2. Fix 2 (ADR first, then implementation)
3. Fix 3 (most moving parts; TDD)

---

## File-Touch Map

| File | Change | Fix |
|------|--------|-----|
| `identity/profile.go` | `BuildCamoufoxConfig()` + `LocalePolicy` type + `WithLocalePolicy` + `Generate()` locale block | 1, 2 |
| `identity/identity_test.go` | mDNS pref assert + locale policy tests | 1, 2 |
| `.dev-squad/v0.0.25-adr-locale-policy.md` | ADR for locale policy design | 2 |
| `captcha/detect.go` | Add `CaptchaPerimeterX`, `isPerimeterX()`, wire into `Detect()` | 3 |
| `captcha/detect_test.go` | `isPerimeterX` HTML snippet tests | 3 |
| `behavior/press_hold.go` | NEW: `PressAndHold` — pure logic, no playwright import | 3 |
| `behavior/press_hold_test.go` | NEW: unit tests for timing distribution, trajectory | 3 |
| `captcha/press_hold_solver.go` | NEW (playwright build tag): `SolvePerimeterX` | 3 |
| `fetch/camoufox_playwright.go` | Wire `SolvePerimeterX` into captcha detection block | 3 |
| `CHANGELOG.md` | v0.0.25 entry | release |
| `README.md` | Version bump + `WithLocalePolicy` brief mention | release |
| `CLAUDE.md` | Status line v0.0.25 + `WithLocalePolicy` note | release |
| `.dev-squad/gotchas.md` | Append lessons per fix | release |

---

## Test Plan

**Unit tests (all buildable without playwright tag):**
- `identity/identity_test.go`: mDNS key present in `BuildCamoufoxConfig()` output
- `identity/identity_test.go`: `LocalePolicyEnglishDefault` forces en-US regardless of `WithCountry("RU")`
- `identity/identity_test.go`: explicit `WithLocale("ja-JP")` overrides policy
- `captcha/detect_test.go`: `isPerimeterX` detects known HTML snippet
- `behavior/press_hold_test.go`: duration samples are within [0.8, 4.0]s; trajectory has ≥2 points

**Integration (playwright tag):**
- `fetch/camoufox_playwright_test.go`: mock page with `px-captcha` element → `SolvePerimeterX` returns without error
- The audit harness `.dev-squad/cloakbrowser-audit/harness/audit.py phase2 --only webrtc` is available for manual re-check of Fix 1 (live browser test)

**Race detector:** `go test -tags playwright -race ./...` must pass green.

---

## Guardrail Checklist

- [ ] No CloakBrowser or competitor name in any project file
- [ ] NopeCHA files untouched (`captcha/nopecha/`, `captcha/nopecha.go`)
- [ ] Default `Generate()` behavior unchanged (locale policy default = ProxyGeo)
- [ ] No Chromium/Nightly browser support added
- [ ] Fix 1 goes through `BuildCamoufoxConfig()` (not raw env vars)
- [ ] Fix 2's `WithLocalePolicy` is documented as opt-in in CHANGELOG
- [ ] Fix 3's playwright path is build-tag-gated (`//go:build playwright`)
- [ ] CLAUDE.md anti-detection principle #6 documented as "RELAXED via opt-in, not violated by default"

---

## Phase Agent Assignments

| Phase | Agent | Task |
|-------|-------|------|
| 0 | Coordinator (me) | This plan |
| 1a | Architect | Fix 2 ADR (locale policy interface design) |
| 1b | Backend | Implement Fix 1 + Fix 2 + Fix 3 with TDD |
| 2 | Reviewer | Diff review: guardrails, NopeCHA untouched, security |
| 3a | QA-Engineer | Audit harness re-run phase2 webrtc + phase4 google/walmart |
| 3b | Auditor | `go test -race ./...`, staticcheck/gocyclo on new files |
| 4 | Git-Ops | Branch + PR + merge + tag + GitHub release |

Phases 3a and 3b are parallel.
