Plan repository audit remediation

This commit is contained in:
Gabriel Brown
2026-08-26 20:42:59 -04:00
parent 54dfa977ee
commit 0d894afe3e
@@ -0,0 +1,498 @@
# Panama repository audit remediation
Approved direction (2026-08-26): repair the complete repository audit in staged,
reviewable commits on one branch. Safety and test trust come first. No repair may
touch a real server, change a live firewall, or run a desktop-takeover check without
Gabriel explicitly approving that runtime step.
## Why this is one program
The audit found defects in four systems that depend on each other:
- `boot`, `install`, and `panama update` decide which code runs with root access.
- The server catalog and updater decide which network services run unattended.
- Quickshell owns capture, dictation, privacy state, and other long-lived desktop work.
- The contract runner is the proof for all three, but its safe classification and two
contracts are currently wrong.
Fixing a product defect while the gate is false-green or incorrectly labelled safe
would replace one uncertainty with another. This design therefore restores the gate
first, then fixes privileged and unattended paths, then the desktop.
## Decisions
| Decision | Choice |
|---|---|
| Delivery | One remediation branch with a small commit per behavior |
| Order | Test trust, bootstrap, updates and server lifecycle, desktop, remaining hardening |
| Test style | Behavioral fixtures at public command and QML interfaces |
| Live desktop | Excluded from the default safe gate; explicit capability and approval required |
| Live server | Never used during implementation; Podman, systemd, SSH, and firewall are stubbed |
| Production | Out of scope; repository changes only |
| Visual design | Unchanged; no mocks are needed because no layout or copy redesign is planned |
| Compatibility | Existing command names and normal successful flows remain intact |
| Failure policy | Privileged or destructive uncertainty fails closed with a recovery instruction |
## Program structure
The work is five repair packages. Each package has its own tests, implementation,
review, and commit sequence. A package may use a helper introduced by an earlier one,
but it may not reach forward into unfinished work.
### Package 1: make the verification gate trustworthy
#### Explicit contract capabilities
`tests/contracts.manifest` becomes the source of truth for contract execution.
Every discovered executable contract has exactly one entry. Its format is
`<capability>[,<capability>...] <repo-relative-path>`, with `#` for whole-line
comments:
```text
hermetic tests/setup/boot-contract
live-compositor tests/hypr/keybind-categories-contract
live-desktop tests/quickshell/settings-pages-contract
network tests/quickshell/home-assistant-helper-contract
privileged tests/setup/root-server-bootstrap-contract
```
The vocabulary is deliberately small:
- `hermetic`: temporary state and stubbed system commands only.
- `live-compositor`: reads or reloads the running compositor, without moving windows.
- `live-desktop`: maps surfaces, moves focus or windows, or changes desktop state.
- `network`: contacts a non-fixture network endpoint.
- `privileged`: needs root or changes system configuration.
Capabilities may be combined. `panama test --safe` runs only `hermetic` contracts.
`hermetic` is exclusive and cannot be combined with another capability. Plain
`panama test` keeps its current all-contract meaning, but prints the capabilities
before each non-hermetic contract and requires a TTY confirmation. Automation must
grant each needed capability explicitly with repeatable flags such as
`--allow live-compositor --allow network`; there is no grant that means "anything."
Pattern selection does not bypass this rule.
The existing `tests/desktop-hijacking` file is retired after its explanations move
beside the matching manifest entries as comments. A contract fails the manifest gate
when a discovered path is missing, a manifest path is stale, a path is duplicated, or
an unknown capability appears. The runner fails closed rather than inferring safety
from source patterns.
#### Runner behavior
Each contract gets a configurable outer timeout. The default is 180 seconds and
`PANAMA_TEST_TIMEOUT_SECONDS` may change it for local diagnosis. The runner captures
stdout and stderr separately. On failure it prints both. On success it prints a
warning and the captured stderr rather than discarding it.
The runner creates its capture directory with `mktemp -d`, removes it on exit, and
reports a timeout as a normal failed contract. One stuck QML or IPC process cannot
block the remaining suite forever.
#### Current red and false-green contracts
- `agent-usage-contract` generates its session directory and timestamps from the
current local day. Production gains no test-only clock interface.
- `compose-secrets-contract` parses YAML and dotenv-shaped files as structured data.
Markdown is scanned only for private-key and known token signatures, not prose that
resembles an assignment.
- `update-command-contract` runs the real `panama update` command against a temporary
local bare remote, clone, temporary state, and stubbed installer/system commands.
Parser failures and fixture command failures must propagate. Successful stderr stays
visible through the runner.
- Contracts that currently write a probe into tracked or live-linked paths move the
probe into a complete temporary copy. Any unavoidable temporary mutation restores
from the EXIT trap, including interruption.
#### Test interfaces
- Contract discovery and execution: `panama test [--safe] [pattern]`.
- Update behavior: the real `panama update` command in disposable Git repositories.
- Secret policy: parsed Compose/env data plus explicit token fixtures.
Tests do not assert that a source file contains a command when they can execute the
public command against stubs and inspect the outcome.
### Package 2: secure bootstrap and privileged installation
#### SSH hardening transaction
Root server bootstrap may harden SSH only when all of these are true:
1. The target account has a nonempty regular `authorized_keys` file.
2. The `.ssh` directory and file are owned by the target user and have safe modes.
3. A candidate Panama drop-in passes `sshd -t` as part of the complete active config.
4. Reloading the detected SSH unit succeeds.
The drop-in is written to a same-directory temporary file and installed atomically.
If validation or reload fails, Panama restores the previous drop-in, validates the
restored configuration, and leaves root/password access unchanged. If the user has no
verified key, hardening is unavailable rather than merely defaulting to no.
The public interface stays `boot --server`. A new root-bootstrap contract supplies
stubbed `id`, `passwd`, `sshd`, `systemctl`, account data, and filesystem state. It
proves the no-key refusal, successful transaction, invalid-config rollback, and failed-
reload rollback.
#### Trusted installation inputs
No network response may be executed as root unless Panama verifies an immutable digest
or package signature first.
- Terra installation no longer uses `--nogpgcheck`. Before implementation, the plan
records the current official repository instructions, signing-key fingerprint, and
verification command in a provenance fixture. The contract pins that fingerprint.
If no verifiable path exists, the installer reports Terra unavailable and stops
before the desktop package transaction that depends on it.
- Claude Desktop's unpinned GitHub Pages installer is removed from the automatic path.
Panama may install the package when a trusted repository is already configured. It
otherwise reports a manual optional step and continues without Claude Desktop.
- Existing remote installers for Bun, Claude Code, and similar tools are audited under
the same rule. A tool that cannot offer a pinned or signed installation path moves to
an explicit optional action rather than remaining in the unattended base install.
Package contracts test artifact provenance and failure behavior with local fixtures.
They never contact the real repositories.
#### Server firewall transaction
Server setup establishes the policy it documents:
- firewalld must be installed, enabled, and active, or server setup fails.
- Ports 80 and 443 accept public traffic only from validated Cloudflare IPv4 and IPv6
ranges held in dedicated firewalld ipsets.
- Port 81 is assigned only to the WireGuard interface or zone.
- No service-specific port is opened automatically.
- Updating Cloudflare ranges validates every CIDR before replacing the last known good
ipsets. An empty or malformed download changes nothing.
- The complete permanent configuration is applied and reloaded as one recoverable
transaction. A failure restores the previous Panama-owned rules.
The repository carries the policy and updater, not a claim that an external cloud
firewall happens to compensate. Fixture tests cover inactive firewalld, zone selection,
bad CIDRs, rollback, and repeated setup.
### Package 3: make updates, migrations, and server lifecycle recoverable
#### Clean-revision machine updates
`panama update` keeps local work stashed until the pulled checkout has completed
`install --upgrade` and migrations. It records the exact stash object it created and
does not assume `stash@{0}` still names it.
An EXIT/INT/TERM recovery handler restores that stash only when the pulled update has
finished and the worktree is clean. If restoration conflicts or the tree is not clean,
the handler leaves the exact stash untouched and prints its object ID and recovery
command. A next-run check also reports an unfinished Panama update, which covers SIGKILL
and power loss that no trap can catch.
Installer edits restored after the trusted update are not executed during that run.
The update contract proves dirty dotfiles, dirty installer code, pull failure, installer
failure, interruption, successful restore, and conflicting restore.
A failed or unavailable pull remains visible in the final status and makes `panama
update` return nonzero after safe local repairs finish. Interactive updates preflight
administrator authentication once. A noninteractive update uses `sudo -n` and fails
immediately with a clear instruction instead of waiting on a hidden prompt. Documentation
states that routine updates ask no Panama questions but may require administrator
authentication.
#### Exact installer state
Before disabling idle behavior, `install` records whether each relevant gsettings key
exists and its exact serialized value. The EXIT handler restores only keys it changed,
using those exact values. A missing schema remains untouched.
Every declared stage is required. A missing or non-executable stage is recorded as a
failed stage and makes the final result nonzero.
Automatic migration baselining is removed. Shipped migrations are self-guarding and run
on both fresh and existing machines. `--baseline` remains an explicit development/admin
command but no install path infers freshness from an absent state directory.
The secret relocation migration exits nonzero while both the old and new secret files
exist. It tightens the old file to mode `0600`, prints the two exact paths and required
manual reconciliation, and retries on the next migration run. It never marks unresolved
secret state complete.
#### Collision-proof displacement
A shared shell module owns backup displacement for installer, user-content, skills, and
server-definition paths. Its small interface accepts a source path and a backup category.
It guarantees:
- Panama-owned symlinks are replaced without backup.
- Foreign symlinks are moved, never discarded.
- Existing backups are never overwritten.
- The destination is unique without relying on second-resolution timestamps alone.
- A failed move leaves the original object in place and returns nonzero.
Callers no longer invent `.bak` or `.pre-panama` names independently. Behavioral tests
exercise repeated runs, foreign symlinks, collisions, and failure paths through the
public stage commands.
#### Server catalog containment and portability
`panama server` accepts only an exact service name returned by the catalog. Separators,
dot components, and aliases are rejected before any target path is created.
Compose definitions use `${HOME}` and `${XDG_RUNTIME_DIR}` rather than `/home/gib` and
`/run/user/1000`. `RequiresMountsFor` is removed where unused and uses `%h` only for a
service that truly needs the media mount. Existing `.env` files must be regular,
non-symlink files and are corrected to mode `0600` on every enable.
The nightly update unit executes a stable command linked under `~/.local/bin`, not a
hard-coded checkout path. Setup creates that link from the resolved `PANAMA_PATH`, so a
supported custom checkout receives the same timer behavior as the default location.
`disable` stops the unit, verifies the compose containers are stopped, and only then
unlinks the unit. Failure leaves the management link installed and reports the commands
needed for diagnosis.
Definition tracking separates `seen` from `applied` hashes. Relinking may update `seen`,
but only a verified successful restart updates `applied`, so an unapplied definition
keeps warning.
#### Safe unattended container updates
The updater takes a nonblocking `flock` for the complete run. It gets resolved images
from `podman compose config --images`; dotenv files are never sourced or evaluated as
shell code.
Before pulling, the updater records every running container's image ID and tags enough
rollback references to recreate the old project. After `compose up -d`, it waits with a
bounded timeout until every expected container is running and each declared healthcheck
is healthy. A service without healthchecks still has to remain running for the complete
stability window.
On failure, it restores the old image references, recreates the project, verifies the
rollback, records the service as failed, and keeps the rollback images. Image pruning
runs only after every changed service passed verification. A rollback failure is called
out separately and keeps all images.
The updater's public interface and systemd timer stay the same. A fixture contract stubs
`podman`, `podman compose`, and `systemctl` to prove pull failure, unhealthy startup,
crash after startup, successful update, successful rollback, failed rollback, frozen and
stopped services, concurrency, and hostile dotenv values.
#### Container privilege and secret policy
Socket consumers are split by what they need:
- Read-only monitoring uses an allowlisted socket proxy. A read-only bind mount is not
treated as an API permission boundary.
- Portainer retains administrative control only when pinned to an immutable image,
bound to WireGuard, excluded from automatic updates, and explicitly enabled as a
trusted administrator.
- Spoon's job worker runs under a dedicated Unix account and rootless Podman runtime so
a job cannot mount the main server account's SSH keys, service data, or `.env` files.
- Services that do not require the Podman API lose the socket mount.
Every service receives only the environment variables it needs. Redis, MinIO, and other
sidecars do not inherit a stack-wide `.env`. The catalog contract inventories socket
mounts, floating tags, `label:disable`, `seccomp:unconfined`, published ports, and broad
`env_file` use. Intentional exceptions name their risk and update policy in a machine-
checked manifest.
Required SMTP and mail settings live in each service's `.env.example`; Compose carries
only `${VAR}` references. `CHANGE_ME` never appears inline where `panama server enable`
cannot detect it.
Publicly sourced images use immutable versions or digests. A service that deliberately
tracks a locally controlled `latest` tag is excluded from unattended pulls unless its
deployment pipeline supplies and records an immutable digest.
### Package 4: fix desktop races and permanent work
#### One monitor per capture transaction
Capture snapshots the focused output name when a transaction opens and never reads live
focus again for that transaction. `shell.qml` creates one capture overlay per
`Quickshell.screens` entry. Only the overlay whose screen matches the snapshotted output
maps and accepts coordinates. Freeze, selection, capture, and commit all use the same
output name.
A two-screen QML fixture proves that focus changes after open do not move the transaction
and that overlay-local coordinates resolve against the frozen output.
Screenshot and recording destinations include millisecond precision plus an exclusive
collision suffix. The large frozen frame loads asynchronously while its overlay remains
unmapped; the overlay maps only after the image is ready or the fallback state is known.
#### Owned dictation recorder
The dictation helper serializes start, stop, cancel, and recovery with a lock. State is
written to a mode-`0600` temporary file and atomically replaced only after `pw-record`
starts.
State carries PID, `/proc` start time, expected executable identity, recording path, and
creation time. Stop and cancel signal a process only after all identity fields match.
Stale state is removed without signalling an unknown PID. Concurrent start returns the
existing active recording instead of launching a second recorder.
Behavioral tests use real short-lived fixture processes to prove concurrent start, stale
PID, PID mismatch, normal stop, cancel, and interrupted state write.
#### Request-owned Screen Intelligence work
Every analysis receives a monotonically increasing generation. Capture, OCR, and model
processes record that generation when started. Stream and exit callbacks ignore output
whose generation no longer matches the active request. `close()` invalidates the
generation before stopping any process.
The public QML interface does not change. A fixture overlaps requests and closes during
OCR to prove an old callback cannot set `ready`, `error`, or replace the new result.
#### Event-driven privacy state
Privacy monitoring uses Quickshell's PipeWire registry, node, link, and property change
signals. It does not fork `pw-dump` on an idle timer. Construction may perform at most
one initial registry read; every later update reacts to graph changes.
The existing curated-event and privacy interfaces remain intact. A contract runs an
isolated registry fixture long enough to prove no periodic process launches occur and
that camera, microphone, and screen-capture transitions still publish the same state.
#### Owned wallpaper processes and atomic writes
Video wallpaper cleanup signals only processes Panama launched. It tracks process IDs
and start times or uses a dedicated user-service cgroup. Global `pkill -x mpvpaper` and
global `pgrep` are removed.
Helpers that rewrite Compose, settings, or generated configuration write a same-directory
temporary file, preserve ownership and mode, flush and fsync, validate the temporary
content, then atomically replace the destination. A failed validation or write leaves the
original byte-for-byte intact.
#### Pinned speech assets
The Whisper container uses an immutable digest. The model has a committed URL, expected
size, and SHA-256. Downloads go to a temporary file and become active only after all
checks pass. A mismatch removes the temporary file and preserves the installed model.
### Package 5: remaining hardening and documentation
#### Desktop entries and media outputs
`panama-webapp` rejects NUL, newline, carriage return, and unsupported URL schemes.
It parses the URL, requires one logical argument, and escapes each Desktop Entry field
according to the specification. Browser switches cannot be introduced through the URL.
`panama-transcode` reserves a unique temporary output in the destination directory,
writes only to that path, and atomically renames after success. Failure cleanup removes
only the inode it created.
#### Documentation and policy alignment
- README role behavior, contract count, safe-test meaning, and sudo requirements match
the implemented commands.
- The manual uses the current Settings route names and hidden-leaf taxonomy.
- Migration guidance describes the actual interactive and noninteractive privilege
policy. Tests validate each command invocation, not file-wide word presence.
- Server documentation distinguishes policy Panama enforces from external firewall or
hosting assumptions.
- Generated settings documentation remains `--check` clean.
## Verification strategy
Every behavior follows one red-green cycle through an agreed interface:
| Repair | Interface under test | System adapter |
|---|---|---|
| Contract safety | `panama test` | temporary manifest and fixture contracts |
| Update safety | `panama update` | local Git remote and stub installer |
| Root bootstrap | `boot --server` | stub accounts, sshd, systemd, filesystem |
| Firewall | `setup-server` | stub firewall-cmd and systemctl |
| Installer state | `install` | temporary HOME/state and stub gsettings/stages |
| Server management | `panama server` | temporary catalog/HOME and stub systemctl/compose |
| Image updater | updater command | stateful Podman/systemd fixture |
| Capture | Capture QML interface | two-screen semantic harness |
| Dictation | helper CLI | fixture recorder processes and temporary runtime dir |
| Screen Intelligence | QML interface | delayed fixture processes |
| Privacy | privacy QML state | isolated PipeWire fixture |
After each package:
1. Run the focused contracts changed by that package.
2. Run `panama test --safe` under the new manifest.
3. Run syntax checks for every touched shell, Python, Lua, QML, YAML, and systemd file
using the tools available on the host.
4. Request independent code review and resolve Critical and Important findings.
5. Commit only that package.
Final repository verification includes the complete hermetic suite, Hyprland config
validation, generated-document checks, Compose rendering with fixture env files, secret
scanning, and `git diff --check`.
Live desktop checks are a separate, explicitly approved gate after all hermetic work is
green. Server cutover, service restarts, SSH reload, firewall mutation, and production
deployment are not part of this implementation branch.
## Audit finding ledger
This table prevents a smaller issue from disappearing behind the larger repairs.
| Finding | Package |
|---|---|
| SSH hardening lockout | 2 |
| Unverified root installation inputs | 2 |
| Missing documented firewall policy | 2 |
| Update executes restored work-in-progress | 3 |
| Interrupted update recovery | 3 |
| Pull failure followed by success status | 3 |
| Hidden sudo prompt on noninteractive update | 3 |
| Hard-coded idle restoration | 3 |
| Automatic migration baseline inference | 3 |
| Unresolved secret migration marked complete | 3 |
| Missing required stage skipped | 3 |
| Backup collisions and foreign symlink loss | 3 |
| Dotenv sourced and evaluated as shell | 3 |
| Invalid container health gate and premature prune | 3 |
| Server disable false success | 3 |
| Hard-coded home and runtime UID | 3 |
| Service-name path components | 3 |
| Definition warning acknowledged before apply | 3 |
| Existing `.env` mode and symlink trust | 3 |
| Concurrent updater runs | 3 |
| Custom `PANAMA_PATH` update unit | 3 |
| Floating images with Podman socket access | 3 |
| Sidecar secret over-distribution | 3 |
| Inline SMTP placeholders outside `.env` | 3 |
| Multi-monitor capture mismatch | 4 |
| Dictation concurrent start and reusable PID | 4 |
| Stale Screen Intelligence callbacks | 4 |
| Permanent `pw-dump` polling | 4 |
| Global `mpvpaper` process cleanup | 4 |
| Non-atomic source/config rewrites | 4 |
| Mutable speech image and model | 4 |
| Capture filename collisions and synchronous frame load | 4 |
| Date-expiring agent-usage contract | 1 |
| Secret-scan prose false positive | 1 |
| False-green update contract | 1 |
| Incomplete safe-test classification | 1 |
| No runner timeout and hidden stderr | 1 |
| Contracts mutating tracked/live-linked paths | 1 |
| Contracts depending on real installed settings | 1 |
| Weak migration privilege policy test | 5 |
| Web-app Desktop Entry injection | 5 |
| Transcode output deletion race | 5 |
| Documentation drift | 5 |
## Out of scope
- Running the bootstrap on a real VPS.
- Applying firewalld rules to a real host.
- Restarting or migrating any existing server service.
- Activating a new Hyprland session or changing monitor state.
- Visual redesign of Settings, capture, notifications, or other shell surfaces.
- Replacing Podman, Quickshell, Hyprland, or the existing command vocabulary.
- Broad refactoring unrelated to an audited failure path.
## Completion criteria
The remediation is complete when every ledger row has a verified implementation. A row
may leave the ledger only when a failing behavioral test disproves the audit finding and
Gabriel approves that removal. The hermetic suite must be green with no hidden stderr;
no contract classified safe may touch live desktop, network, or privileged state;
generated documentation and configuration validators must pass; and an independent
final review must find no unresolved Critical or Important issue.