Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/src/content/docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ set of reserved keys is also accepted for non-rule build-time toggles:
[Debug trace](/agent-browser-shield/debug-trace/) for the retrieval recipes
and event schema.

- `placeholderAdaptivePalette` (boolean, default **off**, experimental) — sample
each placeholder's ancestor backgrounds at insert time and pick a light or
dark stripe palette so redactions on dark-themed pages don't flare against the
page chrome. Off by default while the visual heuristic is still being tuned;
the toggle is also surfaced in the Options page under the *Placeholder
display* section so humans can flip it without rebuilding. Enable for
deployments on consistently dark UIs.

The file may be partial; rules not listed keep the committed default. Unknown
keys (neither a registered rule id nor a reserved key) and non-boolean values
fail the build with a message naming them.
Expand Down
9 changes: 8 additions & 1 deletion extension/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ async function build(): Promise<void> {
Object.keys(overrides.rules).length +
(overrides.optionsButton === undefined ? 0 : 1) +
(overrides.runOnInactiveTabs === undefined ? 0 : 1) +
(overrides.debugTrace === undefined ? 0 : 1);
(overrides.debugTrace === undefined ? 0 : 1) +
(overrides.placeholderAdaptivePalette === undefined ? 0 : 1);
console.log(
`Applying ${changed} build-time default override(s) from ${defaultsPath}.`,
);
Expand Down Expand Up @@ -184,6 +185,12 @@ async function build(): Promise<void> {
"process.env.EXTENSION_DEBUG_TRACE_DEFAULT": JSON.stringify(
overrides.debugTrace === undefined ? "" : String(overrides.debugTrace),
),
"process.env.EXTENSION_PLACEHOLDER_ADAPTIVE_PALETTE_DEFAULT":
JSON.stringify(
overrides.placeholderAdaptivePalette === undefined
? ""
: String(overrides.placeholderAdaptivePalette),
),
},
});

Expand Down
3 changes: 2 additions & 1 deletion extension/data/defaults-overrides.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@

"optionsButton": true,
"runOnInactiveTabs": false,
"debugTrace": false
"debugTrace": false,
"placeholderAdaptivePalette": false
}
36 changes: 36 additions & 0 deletions extension/scripts/__tests__/load-default-overrides.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,42 @@ describe("loadDefaultOverrides", () => {
).toThrow(/non-boolean values for: debugTrace/);
});

it("extracts the placeholderAdaptivePalette reserved key alongside rules", () => {
const file = writeFile(
"with-adaptive-palette.json",
JSON.stringify({ "pii-redact": true, placeholderAdaptivePalette: true }),
);
expect(
loadDefaultOverrides({ path: file, knownRuleIds: KNOWN_IDS }),
).toEqual({
rules: { "pii-redact": true },
placeholderAdaptivePalette: true,
});
});

it("accepts placeholderAdaptivePalette on its own", () => {
const file = writeFile(
"only-adaptive-palette.json",
JSON.stringify({ placeholderAdaptivePalette: false }),
);
expect(
loadDefaultOverrides({ path: file, knownRuleIds: KNOWN_IDS }),
).toEqual({
rules: {},
placeholderAdaptivePalette: false,
});
});

it("rejects a non-boolean placeholderAdaptivePalette value", () => {
const file = writeFile(
"bad-adaptive-palette.json",
JSON.stringify({ placeholderAdaptivePalette: "on" }),
);
expect(() =>
loadDefaultOverrides({ path: file, knownRuleIds: KNOWN_IDS }),
).toThrow(/non-boolean values for: placeholderAdaptivePalette/);
});

it("throws when the file does not exist", () => {
expect(() =>
loadDefaultOverrides({
Expand Down
6 changes: 6 additions & 0 deletions extension/scripts/load-default-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface DefaultOverrides {
optionsButton?: boolean;
runOnInactiveTabs?: boolean;
debugTrace?: boolean;
placeholderAdaptivePalette?: boolean;
}

// Reserved top-level keys are not rule ids; the loader maps each one to a
Expand All @@ -35,6 +36,7 @@ const RESERVED_KEYS = new Set<string>([
"optionsButton",
"runOnInactiveTabs",
"debugTrace",
"placeholderAdaptivePalette",
]);

export function loadDefaultOverrides(
Expand Down Expand Up @@ -95,6 +97,10 @@ export function loadDefaultOverrides(
result.debugTrace = value;
break;
}
case "placeholderAdaptivePalette": {
result.placeholderAdaptivePalette = value;
break;
}
}
continue;
}
Expand Down
163 changes: 163 additions & 0 deletions extension/src/lib/__tests__/placeholder-adaptive-palette.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright (c) 2026 PixieBrix, Inc.
// Licensed under PolyForm Shield 1.0.0 — see LICENSE.

// Adaptive-palette experimental toggle: classifier walks the ancestor chain
// for a non-transparent background, and `replaceWithBlockPlaceholder` /
// `replaceMatchesInTextNode` stamp `data-abs-placeholder-palette="dark"`
// only when the cache is on AND the sample classifies as dark.

import { PLACEHOLDER_PALETTE_ATTR } from "../dom-markers";
import {
PLACEHOLDER_CLASS,
pickPaletteFromAncestor,
replaceMatchesInTextNode,
replaceWithBlockPlaceholder,
} from "../placeholder";
import { setAdaptivePaletteCache } from "../placeholder-adaptive-palette";
import type { RuleId } from "../storage";

const RULE_ID = "footer-redact" as RuleId;
const PII_RULE_ID = "pii-redact" as RuleId;

beforeEach(() => {
document.body.innerHTML = "";
document.body.removeAttribute("style");
// Default to off so the cache state of one test doesn't leak into the
// next. Each test that needs the toggle on flips it explicitly.
setAdaptivePaletteCache(false);
});

describe("pickPaletteFromAncestor", () => {
it("returns 'light' when the nearest non-transparent background is bright", () => {
document.body.innerHTML = `<div id="bg" style="background-color: rgb(255, 255, 255)"><span id="target">x</span></div>`;
const target = document.querySelector<HTMLElement>("#target");
if (!target) {
throw new Error("fixture missing #target");
}
expect(pickPaletteFromAncestor(target)).toBe("light");
});

it("returns 'dark' when the nearest non-transparent background is dark", () => {
document.body.innerHTML = `<div id="bg" style="background-color: rgb(13, 17, 23)"><span id="target">x</span></div>`;
const target = document.querySelector<HTMLElement>("#target");
if (!target) {
throw new Error("fixture missing #target");
}
expect(pickPaletteFromAncestor(target)).toBe("dark");
});

it("walks past transparent ancestors to the first opaque background", () => {
document.body.setAttribute("style", "background-color: rgb(13, 17, 23)");
document.body.innerHTML = `<div><section><span id="target">x</span></section></div>`;
const target = document.querySelector<HTMLElement>("#target");
if (!target) {
throw new Error("fixture missing #target");
}
// The intermediate divs/sections have no background; only <body> does.
expect(pickPaletteFromAncestor(target)).toBe("dark");
});

it("falls back to 'light' when every ancestor is transparent", () => {
document.body.innerHTML = `<div><span id="target">x</span></div>`;
const target = document.querySelector<HTMLElement>("#target");
if (!target) {
throw new Error("fixture missing #target");
}
expect(pickPaletteFromAncestor(target)).toBe("light");
});

it("returns 'light' when start is null", () => {
expect(pickPaletteFromAncestor(null)).toBe("light");
});
});

describe("replaceWithBlockPlaceholder palette stamping", () => {
it("does not stamp the palette attribute when the toggle is off", () => {
document.body.setAttribute("style", "background-color: rgb(13, 17, 23)");
document.body.innerHTML = `<section id="target">x</section>`;
const target = document.querySelector<HTMLElement>("#target");
if (!target) {
throw new Error("fixture missing #target");
}

const placeholder = replaceWithBlockPlaceholder(
target,
RULE_ID,
"[hidden]",
);
expect(placeholder.hasAttribute(PLACEHOLDER_PALETTE_ATTR)).toBe(false);
});

it("stamps palette='dark' when toggle on and ancestor sampled dark", () => {
setAdaptivePaletteCache(true);
document.body.setAttribute("style", "background-color: rgb(13, 17, 23)");
document.body.innerHTML = `<section id="target">x</section>`;
const target = document.querySelector<HTMLElement>("#target");
if (!target) {
throw new Error("fixture missing #target");
}

const placeholder = replaceWithBlockPlaceholder(
target,
RULE_ID,
"[hidden]",
);
expect(placeholder.getAttribute(PLACEHOLDER_PALETTE_ATTR)).toBe("dark");
});

it("omits the attribute when toggle on but ancestor sampled light", () => {
setAdaptivePaletteCache(true);
document.body.setAttribute("style", "background-color: rgb(255, 255, 255)");
document.body.innerHTML = `<section id="target">x</section>`;
const target = document.querySelector<HTMLElement>("#target");
if (!target) {
throw new Error("fixture missing #target");
}

const placeholder = replaceWithBlockPlaceholder(
target,
RULE_ID,
"[hidden]",
);
expect(placeholder.hasAttribute(PLACEHOLDER_PALETTE_ATTR)).toBe(false);
});
});

describe("inline placeholder palette stamping", () => {
it("stamps palette='dark' on inline placeholders when ancestor is dark", () => {
setAdaptivePaletteCache(true);
document.body.innerHTML = `<p id="host" style="background-color: rgb(13, 17, 23)">hello world</p>`;
const host = document.querySelector<HTMLElement>("#host");
if (!host) {
throw new Error("fixture missing #host");
}
const textNode = host.firstChild as Text;

replaceMatchesInTextNode(
textNode,
[{ start: 0, end: 5, label: "[hidden]" }],
PII_RULE_ID,
);

const placeholder = host.querySelector(`.${PLACEHOLDER_CLASS}--inline`);
expect(placeholder?.getAttribute(PLACEHOLDER_PALETTE_ATTR)).toBe("dark");
});

it("omits the attribute on inline placeholders when toggle is off", () => {
document.body.innerHTML = `<p id="host" style="background-color: rgb(13, 17, 23)">hello world</p>`;
const host = document.querySelector<HTMLElement>("#host");
if (!host) {
throw new Error("fixture missing #host");
}
const textNode = host.firstChild as Text;

replaceMatchesInTextNode(
textNode,
[{ start: 0, end: 5, label: "[hidden]" }],
PII_RULE_ID,
);

const placeholder = host.querySelector(`.${PLACEHOLDER_CLASS}--inline`);
expect(placeholder?.hasAttribute(PLACEHOLDER_PALETTE_ATTR)).toBe(false);
});
});
7 changes: 7 additions & 0 deletions extension/src/lib/dom-markers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export const HIDDEN_ATTR = "data-abs-hidden";
// re-render against the right shape.
export const PLACEHOLDER_MODE_ATTR = "data-abs-placeholder-mode";

// Stamped by `placeholder.ts` on each placeholder when the experimental
// adaptive-palette toggle is on and the surrounding background sampled as
// dark. The placeholder stylesheet reads it as `[data-abs-placeholder-
// palette="dark"]` to swap the stripe / chip palette. Absent when the toggle
// is off or the background sampled light.
export const PLACEHOLDER_PALETTE_ATTR = "data-abs-placeholder-palette";

// Per-rule — set by exactly one rule's `apply` and read by its watcher /
// teardown. Add new entries here when a new rule needs an attribute marker.

Expand Down
50 changes: 50 additions & 0 deletions extension/src/lib/placeholder-adaptive-palette.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2026 PixieBrix, Inc.
// Licensed under PolyForm Shield 1.0.0 — see LICENSE.

// Experimental: when enabled, each placeholder samples the background of its
// ancestor chain at insert time and picks a light- or dark-tuned palette so
// the redaction stripes don't clash with the surrounding page (e.g. a
// dark-mode GitHub page where the default light stripes flare). Default off
// while the visual heuristic is still being tuned. Operators can preflip it
// per deployment via the build-time defaults file.
//
// Placeholder creation has to know the toggle's value synchronously, so this
// module also exposes a module-local cache initialised from storage in
// `rule-engine.start()`. Reading from a cache rather than awaiting storage
// per-placeholder keeps the redaction path off the chrome.storage round
// trip.

import { createChromeStorageValue } from "./chrome-storage-value";

export const PLACEHOLDER_ADAPTIVE_PALETTE_DEFAULT = false;

// `process.env.EXTENSION_PLACEHOLDER_ADAPTIVE_PALETTE_DEFAULT` is substituted
// by build.ts when the operator passes a defaults file with a
// `placeholderAdaptivePalette` field. Literal `"true"` / `"false"` forces a
// value; empty string falls back to the committed default above.
function resolveDefault(): boolean {
const raw = process.env.EXTENSION_PLACEHOLDER_ADAPTIVE_PALETTE_DEFAULT;
if (raw === "true") {
return true;
}
if (raw === "false") {
return false;
}
return PLACEHOLDER_ADAPTIVE_PALETTE_DEFAULT;
}

export const placeholderAdaptivePaletteStorage =
createChromeStorageValue<boolean>({
key: "agent-browser-shield.placeholder-adaptive-palette",
defaultValue: resolveDefault(),
});

let cachedEnabled = resolveDefault();

export function isAdaptivePaletteEnabled(): boolean {
return cachedEnabled;
}

export function setAdaptivePaletteCache(value: boolean): void {
cachedEnabled = value;
}
Loading
Loading