Skip to content

feat: per-site permission tiers (deny/read/full)#15

Merged
karngyan merged 18 commits into
mainfrom
feat/permission-model
Jul 11, 2026
Merged

feat: per-site permission tiers (deny/read/full)#15
karngyan merged 18 commits into
mainfrom
feat/permission-model

Conversation

@karngyan

Copy link
Copy Markdown
Contributor

Implements roadmap Phase 1 (Trust, v0.3): a per-site permission model unifying the site-policy and read-only bullets. Spec: docs/superpowers/specs/2026-07-11-permission-model-design.md.

What

  • Three tiers per hostdeny < read < full, resolved exact-rule > longest wildcard (*.host.com, matches apex) > configurable default. Shipped default stays full everywhere: zero-config behavior unchanged, policy is opt-in hardening.
  • Enforced in the extension (dispatchMethod gate): resolves the target tab once, checks the host tier, pins tabId into handler params. navigate/open_tab also check the destination host. Denied tabs are redacted in list_tabs (blocked: true, url/title blanked).
  • Grants are popup-only. New popup "Site permissions" section (current-tab tier toggle, rules list, default stance). The daemon-facing policy_tighten bridge method applies only strictly-tightening changes — the comparison runs in the extension, so a shell agent can't grant itself access.
  • reins policy CLI — show policy + effective tier per open tab; deny/readonly <pattern> tighten; allow always errors with the popup pointer.
  • policy_denied error code threaded background → offscreen → ResponseFrame; message names host, tier, and remediation.
  • Docs: README security section, SKILL.md gotcha (treat denials as user policy, don't retry), changeset (minor).

Testing

  • 358 tests green across packages (protocol matcher/tier-map, extension store+gate+redaction, CLI command, integration incl. denial flow surfacing 502 with the policy message).
  • pnpm -r typecheck, biome, extension + CLI builds green.
  • Real-wire smoke: reins policy through the live daemon to the currently-installed (pre-PR) extension round-trips cleanly (unknown method: policy_get, as expected until the new extension build is loaded). Manual popup pass on the new build still pending — reload the unpacked dev extension to exercise it.

🤖 Generated with Claude Code

karngyan and others added 11 commits July 11, 2026 13:09
Unifies roadmap Phase 1 site-policy and read-only bullets into one
per-host tier model (deny/read/full), enforced in the extension,
grants popup-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate resolves the target tab once, checks host tier, pins tabId into
handler params. policy_denied error codes survive the offscreen relay
to the ResponseFrame. Denied tabs are redacted in list_tabs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review findings from PR #15:
- hostOf now strips trailing dots so https://bank.com./ resolves to the
  same tier as bank.com instead of falling through to the default.
- console/network ring buffers reset when a monitored tab navigates to a
  different host; previously telemetry captured on a denied host stayed
  readable after navigating back to an allowed one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karngyan

Copy link
Copy Markdown
Contributor Author

Adversarial review pass (Fable subagent, spec-guided, read-only verification of the full diff):

Round 1 — 2 majors found, both fixed:

  1. hostOf kept trailing-dot FQDNs, so https://bank.com./ bypassed bank.com deny/read rules → now canonicalized (28a4e79, regression tests).
  2. Console/network ring buffers were tab-scoped, not host-scoped: telemetry captured on a denied host stayed readable after navigating back to an allowed host → buffers now reset on cross-host navigation (28a4e79, new monitor.test.ts).
  3. Adjacent gap: protocol-relative //host/x navigate targets skipped the destination check → now resolved against the current page URL first (b5423ec).

Round 2 — all three confirmed fixed, incl. edge checks: all-dot hosts coerce to undefined (default tier), attach-vs-onUpdated race is safe (both no-op before MONITORS.set), and the new URL(to, tab.url) fallback matches Chrome's own WHATWG resolution. Known residual, in-spec: sub-resource requests from an allowed top-level page are captured regardless of the sub-resource's host (policy governs by top-level host; noted for the SECURITY.md slice).

Invariants verified: enforcement only in the extension, policy_tighten strictly monotonic, every bridge method gated via METHOD_TIERS membership, fail-closed on corrupt storage, policy_denied code intact daemon-side.

karngyan and others added 7 commits July 11, 2026 15:07
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native selects can't be themed; replace with a button + listbox
popover (keyboard navigable, aria listbox semantics). Current-site
row keeps the segmented control.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One 30px control size across the section, concentric radii on the
segmented control, tier badges + hairline dividers for the rules
list, single-row add form with icon submit, popover ring+shadow
instead of border+shadow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setCustomValidity persisted across attempts and made the browser
swallow submit events, so one bad input jammed the form. Form is
novalidate now and the error clears on input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-2 review fixes:
- All policy mutations (popup edits via new reins:policy-change message,
  policy_tighten) run through one write queue in the worker; unserialized
  read-modify-writes could drop a concurrent CLI tighten or popup edit.
- Popup no longer falls back to full-access defaults when stored policy
  is unreadable; it renders an error state and disables edits, matching
  the worker's fail-closed stance.
- Popup re-renders on storage.onChanged (stale view after CLI tighten).
- a11y: aria-pressed on segment buttons; dropdown gets aria-label/
  aria-controls, labeled listbox, focusout close, Escape on trigger.
- Add-rule form validates the pattern locally so storage failures are
  not misreported as syntax errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Containers with display:flex defeated the UA sheet's [hidden] rule, so
the corrupt-policy lockout (and the non-http current-site row hide)
never hid anything. Failed click-path edits now also re-render from
storage so optimistic labels snap back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karngyan

Copy link
Copy Markdown
Contributor Author

Review round 2 (fresh Fable subagent, focus on the popup UI code that landed after round 1):

Found 6 (2 major, 4 minor) — all fixed (8578f75, 43c1cf8):

  1. Unserialized policy writes: popup RMW on chrome.storage could clobber a concurrent reins policy tighten (loosening through the back door). All mutations now flow through a single-writer queue in the service worker; popup sends reins:policy-change intents instead of writing storage. Concurrency tests added.
  2. Popup failed open on corrupt policy (rendered + could persist full-access defaults, wiping rules). Now mirrors the worker's fail-closed stance: error banner, controls disabled.
    3–6. a11y (aria-pressed on segments; labeled listbox + aria-controls + focusout/Escape handling on the custom dropdown), stale popup after CLI tighten (re-render on storage.onChanged), storage errors misreported as pattern-syntax errors (local validation split from write failures).

Verification pass confirmed 5/6 and caught one gap in my fix: [hidden] lost to author display:flex, so the error-state lockout hid nothing — fixed with a [hidden] { display: none !important } guard (43c1cf8), which also fixed the pre-existing never-hidden current-site row on non-http(s) tabs. Reviewer verified policy integrity held regardless (worker parse-on-read refuses writes against corrupt storage end-to-end).

Also confirmed clean: no XSS surface (all textContent), no listener leaks per popup lifetime, no stale closures in delete handlers, no keyboard traps, reins:policy-change unreachable from web pages/other extensions/daemon, enforcement core unchanged and consistent with round-1 fixes.

@karngyan
karngyan merged commit d8108a6 into main Jul 11, 2026
2 checks passed
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.

1 participant