# 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 `List`s the table, `Lookup`s 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.