From e4323344ff907b55d0c929f0ef90124baf09f1cf Mon Sep 17 00:00:00 2001 From: neon798 <209852807+neon798@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:45:05 -0600 Subject: [PATCH 01/10] feat(M4): FPP-forward farbling posture + unlock dark mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch anti-fingerprinting from RFP (uniform, "stands out", per-read canvas randomization) to FPP (per-origin deterministic farbling), the roadmap's "convincing common fingerprint" posture. The three FP modes are mutually exclusive, so RFP must be off for FPP to run. assets/neonwolf.overrides.cfg: - privacy.resistFingerprinting=false, privacy.fingerprintingProtection=true, remoteOverrides.enabled=false. - fingerprintingProtection.overrides adds WebGL randomization, hw-concurrency pin, audio/font/timer protection on top of FPP's default canvas farbling. UA/screen uniform spoofs deliberately omitted (real-for-platform is more common); timezone left real (its JS override is RFP-pref-gated, and a timezone/IP mismatch is itself a tell). - webgl.disabled=false so WebGL is farbled (WebGL-off is a conspicuous tell). - layout.css.prefers-color-scheme.content-override=2 (auto). Verified locally (Marionette): canvas farbling deterministic per-origin (Brave-style), hardwareConcurrency pinned to 4, and DARK MODE unlocked (prefers-color-scheme honors the override under FPP; RFP was forcing light). NOT merged: per plan, ships only behind a full browserleaks/coveryourtracks + WebGL pass (needs a real GL context + network — headful), proving it's more convincing, not less protected. Co-Authored-By: Claude Opus 4.8 --- assets/neonwolf.overrides.cfg | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/assets/neonwolf.overrides.cfg b/assets/neonwolf.overrides.cfg index e4268ed..bde724d 100644 --- a/assets/neonwolf.overrides.cfg +++ b/assets/neonwolf.overrides.cfg @@ -108,13 +108,46 @@ defaultPref("browser.send_pings", false); // disable +NavigatorHWConcurrency, verified +// -> 4), and add audio/font/timer protection. UA/screen-geometry uniform spoofs +// are deliberately NOT added (real-for-platform is more "common" than a faked +// hardened value). TIMEZONE is intentionally left REAL: verified that the JS +// timezone override is gated on the resistFingerprinting *pref* (not the +// JSDateTimeUTC target), so it can't be re-homed onto FPP by override — and a +// spoofed timezone that mismatches IP/locale is itself a tell, so coherent real +// timezone is the more "convincing" choice. This set is a starting point — +// tuned against browserleaks/coveryourtracks. +defaultPref("privacy.resistFingerprinting", false); +defaultPref("privacy.fingerprintingProtection", true); +defaultPref("privacy.fingerprintingProtection.remoteOverrides.enabled", false); // Mozilla can't relax the posture per-site +defaultPref( + "privacy.fingerprintingProtection.overrides", + "+WebGLRandomization,+WebGLRenderInfo,+NavigatorHWConcurrency,+AudioContext,+AudioSampleRate,+FontVisibilityBaseSystem,+ReduceTimerPrecision" +); +defaultPref("layout.css.prefers-color-scheme.content-override", 2); // 2 = auto/follow-theme; guard so dark mode follows the synthwave chrome + // Neonwolf: the native content-classifier ad/tracker blocker is only invoked // via the deferred-annotation channel path; without this it never runs network // blocking. Required for native ClassifyForCancel to execute. From ee8bb766fd4e81af390be75bf45172ced6025870 Mon Sep 17 00:00:00 2001 From: neon798 <209852807+neon798@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:38:39 -0600 Subject: [PATCH 02/10] chore: git-ignore AI hybrid-workflow scratch files AI cowork.md (the Strong/Weak-AI playbook), GROK.md (the handoff artifact it prescribes), and docs/agents/ are local working scratch, never repo content. Co-Authored-By: Claude Fable 5 --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 1b5e822..abb3d59 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,10 @@ scripts/validate/.venv/ # Note: assets/adblock/*.txt and assets/ubo-resources.json ARE committed so the # native ad blocker builds self-contained in CI (gen-adblock-dump.py needs them # at `make dir` time; there is no networked list-fetch step in the release job). + +# --- AI hybrid-build workflow scratch (local only, never pushed) --- +# The Strong/Weak-AI playbook, the GROK.md handoff artifact it prescribes, and +# the helper-agent directions/handoffs are working scratch — not repo content. +AI cowork.md +GROK.md +docs/agents/ From 3119d1b73c06274f261e5e38c4488c2e6a1c2355 Mon Sep 17 00:00:00 2001 From: neon798 <209852807+neon798@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:12:02 -0600 Subject: [PATCH 03/10] fix(M4): actually allow WebGL so FPP can farble it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Empirical validation of the FPP posture found WebGL still dead in content: LibreWolf's webgl-permission.patch gates context creation behind a per-site 'webgl' permission (librewolf.webgl.prompt, compiled default TRUE) and ships the doorhanger hidden — silent deny everywhere, the exact same conspicuous tell webgl.disabled=false was meant to remove. Turn the gate off; WebGL now runs farbled (+WebGLRandomization per-eTLD+1 readback noise, verified deterministic per-origin/per-session; +WebGLRenderInfo spoofs Mozilla/Mozilla). Full headful validation (Marionette on Wayland, real network): canvas + WebGL farble per-eTLD+1, stable across reloads, re-keyed per session, plain control identical across domains with all tiers off; hwConcurrency pinned 4; timezone/screen/UA real; dark mode honored; CoverYourTracks verdict 'strong protection against Web tracking'. Known gap: audio readback is NOT farbled (FPP has no audio randomization target — RFP-only protection; follow-up). Co-Authored-By: Claude Fable 5 --- assets/neonwolf.overrides.cfg | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/assets/neonwolf.overrides.cfg b/assets/neonwolf.overrides.cfg index bde724d..397f4fb 100644 --- a/assets/neonwolf.overrides.cfg +++ b/assets/neonwolf.overrides.cfg @@ -109,6 +109,13 @@ defaultPref("network.http.referer.defaultPolicy", 0); // send no refe defaultPref("privacy.clearOnShutdown_v2.cookiesAndStorage", true); // actually clear cookies on shutdown defaultPref("dom.security.mixed_content.block_active_content", true); // belt-and-suspenders with HTTPS-only defaultPref("webgl.disabled", false); // re-enabled so FPP farbles it (+WebGLRandomization); WebGL-OFF is itself a conspicuous tell (see FPP section below) +// LibreWolf's webgl-permission.patch ALSO gates content WebGL behind a per-site +// "webgl" permission (librewolf.webgl.prompt, compiled default true) and ships +// with the doorhanger hidden (prompt.hide=true) — i.e. WebGL is SILENTLY DENIED +// everywhere, which is the exact same tell as webgl.disabled. Under the +// FPP-forward posture WebGL must actually run so +WebGLRandomization can farble +// it (empirically verified: context creation fails until this is off). +defaultPref("librewolf.webgl.prompt", false); defaultPref("device.sensors.enabled", false); // device-motion/orientation, fingerprint surface, no desktop use defaultPref("dom.battery.enabled", false); // battery-status API, fingerprint surface defaultPref("dom.gamepad.enabled", false); // gamepad API, fingerprint surface From 5f1359f21697f88ac7a9877a1398757c3984b50c Mon Sep 17 00:00:00 2001 From: neon798 <209852807+neon798@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:25:45 -0600 Subject: [PATCH 04/10] feat(M4/M5): Shields FPP integration, uBO-style picker selectors, subscriptions UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shields panel (M4 'integrate with Shields'): - fingerprinting toggle now drives privacy.fingerprintingProtection (the M4 posture pref), label 'Farble fingerprinting' - new per-site 'Farble on this site' subrow writing an -AllTargets entry to BOTH privacy.fingerprintingProtection.granularOverrides AND the baseline tier's granularOverrides (canvas farbling rides baseline FPP — verified empirically); foreign/webcompat entries are never touched, unreadable pref hides the row (all-or-nothing writes), PSL-hosted eTLD+1 edge documented Element picker (M5): real selector generator replacing the placeholder — unique #id fast path, bottom-up max-4-segment path with stable-class filtering (hashed CSS-module classes rejected), :nth-of-type disambiguation, document-uniqueness testing, simpleSelector fallback; pure DOM reads only. Lists page (M5): 'Subscribed lists' manager bound to neonwolf.shields.lists.subscriptions — https-only validation, dedupe, live pref observer, Remove per row, synthwave-styled via shared filters classes. Implemented by Grok (executor) from Claude's spec; line-by-line reviewed, staged into the built tree and Marionette-verified end-to-end (granular-pref roundtrip incl. corrupt-pref no-clobber, page render screenshot). Co-Authored-By: Claude Fable 5 --- .../neonwolf-element-picker-content.js | 160 ++++++++++++++++- .../base/content/neonwolf-shields-lists.js | 135 +++++++++++++- .../base/content/neonwolf-shields-lists.xhtml | 14 ++ .../base/content/neonwolf-shields-panel.js | 169 +++++++++++++++++- .../browser/base/content/neonwolf-shields.css | 6 + .../base/content/neonwolf-shields.xhtml | 7 +- 6 files changed, 481 insertions(+), 10 deletions(-) diff --git a/themes/browser/base/content/neonwolf-element-picker-content.js b/themes/browser/base/content/neonwolf-element-picker-content.js index d889c84..01449f8 100644 --- a/themes/browser/base/content/neonwolf-element-picker-content.js +++ b/themes/browser/base/content/neonwolf-element-picker-content.js @@ -7,9 +7,9 @@ /** * Neonwolf element picker frame script (content process). * - * Injected per pick session. Highlights hovered elements and builds a simple - * cosmetic filter candidate. Selector generation stays chrome-side of trust - * (never a page-provided hook); Claude wires a smarter generator later. + * Injected per pick session. Highlights hovered elements and builds a + * cosmetic filter candidate via a uBO-style selector generator. Selector + * generation stays chrome-side of trust (never a page-provided hook). */ (function () { if (content.window.__nwPickerSession) { @@ -19,6 +19,8 @@ const HIGHLIGHT_ID = "neonwolf-picker-highlight"; const PANEL_ID = "neonwolf-picker-panel"; const STYLE_ID = "neonwolf-picker-style"; + const STABLE_CLASS_RE = /^[A-Za-z_][A-Za-z0-9_-]{1,29}$/; + const MAX_PATH_SEGMENTS = 4; let active = false; let selectedElement = null; @@ -45,10 +47,156 @@ return tag; } + /** True iff sel matches exactly one node in the document (invalid → false). */ + function unique(sel) { + try { + return content.document.querySelectorAll(sel).length === 1; + } catch (e) { + return false; + } + } + + /** Stable classes: identifier-like, <3 digits (filters hashed CSS modules). Cap 2. */ + function stableClasses(el) { + let out = []; + if (!el.classList) { + return out; + } + for (let c of el.classList) { + if (!STABLE_CLASS_RE.test(c)) { + continue; + } + let digits = 0; + for (let i = 0; i < c.length; i++) { + let ch = c.charCodeAt(i); + if (ch >= 48 && ch <= 57) { + digits++; + } + } + if (digits >= 3) { + continue; + } + out.push(c); + if (out.length >= 2) { + break; + } + } + return out; + } + + /** :nth-of-type index: same-tag preceding siblings + 1. */ + function nthOfTypeIndex(el) { + let n = 1; + let tag = el.tagName; + for (let sib = el.previousElementSibling; sib; sib = sib.previousElementSibling) { + if (sib.tagName == tag) { + n++; + } + } + return n; + } + + /** + * True if more than one same-parent sibling matches `segment` (tag+classes). + * Only same-tag siblings are considered for ambiguity. + */ + function segmentAmbiguousAmongSiblings(el, segment) { + let parent = el.parentElement; + if (!parent) { + return false; + } + let matches = 0; + let tag = el.tagName; + for ( + let child = parent.firstElementChild; + child; + child = child.nextElementSibling + ) { + if (child.tagName != tag) { + continue; + } + try { + if (child.matches(segment)) { + matches++; + if (matches > 1) { + return true; + } + } + } catch (e) { + // invalid segment — treat as non-ambiguous + } + } + return false; + } + + /** + * Build one path segment for `el`. Returns `{ segment, stopClimb }`. + * Unique id → `tag#id` and stop climbing; else tag + stable classes + + * optional :nth-of-type when ambiguous among siblings. + */ + function pathSegment(el) { + let tag = el.tagName.toLowerCase(); + if (el.id) { + let idCandidate = "#" + cssEscape(el.id); + if (unique(idCandidate)) { + return { segment: tag + "#" + cssEscape(el.id), stopClimb: true }; + } + } + + let segment = tag; + for (let c of stableClasses(el)) { + segment += "." + cssEscape(c); + } + if (segmentAmbiguousAmongSiblings(el, segment)) { + segment += ":nth-of-type(" + nthOfTypeIndex(el) + ")"; + } + return { segment, stopClimb: false }; + } + + /** + * uBO-style selector generator. Pure DOM reads only — never call page + * functions or hostile content.window hooks. + */ function generateSelector(el) { - // Selector generation stays chrome-side of trust: never defer to a - // page-defined hook (content.window is attacker-controlled). Claude wires a - // smarter generator later. + if (!el || el.nodeType != 1) { + return ""; + } + + // Prefer bare #id when unique in the document. + if (el.id) { + let idSel = "#" + cssEscape(el.id); + if (unique(idSel)) { + return idSel; + } + } + + // Bottom-up descendant path, max 4 segments, joined with " > ". + let parts = []; + let node = el; + for (let depth = 0; node && node.nodeType == 1 && depth < MAX_PATH_SEGMENTS; depth++) { + let { segment, stopClimb } = pathSegment(node); + parts.unshift(segment); + let sel = parts.join(" > "); + if (unique(sel)) { + return sel; + } + if (stopClimb) { + break; + } + node = node.parentElement; + } + + // Deepest path if it still matches the pick; else simple fallback. + let deepest = parts.join(" > "); + if (deepest) { + try { + if (el.matches(deepest)) { + return deepest; + } + } catch (e) { + // fall through to simpleSelector + } + } return simpleSelector(el); } diff --git a/themes/browser/base/content/neonwolf-shields-lists.js b/themes/browser/base/content/neonwolf-shields-lists.js index e0785b4..14fcb8f 100644 --- a/themes/browser/base/content/neonwolf-shields-lists.js +++ b/themes/browser/base/content/neonwolf-shields-lists.js @@ -11,6 +11,7 @@ var gNeonwolfShieldsLists = { PREF_LAST_REFRESH: "neonwolf.shields.lists.lastRefresh", PREF_AUTO_REFRESH: "neonwolf.shields.lists.autoRefresh", PREF_REFRESH_NOW: "neonwolf.shields.lists.refreshNow", + PREF_SUBSCRIPTIONS: "neonwolf.shields.lists.subscriptions", OBS_BRANCH: "neonwolf.shields.lists.", BUNDLED_LISTS: [ @@ -49,6 +50,25 @@ var gNeonwolfShieldsLists = { )); }, + get _subRows() { + delete this._subRows; + return (this._subRows = document.getElementById("neonwolf-lists-sub-rows")); + }, + + get _subInput() { + delete this._subInput; + return (this._subInput = document.getElementById( + "neonwolf-lists-sub-input" + )); + }, + + get _subError() { + delete this._subError; + return (this._subError = document.getElementById( + "neonwolf-lists-sub-error" + )); + }, + _getLastRefresh() { return Services.prefs.getIntPref(this.PREF_LAST_REFRESH, 0); }, @@ -61,6 +81,110 @@ var gNeonwolfShieldsLists = { return new Date(seconds * 1000).toLocaleString(); }, + _loadSubscriptions() { + return Services.prefs + .getStringPref(this.PREF_SUBSCRIPTIONS, "") + .split("\n") + .map(line => line.trim()) + .filter(Boolean); + }, + + _saveSubscriptions(urls) { + Services.prefs.setStringPref(this.PREF_SUBSCRIPTIONS, urls.join("\n")); + }, + + _setSubError(msg) { + let err = this._subError; + if (!err) { + return; + } + err.setAttribute("value", msg || ""); + }, + + _renderSubscriptions() { + let container = this._subRows; + if (!container) { + return; + } + container.textContent = ""; + let urls = this._loadSubscriptions(); + if (!urls.length) { + let empty = document.createXULElement("label"); + empty.className = "neonwolf-filters-subscribe-empty"; + empty.setAttribute("value", "No subscribed lists."); + container.appendChild(empty); + return; + } + for (let url of urls) { + let row = document.createXULElement("hbox"); + row.className = "neonwolf-lists-row"; + row.setAttribute("align", "center"); + + // textContent (not innerHTML) — URL is user/pref-derived. + let urlLabel = document.createXULElement("label"); + urlLabel.className = "neonwolf-filters-subscribe-item"; + urlLabel.setAttribute("flex", "1"); + urlLabel.setAttribute("crop", "end"); + urlLabel.setAttribute("value", url); + row.appendChild(urlLabel); + + let removeBtn = document.createXULElement("button"); + removeBtn.className = "neonwolf-lists-btn"; + removeBtn.setAttribute("label", "Remove"); + removeBtn.addEventListener("command", () => + this._onSubscribeRemove(url) + ); + row.appendChild(removeBtn); + + container.appendChild(row); + } + }, + + _onSubscribeRemove(url) { + let urls = this._loadSubscriptions().filter(u => u !== url); + this._saveSubscriptions(urls); + this._setSubError(""); + this._renderSubscriptions(); + }, + + _onSubscribeAdd() { + let input = this._subInput; + if (!input) { + return; + } + let raw = input.value.trim(); + this._setSubError(""); + + if (!raw) { + this._setSubError("Enter a URL."); + return; + } + + let parsed; + try { + parsed = new URL(raw); + } catch (e) { + this._setSubError("Invalid URL."); + return; + } + + if (parsed.protocol !== "https:") { + this._setSubError("Only https:// URLs are allowed."); + return; + } + + let urls = this._loadSubscriptions(); + if (urls.includes(raw)) { + this._setSubError("Already subscribed."); + return; + } + + urls.push(raw); + this._saveSubscriptions(urls); + input.value = ""; + this._renderSubscriptions(); + }, + _scheduleRender() { if (this._renderPending) { return; @@ -147,6 +271,9 @@ var gNeonwolfShieldsLists = { ) { this._scheduleRender(); } + if (aPrefName == this.PREF_SUBSCRIPTIONS) { + this._renderSubscriptions(); + } }, _onUnload() { @@ -171,14 +298,20 @@ var gNeonwolfShieldsLists = { refreshBtn.addEventListener("command", () => this._onRefreshNow()); } + let subAdd = document.getElementById("neonwolf-lists-sub-add"); + if (subAdd) { + subAdd.addEventListener("command", () => this._onSubscribeAdd()); + } + window.addEventListener("unload", () => this._onUnload(), { once: true }); this._lastRefreshSeen = this._getLastRefresh(); this._syncAutoRefresh(); this._render(); + this._renderSubscriptions(); }, }; window.addEventListener("load", () => gNeonwolfShieldsLists.init(), { once: true, -}); \ No newline at end of file +}); diff --git a/themes/browser/base/content/neonwolf-shields-lists.xhtml b/themes/browser/base/content/neonwolf-shields-lists.xhtml index a03fa2a..b7ae3dd 100644 --- a/themes/browser/base/content/neonwolf-shields-lists.xhtml +++ b/themes/browser/base/content/neonwolf-shields-lists.xhtml @@ -42,6 +42,20 @@ + + +