Skip to content

feat(bin): add fm-firefox isolated Firefox/Marionette session owner - #2025

Closed
AbdullahPesteli wants to merge 2 commits into
kunchenguid:mainfrom
AbdullahPesteli:fm/ship-firstmate-firefox-marionette-tool
Closed

AbdullahPesteli wants to merge 2 commits into
kunchenguid:mainfrom
AbdullahPesteli:fm/ship-firstmate-firefox-marionette-tool

Conversation

@AbdullahPesteli

Copy link
Copy Markdown

Intent

Productize the already live-proven Firefox web-ext + Marionette/WebDriver route as a reusable, harness-agnostic Firstmate tool plus a just-in-time agent skill. The captain explicitly authorized adding this capability to the shared tool set.

Deliverable: one new shell CLI bin/fm-firefox.sh that owns the lifecycle of a single isolated, disposable Firefox process driven over Marionette. Deliberate architecture decisions (not mistakes): (1) chose a thin harness-agnostic shell CLI over a new MCP server, AXI daemon, or global service, because the tool-layer-placement scout established that harness-agnostic CLI surfaces take precedence and every Pi worker already inherits the same MCP config, so a daemon would add a second browser control plane with no reach benefit; (2) the single supported install route is Marionette Addon:Install(temporary) so the tool can own exactly one -no-remote Firefox process it launched, rather than delegating launch to web-ext (which spawns its own process); web-ext is intentionally only reported by doctor as an optional, unmanaged alternative, not used; (3) the Marionette client is a small raw-protocol python3 implementation embedded in the CLI, so the tool's only hard deps are a Firefox binary and python3 (no marionette_driver dependency); marionette_driver is used only by the opt-in live test as an independent verification client.

Safety contract implemented: always create a fresh, empty, task-owned profile (never attach to, copy, or enumerate a live Firefox/Zen profile); self-allocate a loopback Marionette endpoint and refuse the shared default 127.0.0.1:2828; launch exactly one -no-remote Firefox; apply -remote-allow-system-access only to that disposable process and only when --system-access is requested; expose truthful machine-readable JSON receipts; and on stop tear down only the process/profile/port it created after proving the recorded PID is still our Firefox by its profile path, refusing ambiguous PID/port/profile ownership rather than guessing or killing an unrelated process.

Cookie/privacy scope is deliberately truthful, not a guarantee the tool can make: the CLI proves its own Firefox profile is fresh and task-owned but states in its receipt/help that a project-specific native messaging host can still independently discover unrelated Gecko cookie profiles (e.g. Zen) unless the project supplies its own verified cookie-off contract. The tool provides a generic caller env/cookie-off hook (--env, --cookie-off KEY=VALUE) that it forwards into the launched process without interpreting it, recording only the keys (never values) as a caller-asserted contract, so no MediaGrabber business logic is baked into Firstmate. Constraints honored: does not depend on the still-open MediaGrabber PRs #8/#9; never claims generic browser isolation controls project native-host cookie behavior; does not modify any global Pi/MCP configuration; the tool is invoked purely through the shell and is harness-independent.

Also added: an agent-only .agents/skills/firefox-marionette skill (with one one-line AGENTS.md section 13 load trigger) teaching when to choose this route over Playwright/CDP/FoxMCP, the browser-vs-OS boundary, the no-live-profile safety contract, dependency checks, receipts, cleanup, and the native-host cookie caveat, pointing to the CLI --help rather than duplicating flags; operator documentation at docs/firefox-marionette.md and current verification evidence at docs/verification/firefox-marionette.md, both registered in docs/documentation-audiences.json, plus a docs/scripts.md toolbelt row; and registration of both new tests in bin/fm-test-run.sh (hermetic in pure-contract-unit, live in live-harness-optin) with shard weight hints. Two tests were added: a hermetic command-surface test (tests/fm-firefox-marionette.test.sh) that drives a fake Firefox speaking the real Marionette wire format and asserts observed behavior and receipt fields only (never implementation source bytes) - covering dependency failure, clean-profile creation, loopback allocation, ownership refusal, extension-path validation, truthful receipts, exact cleanup, interruption/error cleanup, and the native-host cookie caveat/caller-hook contract; and an opt-in live test (tests/fm-firefox-marionette-live-e2e.test.sh, gated by FM_FIREFOX_LIVE_E2E=1) that uses a synthetic local extension and a loopback HTTP page (no external URL) to prove real add-on install plus web-content and extension/browser-context control without pixel clicks, verifies no existing Firefox/Zen process or the shared endpoint changed, and leaves no Firefox child/profile/port behind, self-skipping when Firefox/python3/WebDriver deps are absent so standard CI stays hermetic. The live proof was run on this Mac against Firefox 153.0.3.

What Changed

  • Added bin/fm-firefox.sh, a harness-agnostic shell CLI that owns the full lifecycle of a single isolated, disposable Firefox driven over Marionette: it always creates a fresh task-owned profile, self-allocates a loopback Marionette endpoint (refusing the shared 127.0.0.1:2828), launches exactly one -no-remote process, installs the addon via a small embedded raw-protocol python3 Marionette client (so hard deps are only a Firefox binary + python3), exposes truthful JSON receipts/doctor output, forwards an uninterpreted --env/--cookie-off caller hook (recording keys only), and on stop tears down only the process/profile/port it created after verifying the recorded PID is still our Firefox by profile path. A start-time INT/TERM/EXIT trap kills the launched pid and removes the fresh profile if interrupted before the session state file is committed, closing the orphaned-process/profile window.
  • Added a just-in-time .agents/skills/firefox-marionette agent skill (with an AGENTS.md section 13 load trigger), operator docs at docs/firefox-marionette.md, verification evidence at docs/verification/firefox-marionette.md, both registered in docs/documentation-audiences.json, and a docs/scripts.md toolbelt row.
  • Added two tests registered in bin/fm-test-run.sh: a hermetic command-surface test (tests/fm-firefox-marionette.test.sh) that drives a fake Firefox speaking the real Marionette wire format and asserts observed behavior plus receipt fields (dependency failure, clean-profile creation, loopback allocation, ownership refusal, extension-path validation, exact/interrupted cleanup, native-host cookie caveat), and an opt-in live e2e test (tests/fm-firefox-marionette-live-e2e.test.sh, gated by FM_FIREFOX_LIVE_E2E=1) that proves real add-on install and web/extension-context control against a loopback page, verifies no existing process or the shared endpoint changed, and self-skips when deps are absent so standard CI stays hermetic.

Risk Assessment

✅ Low: Artımlı değişiklik, Round 1'deki tek uyarıyı (interrupt-penceresi süreç/profil sızıntısı) doğru bir INT/TERM/ERR trap + in-flight teardown ile kapatıyor; ampirik olarak trap'in sinyalde tetiklendiği, lokalleri dinamik kapsamla gördüğü ve açık hata yollarını bozmadığı doğrulandı, ayrıca doğrudan bir hermetik test eklendi.

Testing

Ran the smallest intent-relevant tests: the hermetic fm-firefox-marionette test (14 checks, real Marionette wire protocol against a fake Firefox — dependency refusal, clean owned profile, loopback allocation + 2828 refusal, truthful receipts, ownership-proven refusal, extension-path validation, exact cleanup, interruption/error/SIGTERM teardown, native-host cookie caveat) and the opt-in live e2e against the real Firefox 153.0.3 named in the intent (real temporary add-on install, web-content + extension-context control over Marionette with no pixel clicks, proof that no unrelated Firefox/Zen process or the shared 2828 endpoint changed, and zero leftovers). Both exit 0. The doctor JSON surface was exercised directly and reports honestly. This is a shell CLI with no rendered UI surface, so product-level evidence is the live-e2e CLI transcript and JSON receipts rather than a screenshot. No transient artifacts remained (default state root empty, worktree clean).

Evidence: Live e2e transcript vs real Firefox 153.0.3

ok - fm-firefox live E2E (Mozilla Firefox 153.0.3): real add-on install, web-content and extension-context control, no unrelated browser touched, no leftovers

ok - fm-firefox live E2E (Mozilla Firefox 153.0.3): real add-on install, web-content and extension-context control, no unrelated browser touched, no leftovers
Evidence: Hermetic command-surface test output (14 checks)
ok - dependency failure is diagnosed and refused
ok - start creates a clean owned profile, avoids 2828, and emits a truthful receipt
ok - cookie caveat is truthful and the caller cookie-off contract is recorded by key only
ok - stop removes exactly the profile, state, and process it created
ok - extension is optional and absent cookie contract is reported honestly
ok - extension paths are validated before any launch
ok - the shared 127.0.0.1:2828 endpoint is refused
ok - ambiguous pid ownership is refused instead of guessing
ok - a profile path outside the state root is never removed
ok - a Firefox that dies before readiness is cleaned up with no leftovers
ok - a Firefox that never opens the endpoint is killed and cleaned up
ok - an extension install failure is cleaned up with no leftovers
ok - a SIGTERM during the readiness window orphans no process and leaves no profile or state
ok - fm-firefox.sh hermetic command-surface behavior
Evidence: doctor --json product surface
{"firefox":{"path":"/Applications/Firefox.app/Contents/MacOS/firefox","required":true,"resolved":true},"install_route":"marionette:Addon:Install(temporary)","ok":true,"platform_boundary":"macOS and Linux supported; Windows unsupported","python3":{"required":true,"resolved":true},"schema":"fm-firefox.doctor.v1","tool":"fm-firefox","web_ext":{"note":"optional, unmanaged alternative install route; not used by this tool","required":false,"resolved":true,"version":"10.1.0"}}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ bin/fm-firefox.sh:477 - start komutunda sinyal (SIGTERM) kesinti penceresi için temizlik yok: 477. satırda arka planda (disown edilmiş) Firefox başlatıldıktan sonra, 518. satırda state dosyası yazılana kadar (--timeout kadar, varsayılan 45sn readiness + extension install süresi boyunca) script'te hiçbir trap yok. Bu pencerede fm-firefox.sh PID'ine SIGTERM gelirse (ör. çağıran fleet worker'ını sonlandıran bir supervisor — bu araç tam olarak fleet worker'ları tarafından çağrılmak üzere tasarlı), script ölür ama disown edilmiş Firefox yetim kalır ve profil dizini state root altında kalır; state dosyası hiç yazılmadığı için 'stop' bunu asla geri alamaz. Bu, aracın merkezi 'disposable / no-leftovers / owned-only' garantisini interrupt yolunda ihlal eder. Hermetik test 8c/8d/8e yalnızca Firefox-tarafı hata yollarını kapsıyor, gerçek bir sinyal kesintisini değil. Öneri: start süresince aktif bir INT/TERM/EXIT trap ekleyip, session commit edilene (state dosyası yazılana) kadar başlatılan pid'i öldürüp taze profili silen bir in-flight cleanup ekleyin. Not: Ctrl-C (process group'a SIGINT) senaryosunda Firefox aynı pgroup'ta olduğu için zaten ölür; boşluk yalnızca script PID'ine doğrudan SIGTERM gönderilen durumdadır.

🔧 Fix: trap interrupted fm-firefox start to prevent orphaned process/profile
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-firefox-marionette.test.sh — hermetic command-surface, 14/14 checks pass, exit 0
  • FM_FIREFOX_LIVE_E2E=1 bash tests/fm-firefox-marionette-live-e2e.test.sh — live proof vs real Firefox 153.0.3, exit 0
  • bin/fm-firefox.sh doctor and doctor --json — truthful dependency/receipt product surface (firefox+python3 resolved, web-ext optional/unused)
  • Manual before/after browser snapshot: pre-existing Zen (109 procs) and Firefox pid 890 untouched; no fm-firefox state/profile/port leftovers; worktree clean
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Firstmate and others added 2 commits August 9, 2026 20:59
Productize the proven web-ext + Marionette route as a harness-agnostic CLI
that owns exactly one isolated, disposable Firefox: a fresh task-owned profile,
a self-allocated loopback Marionette endpoint that refuses the shared 2828, a
single -no-remote launch, temporary add-on install over the supported Mozilla
route, truthful machine-readable receipts, and cleanup that tears down only what
it created and refuses ambiguous PID/port/profile ownership.

Cookie scope stays truthful: the tool isolates only its own Firefox profile and
states that a project native host can still discover unrelated Gecko cookie
profiles unless the project supplies its own verified cookie-off contract,
forwarded through a generic env/cookie-off hook with no project business logic
baked in.

Add the agent-only firefox-marionette skill and its AGENTS.md section 13
trigger, operator and verification docs, a hermetic command-surface test driving
a fake Firefox that speaks the real Marionette wire format, and an opt-in live
proof that self-skips when Firefox, python3, or a WebDriver client are absent so
standard CI stays hermetic.
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: closing this as stale. It has been waiting on a contributor update for 14+ days with no author push or comment. Reopen if you want to pick it back up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants