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

158 lines
9.6 KiB
Markdown

# 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.