Turn the rows that only reported things into controls

Autostart entries showed "Enabled" or "Disabled" as plain text. The row did
toggle on click the whole time, so this is an affordance rather than a missing
capability -- but a control that reads as static text is one nobody knows they
have. It is a switch now, with removal alongside it behind a confirmation:
disabling writes Hidden=true and can be undone, deleting the file cannot.

remove-autostart is confined to files the autostart directory owns. It resolves
the path and compares the parent, so a name like "../../.bashrc" cannot escape,
and it refuses symlinks rather than following them -- deleting through one would
remove whatever it points at, which is somewhere else and not ours. Each refusal
was tested against a fixture directory, including a symlink aimed at
/etc/hostname, which survived.

Sharing says who is signed in from another machine: user, origin and since when.
An empty list on this machine proves nothing, so the parser was checked against
sample `who` output -- it picks out remote sessions and leaves out local seats
and the :0 display, which would otherwise report the person at the keyboard as a
remote login.

Media sharing was "Available" and nothing else: rygel installed, rygel.service
disabled, no way to change that from here. It is a switch now, and it says what
it does before you touch it rather than afterwards -- turning it on publishes
media folders to every device on the network with no password in front of them.

Per-application camera and microphone permissions come from the portal's
permission store, which is where an application that asked through the portal
has its answer recorded. The page states the limit plainly instead of implying a
protection that does not exist: a program installed outside the portal opens the
device directly and nothing here stands in its way. Anything that is not an
explicit "yes" is treated as withheld, because guessing generously about a
camera is the wrong way to be wrong.

The first version of the write silently did nothing -- SetPermission takes an
array of strings and was being handed one string -- and the test did not notice,
because it discarded the helper's output and only checked that state was
unchanged afterwards, which was trivially true. The contract now requires the
value to move, and was proven to fail by putting that exact bug back.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Gabriel Brown
2026-08-19 23:10:48 -04:00
parent 7e1c85b094
commit f6b970da21
10 changed files with 664 additions and 11 deletions
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
# Application permissions, as far as the desktop can actually enforce them.
#
# The rules:
#
# 1. The page never claims more than the portal can do. A native binary opens
# /dev/video0 directly, and a settings page implying otherwise is worse
# than one that says nothing -- so the limit is stated on the page, not
# buried in a comment.
# 2. A device nothing has asked for is reported empty, not omitted. "No
# application uses your microphone" and a page that quietly leaves the
# microphone out look identical and mean very different things.
# 3. Absence is not failure. The store answers "No entry for microphone" for a
# device nobody has requested; treating that as an error would make the
# whole page fail because one device is unused.
# 4. Anything that is not an explicit "yes" is withheld. Guessing generously
# about a camera is the wrong way to be wrong.
# 5. A refusal states its reason.
#
# The write path is exercised against an application id that does not exist, so
# no real application's camera access is changed. What is on this machine --
# OBS Studio and GNOME Snapshot -- is read, never written.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-permissions"
service="$repo_dir/config/dot/quickshell/services/Permissions.qml"
page="$repo_dir/config/dot/quickshell/modules/settings/PrivacyPage.qml"
probe="org.panama.ContractProbe"
fail() {
printf 'permissions contract: %s\n' "$1" >&2
exit 1
}
for path in "$helper" "$service" "$page"; do
[[ -r "$path" ]] || fail "missing $path"
done
[[ -x "$helper" ]] || fail 'panama-permissions is not executable'
field() { python3 -c "import json,sys; print(json.load(sys.stdin)$1)"; }
state="$("$helper" snapshot)" || fail 'snapshot failed'
if [[ "$(printf '%s' "$state" | field "['available']")" != "True" ]]; then
printf 'permissions contract: skipped (the portal permission store is not running)\n'
exit 0
fi
# ── 1. The page states the limit ────────────────────────────────────────────
grep -q 'directly' "$page" \
|| fail 'the page does not say that programs outside the portal reach these devices anyway'
# ── 2 & 3. Unused devices are present and empty, not an error ───────────────
printf '%s' "$state" | python3 -c "
import json, sys
state = json.load(sys.stdin)
if state['error']:
raise SystemExit(f\"snapshot reported an error: {state['error']}\")
names = [d['id'] for d in state['devices']]
for required in ('camera', 'microphone', 'speakers'):
if required not in names:
raise SystemExit(f'{required} is missing from the snapshot entirely')
" || fail 'a device with no recorded application was dropped or reported as an error'
# ── 4 & 5. The write path, on an application that does not exist ────────────
before="$(printf '%s' "$state" | field "['devices']")"
denied="$("$helper" set camera "$probe" deny)" || fail 'set deny failed'
reason="$(printf '%s' "$denied" | field "['error']")"
[[ -z "$reason" ]] || fail "denying refused a valid write: $reason"
printf '%s' "$denied" | python3 -c "
import json, sys
for device in json.load(sys.stdin)['devices']:
for app in device['applications']:
if app['app'] == '$probe':
if app['allowed']:
raise SystemExit('a denied application was reported as allowed')
raise SystemExit(0)
raise SystemExit('the denied application was not written at all')
" || fail 'deny did not take effect -- the write path is not doing anything'
allowed="$("$helper" set camera "$probe" allow)" || fail 'set allow failed'
printf '%s' "$allowed" | python3 -c "
import json, sys
for device in json.load(sys.stdin)['devices']:
for app in device['applications']:
if app['app'] == '$probe' and app['allowed']:
raise SystemExit(0)
raise SystemExit('allow did not take effect')
" || fail 'allow did not take effect'
# Refusals name their reason rather than merely failing.
reason="$(printf '%s' "$("$helper" set camera "$probe" maybe)" | field "['error']")"
[[ "$reason" == *"allow or deny"* ]] \
|| fail "an invalid decision was not refused with a reason (got: $reason)"
reason="$(printf '%s' "$("$helper" set nonsense "$probe" allow)" | field "['error']")"
[[ -n "$reason" ]] || fail 'an unknown device was accepted'
# ── Put it back ────────────────────────────────────────────────────────────
"$helper" forget camera "$probe" >/dev/null || fail 'forget failed'
after="$("$helper" snapshot | field "['devices']")"
[[ "$before" == "$after" ]] \
|| fail 'the contract changed recorded permissions and did not restore them'
printf 'permissions contract: ok\n'