9.6 KiB
Privacy & Security redesign — all eight tables
Approved mock: home-mocks/privacy.html (scratchpad, :8642). Spec wins over mock on conflict.
Goals
- 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.
- Traces clear natively: recent files, thumbnails, trash (reusing Disks) — the
openGnomePanel("privacy")punt dies and privacy becomes Panama-OWNED. - Secrets polish: per-action confirm state (Copy stops doubling as Cancel), copy feedback, friendly item descriptions.
- 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.
- 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 thedevicestable'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.xbelto an empty valid xbel document (not delete — GTK recreates but an empty valid file takes effect instantly).clear-thumbnails— guarded removal inside~/.cache/thumbnailsonly (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-passphraseflag the UI never passes). Returns the fresh snapshot.fix-permissions NAME— chmod 600, same confinement, returns fresh snapshot.agent-removeexists; unchanged.
Services (A)
- Permissions.qml:
tablesmodel 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; seamPANAMA_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 (copiedKeycleared by a timer, the KeyringcopiedPathpattern),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:privacybecomes 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, NEWscripts/panama-privacy,scripts/panama-ssh-keys,services/Permissions.qml, NEWservices/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, withgrantscounting the stored sessions behind it.forget TABLE APPdrops every one of them, which keeps the pinnedrevoke(table, app)signature honest. speakersis 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):
snapshotrows carrygrants(int) andraw(string) besideapp/allowed; the payload carriessimpleTablesandrevokeOnlyTablesso the service never hardcodes a list the helper could change.set TABLE APP true|false(notallow|deny— the old CLI's words).tracessub-objects carrypath,present, and (thumbnails)measured, which is false when the walk hit its budget and the byte count is a floor.Tracesdoes not measure on startup.measuredis false until the page callsmeasure().Permissions.devicessurvives as a derived camera/microphone view, because ApplicationsPage reads it to answer "does this app have a privacy rule".panama-ssh-keys generateruns ssh-keygen withSSH_ASKPASS_REQUIRE=neverand noDISPLAY: this desktop setsSSH_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 toHOME, so a hermetic run pointsHOMEat 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.