Files
Panama/docs/superpowers/specs/2026-08-24-privacy-security-redesign.md

9.6 KiB

Privacy & Security redesign — all eight tables

Approved mock: home-mocks/privacy.html (scratchpad, :8642). Spec wins over mock on conflict.

Goals

  1. One subject for eyes and ears: live PipeWire tiles + portal permission rows unified — camera, microphone, screen sharing (screencast table), remote desktop — plus the background table (7 live rows) as its own card. The sandboxing honesty note stays.
  2. Traces clear natively: recent files, thumbnails, trash (reusing Disks) — the openGnomePanel("privacy") punt dies and privacy becomes Panama-OWNED.
  3. Secrets polish: per-action confirm state (Copy stops doubling as Cancel), copy feedback, friendly item descriptions.
  4. SSH Keys complete: generation with a pty-fed passphrase, agent remove wired (with the keyring-agent honesty as prose), Fix-permissions action, copy feedback, visible empty states, refresh rows on both pages.
  5. The duplicated Screen-lock card is replaced by an "Elsewhere" pointer card (Power & Lock, Notifications).

Non-goals: location (geoclue absent — render the section only if the table has entries), notifications portal table (NotificationsPage owns the subject), documents-portal grants, telemetry/USB-protection gsettings (inert without their GNOME daemons — the trap the page's own header warns about), known-hosts hashed-entry removal, keyring collection management.

Helpers (pinned)

scripts/panama-permissions — generalized from 3 hardcoded devices to tables:

  • snapshot{ tables: { camera: [...], microphone: [...], screencast: [...], "remote-desktop": [...], background: [...], location: [...] }, available, error }; each row { app, allowed }. camera/microphone stay the devices table's simple yes/no; screencast / remote-desktop values are structured GVariants — those rows report presence only and support revoke only (DeletePermission), never Set (pinned: the page must not offer a toggle it cannot honor). background is plain yes/no (toggleable). location read-only listing.
  • set TABLE APP true|false (only for simple-valued tables: camera, microphone, background), forget TABLE APP (all tables). Table and app ids validated.

NEW scripts/panama-privacy — traces:

  • traces{ recents: { bytes, entries }, thumbnails: { bytes }, error } (du-based, budgeted).
  • clear-recents — truncate ~/.local/share/recently-used.xbel to an empty valid xbel document (not delete — GTK recreates but an empty valid file takes effect instantly).
  • clear-thumbnails — guarded removal inside ~/.cache/thumbnails only (resolve, refuse symlink escape — the panama-disks guard pattern).
  • Trash is NOT here — the page reuses Disks.clean("trash") / its cleanable byte count.

scripts/panama-ssh-keys:

  • generate NAME COMMENT — ed25519 only; NAME validated ^[A-Za-z0-9_.-]{1,64}$, confined to ~/.ssh, refuses overwrite; passphrase read from stdin by the helper, handed to ssh-keygen over a pty — never argv, never a temp file (empty passphrase allowed but the UI requires non-empty; helper accepts empty only with an explicit --no-passphrase flag the UI never passes). Returns the fresh snapshot.
  • fix-permissions NAME — chmod 600, same confinement, returns fresh snapshot.
  • agent-remove exists; unchanged.

Services (A)

  • Permissions.qml: tables model per the snapshot; setPermission(table, app, allowed), revoke(table, app); per-table helpers the UI needs (simpleTables, revokeOnlyTables).
  • NEW Traces.qml: recentsBytes/entries, thumbnailsBytes, measured, measure(), clearRecents(), clearThumbnails(), busy/lastError; seam PANAMA_PRIVACY_HELPER.
  • SshKeys.qml: generate(name, comment, passphrase) (passphrase via Process stdin), removeFromAgent(path) (wires the existing verb; surfaces the durableRemoval refusal message), fixPermissions(name), copy feedback (copiedKey cleared by a timer, the Keyring copiedPath pattern), refresh() exposed for a page row.
  • Keyring.qml: no changes expected; the confirm-state fix is page-side.

UI (B)

PrivacyPage.qml rebuilt (gains objectName: "privacy"): unified Camera/mic/screen card (live tiles from PrivacyState with the in-use warn tone; grouped sections per table with uppercase labels + counts; camera/mic rows Ask-again + toggle; screencast/remote-desktop rows detail-explained with two-stage Revoke; empty-section honesty lines; the "not sandboxed and never ask" note); Run in the background card (all rows, toggles); Saved passwords & secrets (unlock state row + lazy Saved-items with per-row copy feedback and per-row confirm state — separate confirmingItem from the copy path); Traces card (recents/thumbnails from Traces, trash from Disks cleanables with "the same Trash Storage cleans" detail, clipboard-history pointer row); Device security card + "Check again" refresh row; Elsewhere card (Power & Lock, Notifications pointers). Location section only when the table is non-empty. The Screen-lock card is DELETED. No openGnomePanel calls remain.

SshKeysPage.qml rebuilt: error rows into cards; Your keys card (rows + copy feedback + generate flow — name/comment LiveFieldRows, two SecretFieldRows with match validation, Create key disabled until valid + matching); Agent card (held keys with Remove, the design-not-a-bug prose when gnome-keyring); over-permissive warning card gains Fix permissions; Known hosts card always visible with an empty state; a refresh row.

Search & docs (C)

New entries: Background apps, Screen sharing permission, Remote desktop permission, Clear recent files, Thumbnails, Application permissions (privacy) → privacy; Generate an SSH key, Fix key permissions, Remove from agent → ssh-keys. Docs regen only if schema changes (none — verify).

Contracts (C — write; hermetic runs only)

  • permissions-contract: tables model; the revoke-only rule for structured tables (no Set path for screencast/remote-desktop anywhere — AST pin); the page never claims more than the portal enforces (kept); validated table/app ids.
  • NEW privacy-traces-contract: hermetic — clear-recents writes a valid empty xbel (never deletes), clear-thumbnails guarded (symlink escape refused), no urgency language in the Traces card copy (the anti-racket stance), trash reuses Disks (no second trash implementation — grep pin).
  • secrets-contract: extend — per-row confirm state separated from copy (the collision pin), copy feedback present; all existing pins kept.
  • ssh-keys-contract: extend — generate's passphrase never in argv (AST + runtime with a recording stub), pty usage pinned, name confinement + overwrite refusal, fix-permissions confinement, agent-remove reachable from QML now + the honesty prose, copy feedback; all existing pins kept (private keys never read, passphrase rule).
  • gnome-handoff-contract: privacy becomes OWNED; verify no page hands off to it.
  • lock-screen-settings-contract: reconcile with the lock card's removal from PrivacyPage.
  • Backlog Phase 12; README count line (172 → 173 expected).

Agent ownership (parallel)

  • A: scripts/panama-permissions, NEW scripts/panama-privacy, scripts/panama-ssh-keys, services/Permissions.qml, NEW services/Traces.qml, services/SshKeys.qml.
  • B: modules/settings/PrivacyPage.qml, SshKeysPage.qml, new components (+ qmldir).
  • C: services/SettingsSearch.qml, contracts above, backlog, README count line.

As built (A) — refinements to the pinned APIs

Read the real permission store before finalizing, and two things there were not what the spec assumed:

  • screencast / remote-desktop ids are opaque restore tokens, one per remembered session, not the table name. So the helper Lists the table, Lookups each token, and folds the result by application — one row per app, with grants counting the stored sessions behind it. forget TABLE APP drops every one of them, which keeps the pinned revoke(table, app) signature honest.
  • speakers is gone. The spec names six tables and speakers is not among them; the old three-device model is replaced wholesale.

Additive to the pinned shapes (nothing removed):

  • snapshot rows carry grants (int) and raw (string) beside app/allowed; the payload carries simpleTables and revokeOnlyTables so the service never hardcodes a list the helper could change.
  • set TABLE APP true|false (not allow|deny — the old CLI's words).
  • traces sub-objects carry path, present, and (thumbnails) measured, which is false when the walk hit its budget and the byte count is a floor.
  • Traces does not measure on startup. measured is false until the page calls measure().
  • Permissions.devices survives as a derived camera/microphone view, because ApplicationsPage reads it to answer "does this app have a privacy rule".
  • panama-ssh-keys generate runs ssh-keygen with SSH_ASKPASS_REQUIRE=never and no DISPLAY: this desktop sets SSH_ASKPASS_REQUIRE=prefer, which made ssh-keygen open a graphical dialog and ignore the terminal entirely.
  • Helper seams: PANAMA_PRIVACY_HELPER, PANAMA_PERMISSIONS_HELPER, PANAMA_SSH_KEYS_HELPER. Fixture seams: PANAMA_PRIVACY_RECENTS, PANAMA_PRIVACY_THUMBNAILS (both still confined to HOME, so a hermetic run points HOME at a scratch directory).

Hard rules: NO live mutations — no portal Set/Delete, no keyring writes, no ssh-keygen runs against the real ~/.ssh, no chmod, no truncating the real recents, no thumbnail deletion. Read-only probes and hermetic stubs only. Valid QML/Python at every save. B programs against the pinned APIs; A updates this spec before changing them.