Author SHA1 Message Date
Gabriel Brown a69cacc006 WIP: Paused over-hardening fix wave (boot checkout verification, double-read package manifest) 2026-09-17 11:44:01 -04:00
Gabriel Brown 28e387868f Docs: Record signed Terra bootstrap proof 2026-08-27 11:13:59 -04:00
Gabriel Brown 1f65e09865 Test: Prove verified bootstrap boundaries 2026-08-27 10:44:14 -04:00
Gabriel Brown dce00e45d1 Docs: Pin the verified Panama bootstrap 2026-08-27 10:14:55 -04:00
Gabriel Brown 0d1841cf86 Fix: Verify the initial Panama revision 2026-08-27 10:06:13 -04:00
Gabriel Brown ba4e5e6677 Fix: Lock installer input state 2026-08-27 09:48:28 -04:00
Gabriel Brown 1ffd05f0e1 Fix: Re-run verified installer inputs 2026-08-27 09:34:20 -04:00
Gabriel Brown 92c60c3ecd Fix: Close runtime provenance residuals 2026-08-27 09:22:17 -04:00
Gabriel Brown 31fc1d3eaa Fix: Harden pinned runtime activation 2026-08-27 08:32:44 -04:00
Gabriel Brown 8264ca6320 Fix: Pin runtime and agent artifacts 2026-08-27 07:49:12 -04:00
Gabriel Brown eab4280969 Fix: Parse complete Terra repository state 2026-08-27 07:25:25 -04:00
Gabriel Brown 1c3cd7ac72 Fix: Enforce effective repository trust preflight 2026-08-27 07:10:11 -04:00
Gabriel Brown 8652f92aae Fix: Validate existing package repositories 2026-08-27 06:46:36 -04:00
Gabriel Brown 4abf3f8acd Fix: Verify third-party package repositories 2026-08-27 06:27:35 -04:00
Gabriel Brown 3f8539176a Test: Strengthen ambient GPG isolation contract 2026-08-27 06:10:32 -04:00
Gabriel Brown 18ecc2244f Fix: Isolate installer GPG inspection 2026-08-27 06:05:42 -04:00
Gabriel Brown 94a4314730 Fix: Harden installer provenance boundary 2026-08-27 05:56:46 -04:00
Gabriel Brown 27acbe7a95 Test: Add installer provenance boundary 2026-08-27 05:43:26 -04:00
Gabriel Brown fc8f226747 Fix: Complete SSH bootstrap hardening 2026-08-27 05:19:58 -04:00
Gabriel Brown 98e29eb8f3 Test: Strengthen SSH bootstrap documentation contract 2026-08-27 04:35:45 -04:00
Gabriel Brown 00e11e0b8d Docs: Explain transactional SSH hardening 2026-08-27 04:25:05 -04:00
Gabriel Brown ac25fd776c Fix: Clean interrupted SSH candidates 2026-08-27 04:12:45 -04:00
Gabriel Brown 51e2c8418a Fix: Complete SSH hardening recovery 2026-08-27 04:06:54 -04:00
Gabriel Brown d267da58ad Fix: Roll back failed SSH hardening 2026-08-27 03:44:57 -04:00
Gabriel Brown 20b41c3976 Fix: Gate SSH hardening on a verified key 2026-08-27 03:28:42 -04:00
Gabriel Brown bfb37afd69 Plan secure bootstrap and privileged installation 2026-08-27 03:11:00 -04:00
Gabriel Brown b361db8486 Design secure bootstrap and privileged installation 2026-08-27 02:23:55 -04:00
Gabriel Brown 12b858371e Fix: Close verification gate review findings 2026-08-27 00:21:45 -04:00
Gabriel Brown 77f625d7bb Docs: Clarify contract output handling 2026-08-26 23:38:09 -04:00
Gabriel Brown 34e295d184 Docs: Explain contract capability gates 2026-08-26 23:32:38 -04:00
Gabriel Brown e19fb00276 Test: Harden dock fixture cleanup 2026-08-26 23:22:26 -04:00
Gabriel Brown 37688f702a Test: Isolate dock geometry fixtures 2026-08-26 23:12:14 -04:00
Gabriel Brown 71f4f0d24b Test: Guard settings docs contract cleanup 2026-08-26 23:05:14 -04:00
Gabriel Brown 3839183401 Test: Isolate generated docs and settings fixtures 2026-08-26 22:59:28 -04:00
Gabriel Brown 68b6664111 Test: Propagate update fixture failures 2026-08-26 22:53:39 -04:00
Gabriel Brown 803362ebad Test: Exercise updates through the real command 2026-08-26 22:46:46 -04:00
Gabriel Brown 6a7bdcb835 Test: Harden compose secret fixtures 2026-08-26 22:38:00 -04:00
Gabriel Brown 37fd5e890e Test: Scan compose secrets by data shape 2026-08-26 22:31:22 -04:00
Gabriel Brown a19c6dc3ef Test: Make agent usage fixtures date independent 2026-08-26 22:26:19 -04:00
Gabriel Brown 6d71a77798 Test: Cover contract runner consent 2026-08-26 22:22:57 -04:00
Gabriel Brown d8144a66c1 Fix: Make contract execution safe and diagnostic 2026-08-26 22:11:56 -04:00
Gabriel Brown 43d1e15858 Test: Harden manifest validation 2026-08-26 22:00:52 -04:00
Gabriel Brown 1192ad64dc Test: Classify every contract capability 2026-08-26 21:52:16 -04:00
Gabriel Brown 044139ed63 Plan verification gate remediation 2026-08-26 21:11:25 -04:00
Gabriel Brown 0d894afe3e Plan repository audit remediation 2026-08-26 20:42:59 -04:00
73 changed files with 17437 additions and 905 deletions
+10 -4
View File
@@ -138,10 +138,16 @@ steps, each checked and skipped when already true:
exists, use it. exists, use it.
2. Ensure the user has a password (needed for sudo) — `passwd` interactively 2. Ensure the user has a password (needed for sudo) — `passwd` interactively
if none is set. if none is set.
3. Copy root's `authorized_keys` to the user when the user has none. 3. Copy root's `authorized_keys` only after OpenSSH parses every key line. The
4. Offer sshd hardening (yes/no, default yes): drop-in under destination directory and file are created as the target UID at `0700/0600`
`/etc/ssh/sshd_config.d/` with `PermitRootLogin no`, and revalidated before hardening is available.
`PasswordAuthentication no`, then reload sshd. Skipped when already set. 4. Offer sshd hardening (yes/no, default yes): atomically install
`/etc/ssh/sshd_config.d/00-panama.conf` with `PermitRootLogin no`,
`PasswordAuthentication no`, and `KbdInteractiveAuthentication no`. Run
`sshd -t` plus effective root/target `sshd -T -C` checks before reloading the
detected unit. Restore a prior regular file with metadata on failure. A
missing unit or unsupported existing drop-in leaves SSH unchanged and
bootstrap continues. The later binding transaction design owns the details.
5. Move/clone the checkout under the user's home, chown it, and re-exec 5. Move/clone the checkout under the user's home, chown it, and re-exec
`install --server` as that user. `install --server` as that user.
+48 -6
View File
@@ -50,13 +50,17 @@ Editing `config/dot/quickshell/**` hot-reloads the live shell on every save.
## The contract discipline ## The contract discipline
~180 executable contracts under `tests/` pin behavior. The rules: Every executable contract under `tests/` is classified in `tests/contracts.manifest`. The rules:
- `panama contracts <file>` names the contracts that mention a file; run those after touching it. - `panama contracts <file>` names the contracts that mention a file; run those after touching it.
- During a desktop session, run `panama test --safe [pattern]` — it skips everything listed in - During a desktop session, run `panama test --safe [pattern]`. `--safe` selects only contracts
`tests/desktop-hijacking` (contracts that restart the shell, open real overlays, or write classified as `hermetic`.
through the live compositor). Run the full `panama test` only with the user's go-ahead, and say - A plain full run prompts in a terminal before any selected non-hermetic contract starts. In
so first: it visibly hijacks the desktop. automation, grant every required capability with repeatable flags, for example
`panama test --allow live-compositor --allow live-desktop keybinds`.
- Each contract has a 180-second outer timeout by default. Override it with a positive
`PANAMA_TEST_TIMEOUT_SECONDS` value. Failures print the contract's captured stdout and stderr.
Successful stdout stays quiet. Successful stderr is surfaced as a warning.
- Contracts run directly too: `tests/setup/interview-contract`. - Contracts run directly too: `tests/setup/interview-contract`.
- After changing `PreferenceSchema.qml` or `services/SettingsRoutes.qml`, regenerate: - After changing `PreferenceSchema.qml` or `services/SettingsRoutes.qml`, regenerate:
`config/dot/quickshell/scripts/panama-settings-docs` (writes `docs/settings.md`) and `config/dot/quickshell/scripts/panama-settings-docs` (writes `docs/settings.md`) and
@@ -88,4 +92,42 @@ it against the real system.
## Root work ## Root work
Never bare `sudo` — load the `panama-sudo` skill. Migrations already follow the rule. Never bare `sudo`. Load the `panama-sudo` skill first. Migrations already follow the rule.
## Initial bootstrap trust
Use the complete command in `README.md` for a new machine. It downloads `boot`
from the documented commit URL into a private temporary directory, enforces the
documented curl time and byte limits, verifies the committed blob with
`sha256sum -c`, and passes both pins to the verified script. Desktop and server
bootstrap use the same commit and digest. Never substitute a branch URL, pipe a
response into Bash, or invent a newer pin from an uncommitted file.
`boot` validates the full lowercase commit and SHA-256 before Git or install. A
fresh destination fetches only that revision and creates a tracked local `main`
after HEAD matches. An existing checkout must be clean and an ancestor of the
requested revision; it advances with fast-forward only. Dirty, divergent,
fetch-failed, or mismatched checkouts stop without reset or install. Once Panama
exists, use `panama update` for normal updates.
`boot --server` is the one exception where a new Fedora VPS may begin as root.
Before it offers SSH hardening, it copies a safe root key when possible or
verifies the target key. The target user's `.ssh` must be owned by that user at
`0700`, and `authorized_keys` must be owned by that user at `0600`. Without a
verified target key, SSH hardening is unavailable and the bootstrap continues.
Every non-comment key line must parse with `ssh-keygen`. Root-key destination
creation and writing run as the target UID, followed by the same owner, mode,
and key checks. Do not replace that with root writes or assume the user's
primary group matches the username.
Accepted hardening uses atomic same-directory `00-panama.conf` with exactly
`PermitRootLogin no`, `PasswordAuthentication no`, and
`KbdInteractiveAuthentication no`. A pre-existing symlink or non-regular
object makes hardening unavailable, as does a missing SSH unit. Panama runs
`sshd -t`, then checks effective root and target-user policy with `sshd -T -C`
before reloading the detected unit. Validation or reload failure restores a
prior regular file with its metadata before it retries validation and reload.
Failed recovery stops the handoff with instructions that distinguish a prior
file from no prior file. The fixture contracts also cover declined hardening
and interrupted preparation. `panama test --safe` never reloads a live daemon,
so it is not live-host proof.
+3 -3
View File
@@ -39,10 +39,10 @@ __pycache__/
/config/firefox/chrome/panama-theme.css /config/firefox/chrome/panama-theme.css
/config/firefox/chrome/panama-theme.css.tmp /config/firefox/chrome/panama-theme.css.tmp
# Build products of the Vicinae extension. The source is the repository's; the # Build products of the Vicinae extension. The source and reviewed lockfile are
# dependency tree and the bundle it produces are machine state, rebuilt by # in the repository; the dependency tree and bundle are machine state.
# `panama apps`.
/config/local/share/vicinae/extensions/*/node_modules/ /config/local/share/vicinae/extensions/*/node_modules/
/config/local/share/vicinae/extensions/*/dist/ /config/local/share/vicinae/extensions/*/dist/
/config/local/share/vicinae/extensions/*/build/ /config/local/share/vicinae/extensions/*/build/
/config/local/share/vicinae/extensions/*/package-lock.json /config/local/share/vicinae/extensions/*/package-lock.json
!/config/local/share/vicinae/extensions/panama-search/package-lock.json
+85 -23
View File
@@ -1,25 +1,37 @@
# Panama # Panama
Formerly Sunhat. A personal config for Fedora, with the intention of helping a Formerly Sunhat. A personal config for Fedora, with the intention of helping a
user set up their Fedora system with one command. user set up their Fedora system with one checked command.
```sh ```sh
bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot) (
set -euo pipefail
bootstrap_commit=0d1841cf86975d52485c94c0caa5d1b67e5c54a1
bootstrap_sha=fd8dcd70d1266da109972df346e3d85d1037f6f6e1942b0157064412ab892372
bootstrap_dir="$(umask 077; mktemp -d)"
trap 'rm -rf -- "$bootstrap_dir"' EXIT
bootstrap="$bootstrap_dir/boot"
curl --fail --location --connect-timeout 10 --max-time 30 --max-filesize 262144 \
--output "$bootstrap" \
"https://git.gbrown.org/gib/Panama/raw/commit/$bootstrap_commit/boot"
printf '%s %s\n' "$bootstrap_sha" "$bootstrap" >"$bootstrap_dir/boot.sha256"
sha256sum -c "$bootstrap_dir/boot.sha256"
PANAMA_BOOT_REVISION="$bootstrap_commit" PANAMA_BOOT_SHA256="$bootstrap_sha" \
bash "$bootstrap"
)
``` ```
`boot` installs git if the machine lacks it, clones this repository to `boot` installs git if the machine lacks it, creates a verified checkout at
`~/.local/share/Panama` (or `$PANAMA_PATH`), and hands off to `install`. It is `~/.local/share/Panama` (or `$PANAMA_PATH`), and hands off to `install`. It is
deliberately small enough to read first, and the same two steps by hand work deliberately small enough to read first. The command downloads one immutable
identically: `boot` blob into a private temporary directory, checks its SHA-256, then asks
that verified script to fetch the same commit. A clean existing checkout may
fast-forward to that exact revision. A dirty or divergent checkout, a failed
fetch, or a revision mismatch stops before `install`; Panama never resets local
work or falls back to whatever checkout happens to be present.
```sh Once a machine exists, the command that keeps it current is `panama update`
git clone https://git.gbrown.org/gib/Panama.git ~/.local/share/Panama one command, and it never asks you anything:
~/.local/share/Panama/install
```
Both are safe to run again: an existing clone is fast-forwarded rather than
replaced. Once a machine exists, though, the command that keeps it current is
`panama update` — one command, and it never asks you anything:
```sh ```sh
panama update panama update
@@ -35,15 +47,52 @@ shell environment minus everything that needs a screen, plus rootless podman
and the compose services in [`server/`](server/README.md). The answer is the and the compose services in [`server/`](server/README.md). The answer is the
one thing Panama records durably (`~/.local/state/panama/role`), because one thing Panama records durably (`~/.local/state/panama/role`), because
`panama update` asks nothing and still has to know. A fresh server skips the `panama update` asks nothing and still has to know. A fresh server skips the
question entirely: question entirely. It uses the same immutable boot blob and pins as the desktop
command; only the final `--server` argument differs:
```sh ```sh
bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot) --server (
set -euo pipefail
bootstrap_commit=0d1841cf86975d52485c94c0caa5d1b67e5c54a1
bootstrap_sha=fd8dcd70d1266da109972df346e3d85d1037f6f6e1942b0157064412ab892372
bootstrap_dir="$(umask 077; mktemp -d)"
trap 'rm -rf -- "$bootstrap_dir"' EXIT
bootstrap="$bootstrap_dir/boot"
curl --fail --location --connect-timeout 10 --max-time 30 --max-filesize 262144 \
--output "$bootstrap" \
"https://git.gbrown.org/gib/Panama/raw/commit/$bootstrap_commit/boot"
printf '%s %s\n' "$bootstrap_sha" "$bootstrap" >"$bootstrap_dir/boot.sha256"
sha256sum -c "$bootstrap_dir/boot.sha256"
PANAMA_BOOT_REVISION="$bootstrap_commit" PANAMA_BOOT_SHA256="$bootstrap_sha" \
bash "$bootstrap" --server
)
``` ```
That command also works from a brand-new VPS's **root** login: it creates That command also works from a brand-new VPS's **root** login. It creates or
your user with sudo, moves the SSH key over, offers to harden sshd, and hands reuses your sudo-enabled user, then copies a safe root key when it can or
off to a normal install as that user. verifies the target key before offering SSH hardening. A verified target key
means the target user owns `.ssh` with mode `0700` and `authorized_keys` with
mode `0600`. Every non-comment line in `authorized_keys` must be valid OpenSSH
key material that `ssh-keygen` can parse. Root-key destination writes run as
the target user, and Panama rechecks the resulting owner, modes, and keys. SSH
hardening is unavailable without a verified target key or installed SSH unit,
and the install continues without it. Declining hardening also leaves SSH
unchanged.
When you accept hardening, Panama uses an atomic same-directory drop-in named
`00-panama.conf`, whose early filename gives it safer precedence. A pre-existing
symlink or non-regular object makes hardening unavailable. The effective policy
is exactly `PermitRootLogin no`, `PasswordAuthentication no`, and
`KbdInteractiveAuthentication no`. Panama validates syntax with `sshd -t` and
checks `sshd -T -C` for both root and target-user contexts before it reloads
the detected SSH unit. If validation or reload fails, it restores the previous
drop-in regular file with its metadata and validates and reloads that restored
configuration. Recovery that cannot complete stops the handoff and prints the
right manual command for either a prior file or no prior file. The fixture
contracts test these branches. No real daemon reload runs under
`panama test --safe`, so that suite is not live-host proof.
After that, it hands off to a normal install as the new user.
`install` asks its questions first and then runs the stages in `setup/scripts/` `install` asks its questions first and then runs the stages in `setup/scripts/`
in order, without stopping again: in order, without stopping again:
@@ -198,14 +247,26 @@ docs/ Settings reference, and the design specs behind the work
## Tests ## Tests
185 of them, under `tests/`. Run the lot, or a subset by pattern: 188 of them, under `tests/`. `tests/contracts.manifest` classifies every
contract by the capabilities it needs. Run the hermetic set, or grant a
specific external capability when automation needs it:
```sh ```sh
panama test # everything panama test --safe
panama test dock # just the ones matching "dock" panama test --allow live-host updates
panama test --allow live-compositor keybinds
PANAMA_TEST_TIMEOUT_SECONDS=300 panama test --safe
tests/setup/interview-contract # or one directly; they are plain executables tests/setup/interview-contract # or one directly; they are plain executables
``` ```
`--safe` means hermetic contracts only. A plain full run prompts in a terminal
before it starts any non-hermetic contract. Automation cannot answer that
prompt, so it must grant every required capability with a repeatable `--allow`.
Each contract has a 180-second outer timeout by default. Set
`PANAMA_TEST_TIMEOUT_SECONDS` to a positive integer to override it. When a
contract fails, the runner prints its captured stdout and stderr. Successful
stdout stays quiet. Successful stderr is surfaced as a warning.
They are called contracts rather than unit tests because that is what they are: They are called contracts rather than unit tests because that is what they are:
each one pins a decision that was expensive to get right and is cheap to undo by each one pins a decision that was expensive to get right and is cheap to undo by
accident. Most read or measure the real thing — launching a shell to measure a accident. Most read or measure the real thing — launching a shell to measure a
@@ -243,8 +304,9 @@ panama sync # review, commit and push your changes to this repo
panama edit # open it in Neovim panama edit # open it in Neovim
panama doctor # what is actually running, not what was installed panama doctor # what is actually running, not what was installed
panama diagnose # hand the health summary and recent errors to your agent panama diagnose # hand the health summary and recent errors to your agent
panama test # every contract, or a subset by pattern panama test # every contract, prompting before non-hermetic work
panama test --safe # the same, minus the ones that take over the desktop panama test --safe # hermetic contracts only
panama test --allow live-host updates # grant one capability to automation
panama contracts <file> # which contracts mention a file, and can they be run panama contracts <file> # which contracts mention a file, and can they be run
panama migrate # apply repairs this machine has not had yet panama migrate # apply repairs this machine has not had yet
panama upgrade # re-run ./install from anywhere, interview and all panama upgrade # re-run ./install from anywhere, interview and all
+370 -109
View File
@@ -10,7 +10,7 @@
# edit Open the Panama repo in Neovim # edit Open the Panama repo in Neovim
# doctor Report what is actually running on this machine # doctor Report what is actually running on this machine
# diagnose Hand this machine's health and recent errors to your agent # diagnose Hand this machine's health and recent errors to your agent
# test Run every contract under tests/ (--safe skips the hijacking ones) # test Run contracts classified by tests/contracts.manifest
# contracts Name the contracts that mention a given file # contracts Name the contracts that mention a given file
# upgrade Re-run the installer from anywhere, interview included # upgrade Re-run the installer from anywhere, interview included
# migrate Apply repairs this machine has not had yet # migrate Apply repairs this machine has not had yet
@@ -57,7 +57,7 @@ header(){ printf '\n%s%s%s\n' "${MAGENTA}${BOLD}" "$*" "$RESET"; }
# Ask a yes/no question. Returns 0 for yes, 1 for no. Default = no. # Ask a yes/no question. Returns 0 for yes, 1 for no. Default = no.
confirm() { confirm() {
local prompt="$1" reply local prompt="$1" reply
printf '%s?%s %s %s[y/N]%s ' "${CYAN}${BOLD}" "$RESET" "$prompt" "$BOLD" "$RESET" printf '%s?%s %s %s[y/N]%s ' "${CYAN}${BOLD}" "$RESET" "$prompt" "$BOLD" "$RESET" >&2
read -r reply || true read -r reply || true
[[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]] [[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]]
} }
@@ -85,12 +85,18 @@ ${BOLD}Commands:${RESET}
${GREEN}diagnose${RESET} Hand the health summary, the recent journal errors and ${GREEN}diagnose${RESET} Hand the health summary, the recent journal errors and
whatever you say is wrong to your coding agent, in a terminal. whatever you say is wrong to your coding agent, in a terminal.
Needs an agent chosen on Settings System Agents. Needs an agent chosen on Settings System Agents.
${GREEN}test${RESET} Run every contract under tests/. Give it a pattern to run ${GREEN}test${RESET} Run contracts classified by tests/contracts.manifest. Give it a
a subset: 'panama test dock' runs the ones matching 'dock'. pattern to run a subset. --safe selects hermetic contracts only.
--safe skips the ones that take over the live desktop; what Plain terminal runs prompt before non-hermetic work. Automation
they are and why is tests/desktop-hijacking. must grant each required capability with a repeatable --allow.
${GREEN}contracts${RESET} Name the contracts that mention a given file, each marked Each non-hermetic contract announces its exact capabilities
safe or desktop. A heuristic over the text of tests/, so it before it starts.
Failures print captured stdout/stderr. Successful stdout stays
quiet; successful stderr is a warning. The default outer timeout
is 180 seconds. Set PANAMA_TEST_TIMEOUT_SECONDS to a positive
integer to override it.
${GREEN}contracts${RESET} Name the contracts that mention a given file, each labeled
with manifest capabilities. A heuristic over the text of tests/, so it
answers "what should I run" rather than "what covers this". answers "what should I run" rather than "what covers this".
${GREEN}upgrade${RESET} Re-run ./install from anywhere, interview and all. For a new ${GREEN}upgrade${RESET} Re-run ./install from anywhere, interview and all. For a new
machine, or to change an answer you gave. Routine updates are machine, or to change an answer you gave. Routine updates are
@@ -121,6 +127,9 @@ ${BOLD}Examples:${RESET}
$PROGRAM diagnose the bar disappears after unplugging the monitor $PROGRAM diagnose the bar disappears after unplugging the monitor
$PROGRAM test dock $PROGRAM test dock
$PROGRAM test --safe $PROGRAM test --safe
$PROGRAM test --allow live-host updates
$PROGRAM test --allow live-compositor keybinds
PANAMA_TEST_TIMEOUT_SECONDS=300 $PROGRAM test --safe
$PROGRAM contracts config/dot/quickshell/services/Displays.qml $PROGRAM contracts config/dot/quickshell/services/Displays.qml
$PROGRAM upgrade $PROGRAM upgrade
$PROGRAM apps $PROGRAM apps
@@ -431,143 +440,394 @@ PROMPT
} }
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# The desktop-hijacking ledger # Contract manifest
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# #
# tests/desktop-hijacking lists, one repo-relative path per line with a '#' # The manifest is the runtime authority for every collected contract. An absent
# comment saying what it does to the live session, the contracts that drive the # manifest is unsafe: this command must never infer that unclassified tests are
# real shell, compositor or machine rather than a harness. Read by `test --safe` # hermetic.
# to decide what to skip, and by `contracts` to mark each hit. CONTRACT_MANIFEST="tests/contracts.manifest"
# CONTRACT_CAPABILITIES=(hermetic live-host live-compositor live-desktop network privileged)
# Prints the paths, comments and blank lines stripped. A missing ledger prints
# nothing: no ledger means nothing is known to hijack, which is the honest
# reading of an absent file and keeps `--safe` from failing on a fresh checkout.
DESKTOP_HIJACKING_LEDGER="tests/desktop-hijacking"
hijacking_entries() { contract_paths() {
local ledger="$PANAMA_DIR/$DESKTOP_HIJACKING_LEDGER" line local candidate
[[ -r "$ledger" ]] || return 0 while IFS= read -r candidate; do
[[ -x "$candidate" || "$candidate" == *_test.py ]] || continue
printf 'tests/%s\n' "${candidate#"$PANAMA_DIR/tests/"}"
done < <(find "$PANAMA_DIR/tests" -type f \
-not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
}
contract_manifest_entries() {
local line capabilities path
while IFS= read -r line || [[ -n "$line" ]]; do while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}" [[ "$line" =~ ^[[:space:]]*(#|$) ]] && continue
line="${line#"${line%%[![:space:]]*}"}" IFS=$' \t' read -r capabilities path <<<"$line"
line="${line%"${line##*[![:space:]]}"}" printf '%s\t%s\n' "$path" "$capabilities"
[[ -n "$line" ]] && printf '%s\n' "$line" done < "$PANAMA_DIR/$CONTRACT_MANIFEST"
done < "$ledger" }
return 0
require_contract_manifest() {
[[ -r "$PANAMA_DIR/$CONTRACT_MANIFEST" ]] || {
err "Contract manifest is missing or unreadable: $PANAMA_DIR/$CONTRACT_MANIFEST"
return 1
}
}
validate_contract_manifest() {
require_contract_manifest || return 1
local manifest="$PANAMA_DIR/$CONTRACT_MANIFEST"
local line capabilities path extra previous_comment="" previous_was_comment=0
local previous_path="" capability discovered
local -a capability_list=() findings=()
local -A expected_contracts=() manifest_paths=()
while IFS= read -r discovered; do
expected_contracts["$discovered"]=1
done < <(contract_paths)
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*# ]]; then
previous_comment="${line#*#}"
previous_comment="${previous_comment#"${previous_comment%%[![:space:]]*}"}"
previous_comment="${previous_comment%"${previous_comment##*[![:space:]]}"}"
previous_was_comment=1
continue
fi
if [[ "$line" =~ ^[[:space:]]*$ ]]; then
previous_comment=""
previous_was_comment=0
continue
fi
IFS=$' \t' read -r capabilities path extra <<<"$line"
if [[ -z "${capabilities:-}" || -z "${path:-}" || -n "${extra:-}" ]]; then
findings+=("manifest line is not exactly two fields: $line")
previous_comment=""
previous_was_comment=0
continue
fi
if [[ -n "$previous_path" && "$path" < "$previous_path" ]]; then
findings+=('paths are not lexicographically sorted')
fi
previous_path="$path"
if [[ -n "${manifest_paths[$path]:-}" ]]; then
findings+=("duplicate path $path")
fi
manifest_paths["$path"]=1
local -A line_capabilities=()
if [[ "$capabilities" == ,* || "$capabilities" == *, || "$capabilities" == *,,* ]]; then
findings+=("empty capability on $path")
fi
IFS=',' read -r -a capability_list <<<"$capabilities"
for capability in "${capability_list[@]}"; do
[[ -n "$capability" ]] || continue
if [[ -n "${line_capabilities[$capability]:-}" ]]; then
findings+=("duplicate capability $capability on $path")
fi
line_capabilities["$capability"]=1
is_contract_capability "$capability" \
|| findings+=("unknown capability $capability on $path")
done
if [[ -n "${line_capabilities[hermetic]:-}" && ${#line_capabilities[@]} -ne 1 ]]; then
findings+=("hermetic must appear alone on $path")
fi
if [[ "$capabilities" != hermetic ]]; then
if (( previous_was_comment != 1 )); then
findings+=("$path is non-hermetic but lacks a directly preceding comment")
elif [[ -z "$previous_comment" ]]; then
findings+=("$path is non-hermetic but lacks a non-empty directly preceding comment")
fi
fi
previous_comment=""
previous_was_comment=0
done < "$manifest"
for discovered in "${!expected_contracts[@]}"; do
[[ -n "${manifest_paths[$discovered]:-}" ]] \
|| findings+=("missing contract $discovered")
done
for path in "${!manifest_paths[@]}"; do
[[ -n "${expected_contracts[$path]:-}" ]] \
|| findings+=("stale manifest path $path")
done
if (( ${#findings[@]} > 0 )); then
err "Contract manifest validation failed with ${#findings[@]} finding(s):"
printf ' - %s\n' "${findings[@]}" >&2
return 1
fi
}
test_usage() {
err "Usage: ${BOLD}$PROGRAM test [--safe] [--allow <capability>] [pattern]${RESET}"
return 2
}
is_contract_capability() {
local capability="$1" known
for known in "${CONTRACT_CAPABILITIES[@]}"; do
[[ "$capability" == "$known" ]] && return 0
done
return 1
} }
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Command: test # Command: test
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# #
# The contracts are the main safety net in this repository and had no entry # The contracts are the main safety net in this repository. The manifest is the
# point: 121 executables with no runner and no mention in the README, which is # single list of what the runner executes and which external boundaries each
# most of the way to not having them. # contract reaches.
# #
# Each runs in its own process and a failure does not stop the rest, because the # Each runs in its own process and a failure does not stop the rest, because the
# useful output is the whole list of what is broken rather than the first thing # useful output is the whole list of what is broken rather than the first thing
# that broke. The exit code is what a caller can act on. # that broke. The exit code is what a caller can act on.
# #
# --safe exists because a fair number of these contracts ARE the desktop: they # --safe runs only contracts the manifest classifies as hermetic and reports
# open overlays, restart the shell, move your windows. Running the suite while # each external capability it skipped. A plain terminal run asks before any
# sitting in front of the machine used to mean losing the session for a few # selected non-hermetic work. Automation must grant every required capability
# minutes, so the honest options were "run everything" or "run nothing". --safe # with repeatable --allow flags. Non-hermetic contracts announce their exact
# is the third: skip exactly what tests/desktop-hijacking names, and say how # capability list before execution. Each contract gets an outer timeout, 180
# many were skipped, so the gap is stated rather than implied. # seconds by default. PANAMA_TEST_TIMEOUT_SECONDS accepts a positive integer
# override. Failures include captured stdout and stderr. Successful stdout
# stays quiet, while successful stderr is surfaced as a warning.
PANAMA_ACTIVE_CONTRACT_PID=""
PANAMA_CONTRACT_CAPTURE_DIR=""
cleanup_contract_capture() {
if [[ -n "$PANAMA_CONTRACT_CAPTURE_DIR" && -d "$PANAMA_CONTRACT_CAPTURE_DIR" ]]; then
rm -rf -- "$PANAMA_CONTRACT_CAPTURE_DIR" || true
fi
PANAMA_CONTRACT_CAPTURE_DIR=""
}
terminate_active_contract() {
local pid="$PANAMA_ACTIVE_CONTRACT_PID"
PANAMA_ACTIVE_CONTRACT_PID=""
[[ "$pid" =~ ^[1-9][0-9]*$ && "$pid" != "$$" ]] || return 0
# GNU timeout owns a process group whose ID is its PID. Signal that complete
# group so a contract cannot leave descendants behind, with a direct-PID
# fallback for implementations that do not create the group.
kill -TERM -- "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
}
handle_contract_signal() {
local signal_status="$1"
trap - INT TERM
terminate_active_contract
cleanup_contract_capture
trap - EXIT
exit "$signal_status"
}
prepare_contract_capture() {
local capture_dir=""
if ! capture_dir="$(mktemp -d)"; then
err 'Could not create contract capture directory.'
return 1
fi
if [[ -z "$capture_dir" || ! -d "$capture_dir" ]]; then
err 'Could not create contract capture directory.'
return 1
fi
PANAMA_CONTRACT_CAPTURE_DIR="$capture_dir"
trap cleanup_contract_capture EXIT
trap 'handle_contract_signal 130' INT
trap 'handle_contract_signal 143' TERM
}
cmd_test() { cmd_test() {
local pattern="" safe=0 arg local timeout_seconds="${PANAMA_TEST_TIMEOUT_SECONDS:-180}"
# Position-independent, because 'panama test --safe dock' and [[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || {
# 'panama test dock --safe' are the same intent and nobody should have to err 'PANAMA_TEST_TIMEOUT_SECONDS must be a positive integer.'
# remember which one this accepts. return 2
for arg in "$@"; do }
validate_contract_manifest || return 1
cmd_test_impl "$timeout_seconds" "$@"
}
cmd_test_impl() {
local timeout_seconds="$1"
shift
local pattern="" safe=0 arg capability capabilities rel path
local -A grants=() manifest_capabilities=() skipped_counts=() missing_grants=() selected_capabilities=()
local -a suite=() missing_capability_list=() selected_capability_list=() capability_list=()
# Position-independent: flags can precede or follow the optional pattern.
while (( $# > 0 )); do
arg="$1"
shift
case "$arg" in case "$arg" in
--safe) safe=1 ;; --safe) safe=1 ;;
*) pattern="$arg" ;; --allow)
(( $# > 0 )) || { test_usage; return 2; }
capability="$1"
shift
is_contract_capability "$capability" || {
err "Unknown contract capability: $capability"
return 2
}
[[ "$capability" != hermetic ]] || {
err 'hermetic contracts do not need --allow.'
return 2
}
grants["$capability"]=1
;;
--*) test_usage; return 2 ;;
*)
[[ -z "$pattern" ]] || { test_usage; return 2; }
pattern="$arg"
;;
esac esac
done done
local -a suite=() (( safe == 0 || ${#grants[@]} == 0 )) || {
local -A hijacking=() err '--safe cannot be combined with --allow.'
local skipped=0 entry rel return 2
}
if (( safe )); then while IFS=$'\t' read -r rel capabilities; do
while IFS= read -r entry; do manifest_capabilities["$rel"]="$capabilities"
hijacking["$entry"]=1 [[ -z "$pattern" || "$rel" == *"$pattern"* ]] || continue
done < <(hijacking_entries) if (( safe )) && [[ "$capabilities" != hermetic ]]; then
fi IFS=',' read -r -a capability_list <<<"$capabilities"
for capability in "${capability_list[@]}"; do
# Executables, plus the Python suites. Those are unittest files rather than (( ++skipped_counts["$capability"] ))
# executables, and collecting only what has the executable bit would skip them done
# without saying so -- which is how all three came to be run by nothing at all.
# A runner with a blind spot is worse than no runner, because it reports PASS.
while IFS= read -r path; do
[[ -x "$path" || "$path" == *_test.py ]] || continue
[[ -z "$pattern" || "$path" == *"$pattern"* ]] || continue
rel="tests/${path#"$PANAMA_DIR"/tests/}"
if (( safe )) && [[ -n "${hijacking[$rel]:-}" ]]; then
(( ++skipped ))
continue continue
fi fi
suite+=("$path") suite+=("$rel")
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort) done < <(contract_manifest_entries)
if (( ${#suite[@]} == 0 )); then if (( ${#suite[@]} == 0 )); then
# "Nothing matched" and "everything that matched was skipped" are different if (( safe )) && (( ${#skipped_counts[@]} > 0 )); then
# answers, and reporting the first for the second is how --safe would come err "Every contract matching '${pattern}' needs an external capability; --safe skipped all of them."
# to look like a broken pattern.
if (( skipped > 0 )); then
err "Every contract matching '${pattern}' is desktop-hijacking; --safe skipped all ${skipped}."
printf ' What they do to the session: %s/%s\n' "$PANAMA_DIR" "$DESKTOP_HIJACKING_LEDGER" >&2
else else
err "No contracts match '${pattern}'" err "No contracts match '${pattern}'"
fi fi
exit 1 return 1
fi fi
if (( safe )); then
for capability in "${CONTRACT_CAPABILITIES[@]}"; do
[[ "$capability" == hermetic ]] && continue
printf 'Skipped %d %s contract(s).\n' "${skipped_counts[$capability]:-0}" "$capability"
done
else
for rel in "${suite[@]}"; do
capabilities="${manifest_capabilities[$rel]}"
[[ "$capabilities" == hermetic ]] && continue
IFS=',' read -r -a capability_list <<<"$capabilities"
for capability in "${capability_list[@]}"; do
selected_capabilities["$capability"]=1
[[ -n "${grants[$capability]:-}" ]] || missing_grants["$capability"]=1
done
done
for capability in "${CONTRACT_CAPABILITIES[@]}"; do
[[ "$capability" == hermetic ]] && continue
[[ -n "${selected_capabilities[$capability]:-}" ]] && selected_capability_list+=("$capability")
[[ -n "${missing_grants[$capability]:-}" ]] && missing_capability_list+=("$capability")
done
if (( ${#missing_capability_list[@]} > 0 )); then
if [[ -t 0 && -t 2 ]]; then
confirm "Run ${#suite[@]} contract(s) requiring: ${selected_capability_list[*]}?" || {
warn 'No contracts were run.'
return 1
}
else
err "Selected contracts require: ${missing_capability_list[*]}."
for capability in "${missing_capability_list[@]}"; do
printf ' Automation: pass --allow %s\n' "$capability" >&2
done
return 1
fi
fi
fi
local capture_dir stdout_file stderr_file name run_status index=0 final_status=0
local -a failed=() runner=()
prepare_contract_capture || return 1
capture_dir="$PANAMA_CONTRACT_CAPTURE_DIR"
info "Running ${#suite[@]} contract(s)" info "Running ${#suite[@]} contract(s)"
local -a failed=() for index in "${!suite[@]}"; do
local path name rel="${suite[$index]}"
local -a runner path="$PANAMA_DIR/$rel"
for path in "${suite[@]}"; do name="${rel#tests/}"
name="${path#"$PANAMA_DIR"/tests/}" stdout_file="$capture_dir/$index.stdout"
stderr_file="$capture_dir/$index.stderr"
if [[ "$path" == *_test.py ]]; then if [[ "$path" == *_test.py ]]; then
runner=(python3 "$path") runner=(python3 "$path")
else else
runner=("$path") runner=("$path")
fi fi
if "${runner[@]}" >/dev/null 2>&1; then capabilities="${manifest_capabilities[$rel]}"
ok "$name" if [[ "$capabilities" != hermetic ]]; then
else info "Running $name [$capabilities]"
err "$name"
failed+=("$name")
fi fi
run_status=0
timeout --signal=TERM --kill-after=5 "$timeout_seconds" \
"${runner[@]}" >"$stdout_file" 2>"$stderr_file" &
PANAMA_ACTIVE_CONTRACT_PID=$!
wait "$PANAMA_ACTIVE_CONTRACT_PID" || run_status=$?
PANAMA_ACTIVE_CONTRACT_PID=""
if (( run_status == 0 )); then
ok "$name"
if [[ -s "$stderr_file" ]]; then
warn "$name wrote to stderr:"
cat "$stderr_file" >&2
fi
continue
fi
if (( run_status == 124 || run_status == 137 )); then
err "$name timed out after ${timeout_seconds}s"
else
err "$name failed (exit $run_status)"
fi
[[ -s "$stdout_file" ]] && {
printf '%s stdout:\n' "$name" >&2
cat "$stdout_file" >&2
}
[[ -s "$stderr_file" ]] && {
printf '%s stderr:\n' "$name" >&2
cat "$stderr_file" >&2
}
failed+=("$name")
done done
header "Result" header "Result"
if (( ${#failed[@]} == 0 )); then if (( ${#failed[@]} == 0 )); then
ok "${#suite[@]} contract(s) passed" ok "${#suite[@]} contract(s) passed"
if (( safe )); then else
printf 'Skipped %d desktop-hijacking contract(s) (%s).\n' "$skipped" "$DESKTOP_HIJACKING_LEDGER"
fi
return 0
fi
err "${#failed[@]} of ${#suite[@]} failed:" err "${#failed[@]} of ${#suite[@]} failed:"
printf ' %s\n' "${failed[@]}" >&2 printf ' %s\n' "${failed[@]}" >&2
warn "Run one on its own to see why: ${BOLD}${PANAMA_DIR}/tests/<name>${RESET}" final_status=1
if (( safe )); then
printf 'Skipped %d desktop-hijacking contract(s) (%s).\n' "$skipped" "$DESKTOP_HIJACKING_LEDGER"
fi fi
return 1
cleanup_contract_capture
trap - EXIT INT TERM
return "$final_status"
} }
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Command: contracts # Command: contracts
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# #
# "I changed this file -- what should I run?" There are 177 contracts and no # "I changed this file -- what should I run?" The suite is large enough that
# index, so the honest answers were "all of them" (minutes, and half of them # running all of it or guessing from contract names are both poor answers.
# take the desktop away) or "the ones whose name sounds related" (which is how
# a covering contract gets skipped).
# #
# This is a grep, and says so. A contract that names the file, or a # This is a grep, and says so. A contract that names the file, or a
# parent-trimmed suffix of it, or just its basename, is a contract worth # parent-trimmed suffix of it, or just its basename, is a contract worth
@@ -577,8 +837,8 @@ cmd_test() {
# a real answer -- exit 1 so a script can tell the difference -- but it is a # a real answer -- exit 1 so a script can tell the difference -- but it is a
# statement about this search, not about the file. # statement about this search, not about the file.
# #
# Each hit is marked from tests/desktop-hijacking, so the output also answers # Each hit is labeled from tests/contracts.manifest, so the output also answers
# "and can I run them right now". # which boundary the matching contract reaches.
cmd_contracts() { cmd_contracts() {
local target="${1:-}" local target="${1:-}"
if [[ -z "$target" ]]; then if [[ -z "$target" ]]; then
@@ -621,21 +881,22 @@ cmd_contracts() {
suffix="${suffix#*/}" suffix="${suffix#*/}"
done done
local -A hijacking=() validate_contract_manifest || return 1
local entry local -A manifest_capabilities=()
while IFS= read -r entry; do local capabilities
hijacking["$entry"]=1 while IFS=$'\t' read -r rel capabilities; do
done < <(hijacking_entries) manifest_capabilities["$rel"]="$capabilities"
done < <(contract_manifest_entries)
# The same collection `test` runs, so anything named here is something the # The same collection `test` runs, so anything named here is something the
# runner would actually execute. # runner would actually execute.
local -a hits=() local -a hits=()
local candidate rel local candidate rel
while IFS= read -r candidate; do while IFS= read -r rel; do
[[ -x "$candidate" || "$candidate" == *_test.py ]] || continue candidate="$PANAMA_DIR/$rel"
grep -qF "${patterns[@]}" "$candidate" 2>/dev/null || continue grep -qF "${patterns[@]}" "$candidate" 2>/dev/null || continue
hits+=("tests/${candidate#"$PANAMA_DIR"/tests/}") hits+=("$rel")
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort) done < <(contract_paths)
if (( ${#hits[@]} == 0 )); then if (( ${#hits[@]} == 0 )); then
printf 'No contract mentions %s — coverage may be indirect (a harness or a generated artifact); nothing verified.\n' "$path" >&2 printf 'No contract mentions %s — coverage may be indirect (a harness or a generated artifact); nothing verified.\n' "$path" >&2
@@ -643,11 +904,11 @@ cmd_contracts() {
fi fi
for rel in "${hits[@]}"; do for rel in "${hits[@]}"; do
if [[ -n "${hijacking[$rel]:-}" ]]; then [[ -n "${manifest_capabilities[$rel]:-}" ]] || {
printf '%s [desktop]\n' "$rel" err "Contract has no manifest capability label: $rel"
else return 1
printf '%s [safe]\n' "$rel" }
fi printf '%s [%s]\n' "$rel" "${manifest_capabilities[$rel]}"
done done
} }
+478 -55
View File
@@ -2,8 +2,9 @@
# Panama's front door: the one command a fresh Fedora machine needs. # Panama's front door: the one command a fresh Fedora machine needs.
# #
# bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot) # Download this file from the documented commit URL, verify its documented
# bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot) --server # SHA-256, then pass both immutable values as PANAMA_BOOT_REVISION and
# PANAMA_BOOT_SHA256 when invoking it.
# #
# Deliberately dumb, because a copy of this script leaves the repository the # Deliberately dumb, because a copy of this script leaves the repository the
# moment somebody curls it -- nothing here can be fixed by re-running # moment somebody curls it -- nothing here can be fixed by re-running
@@ -17,9 +18,144 @@
set -euo pipefail set -euo pipefail
if [[ ! "${PANAMA_BOOT_REVISION:-}" =~ ^[0-9a-f]{40}$ ]]; then
echo "boot: PANAMA_BOOT_REVISION must be a full lowercase commit" >&2
exit 1
fi
if [[ ! "${PANAMA_BOOT_SHA256:-}" =~ ^[0-9a-f]{64}$ ]]; then
echo "boot: PANAMA_BOOT_SHA256 must be a lowercase SHA-256" >&2
exit 1
fi
actual_boot_sha="$(sha256sum "${BASH_SOURCE[0]}" | cut -d' ' -f1)"
if [[ "$actual_boot_sha" != "$PANAMA_BOOT_SHA256" ]]; then
echo "boot: downloaded boot file does not match PANAMA_BOOT_SHA256" >&2
exit 1
fi
REPO_URL="https://git.gbrown.org/gib/Panama.git" REPO_URL="https://git.gbrown.org/gib/Panama.git"
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}" PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
export PANAMA_PATH export PANAMA_PATH
BOOTSTRAP_USER=""
checkout_command() {
if [[ -n "$BOOTSTRAP_USER" ]]; then
runuser -u "$BOOTSTRAP_USER" -- "$@"
else
"$@"
fi
}
# Git's index hints are performance promises, not trust evidence. In
# particular, assume-unchanged and skip-worktree can make porcelain status
# report a clean checkout whose files no longer match HEAD. Compare every
# tracked blob and Git mode with the verified commit before handing control to
# any file in the worktree.
checkout_matches_verified_commit() (
local checkout="$1" listing="" entry metadata mode type expected path actual
local link_target_with_sentinel link_target
trap '[[ -z "$listing" ]] || rm -f -- "$listing"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
listing="$(mktemp -u -t panama-boot-tree.XXXXXX)" || exit 1
umask 077
if ! (set -o noclobber; : >"$listing") 2>/dev/null; then
listing=""
exit 1
fi
checkout_command git -C "$checkout" ls-tree -rz --full-tree \
"$PANAMA_BOOT_REVISION" >"$listing" || exit 1
while IFS= read -r -d '' entry; do
[[ "$entry" == *$'\t'* ]] || exit 1
metadata="${entry%%$'\t'*}"
path="${entry#*$'\t'}"
read -r mode type expected <<<"$metadata"
[[ "$type" == blob && -n "$path" && "$path" != /* ]] || exit 1
case "$mode" in
100644) [[ -f "$checkout/$path" && ! -L "$checkout/$path" \
&& ! -x "$checkout/$path" ]] || exit 1 ;;
100755) [[ -f "$checkout/$path" && ! -L "$checkout/$path" \
&& -x "$checkout/$path" ]] || exit 1 ;;
120000)
[[ -L "$checkout/$path" ]] || exit 1
# hash-object given a pathname follows a symlink. Git's 120000 blob is
# the link text itself, including any trailing newlines, so preserve
# those bytes with a sentinel and hash stdin instead.
link_target_with_sentinel="$(
readlink -n -- "$checkout/$path" && printf .
)" || exit 1
[[ "$link_target_with_sentinel" == *. ]] || exit 1
link_target="${link_target_with_sentinel%.}"
actual="$(
printf '%s' "$link_target" \
| checkout_command git -C "$checkout" hash-object --stdin
)" || exit 1
[[ "$actual" == "$expected" ]] || exit 1
continue
;;
*) exit 1 ;;
esac
actual="$(checkout_command git -C "$checkout" hash-object --no-filters -- "$path")" \
|| exit 1
[[ "$actual" == "$expected" ]] || exit 1
done <"$listing"
)
prepare_panama_checkout() {
local checkout="$1" actual_head checkout_status
if [[ -e "$checkout" && ! -d "$checkout/.git" ]]; then
printf 'boot: %s exists but is not a Panama Git checkout\n' "$checkout" >&2
return 1
fi
if [[ -d "$checkout/.git" ]]; then
checkout_status="$(checkout_command git -C "$checkout" status --porcelain)" || {
printf 'boot: could not inspect the existing checkout at %s\n' "$checkout" >&2
return 1
}
if [[ -n "$checkout_status" ]]; then
printf 'boot: existing checkout at %s has local changes\n' "$checkout" >&2
return 1
fi
echo "Panama is already cloned at $checkout; fetching the verified revision"
checkout_command git -C "$checkout" fetch origin "$PANAMA_BOOT_REVISION" || return 1
if ! checkout_command git -C "$checkout" merge-base --is-ancestor \
HEAD "$PANAMA_BOOT_REVISION"; then
echo "boot: existing checkout cannot fast-forward to the verified revision" >&2
return 1
fi
checkout_command git -C "$checkout" merge --ff-only "$PANAMA_BOOT_REVISION" || return 1
else
checkout_command mkdir -p "$(dirname "$checkout")" || return 1
checkout_command git init "$checkout" || return 1
checkout_command git -C "$checkout" remote add origin "$REPO_URL" || return 1
checkout_command git -C "$checkout" fetch --depth=1 origin \
"$PANAMA_BOOT_REVISION" || return 1
checkout_command git -C "$checkout" checkout --detach \
"$PANAMA_BOOT_REVISION" || return 1
actual_head="$(checkout_command git -C "$checkout" rev-parse 'HEAD^{commit}')" || return 1
if [[ "$actual_head" != "$PANAMA_BOOT_REVISION" ]]; then
echo "boot: fetched checkout does not match PANAMA_BOOT_REVISION" >&2
return 1
fi
checkout_command git -C "$checkout" checkout -b main || return 1
checkout_command git -C "$checkout" config branch.main.remote origin || return 1
checkout_command git -C "$checkout" config branch.main.merge refs/heads/main || return 1
fi
actual_head="$(checkout_command git -C "$checkout" rev-parse 'HEAD^{commit}')" || return 1
if [[ "$actual_head" != "$PANAMA_BOOT_REVISION" ]]; then
echo "boot: checkout HEAD does not match PANAMA_BOOT_REVISION" >&2
return 1
fi
}
SERVER=0 SERVER=0
INSTALL_ARGS=() INSTALL_ARGS=()
@@ -33,6 +169,290 @@ for arg in "$@"; do
esac esac
done done
# Keep the worktree comparison at the last possible boundary. Checkout
# preparation may invoke several commands and return to the caller; performing
# the byte/mode/link check here ensures a change in that interval is rejected
# before any tracked file is executed.
verified_install_handoff() {
local use_tty="$1"
if ! checkout_matches_verified_commit "$PANAMA_PATH"; then
echo "boot: checkout files do not match PANAMA_BOOT_REVISION" >&2
return 1
fi
if [[ -n "$BOOTSTRAP_USER" ]]; then
if (( use_tty )); then
exec runuser -u "$BOOTSTRAP_USER" -- env PANAMA_PATH="$PANAMA_PATH" \
"$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
fi
exec runuser -u "$BOOTSTRAP_USER" -- env PANAMA_PATH="$PANAMA_PATH" \
"$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
fi
if (( use_tty )); then
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
fi
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
}
# The public bootstrap contract runs this branch as an ordinary user with a
# stubbed root identity. Keep its filesystem adapter unavailable to a real root
# shell so it cannot redirect a real installation by accident.
BOOT_ROOT="${PANAMA_BOOT_FIXTURE_ROOT:-}"
if [[ -n "$BOOT_ROOT" && "$EUID" -eq 0 ]]; then
echo "boot: PANAMA_BOOT_FIXTURE_ROOT is test-only" >&2
exit 1
fi
system_path() {
local path="$1"
[[ "$path" == /* ]] || return 2
printf '%s%s\n' "$BOOT_ROOT" "$path"
}
valid_authorized_keys() {
local keys="$1" line saw_key=0
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]]; then
continue
fi
if ! ssh-keygen -l -f /dev/stdin >/dev/null 2>&1 <<<"$line"; then
return 1
fi
saw_key=1
done <"$keys"
(( saw_key ))
}
safe_authorized_keys() {
local username="$1" user_home="$2" uid ssh_dir keys
uid="$(id -u "$username")" || return 1
[[ "$uid" =~ ^[0-9]+$ && "$uid" != 0 && "$user_home" == /* ]] || return 1
ssh_dir="$user_home/.ssh"
keys="$ssh_dir/authorized_keys"
[[ -d "$ssh_dir" && ! -L "$ssh_dir" && -f "$keys" && ! -L "$keys" ]] || return 1
[[ "$(stat -Lc '%u:%a' "$ssh_dir")" == "$uid:700" ]] || return 1
[[ "$(stat -Lc '%u:%a' "$keys")" == "$uid:600" ]] || return 1
valid_authorized_keys "$keys"
}
safe_root_authorized_keys() {
local keys
keys="$(system_path /root/.ssh/authorized_keys)" || return 1
[[ -f "$keys" && ! -L "$keys" ]] || return 1
[[ "$(stat -Lc '%u:%a' "$keys")" == '0:600' ]] || return 1
valid_authorized_keys "$keys"
}
detect_ssh_unit() {
local unit
for unit in sshd.service ssh.service; do
systemctl cat "$unit" >/dev/null 2>&1 && {
printf '%s\n' "$unit"
return 0
}
done
return 1
}
restore_ssh_dropin() {
local restore
if (( ssh_had_prior )); then
[[ -n "$ssh_backup" && -f "$ssh_backup" && ! -L "$ssh_backup" ]] || return 1
restore="$(mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.restore)" || return 1
if ! cp -a -- "$ssh_backup" "$restore"; then
remove_ssh_artifact "$restore" || true
return 1
fi
if ! mv -f -- "$restore" "$ssh_dropin"; then
remove_ssh_artifact "$restore" || true
return 1
fi
else
remove_ssh_artifact "$ssh_dropin"
fi
}
restore_ssh_transaction_traps() {
trap - EXIT INT TERM
[[ -n "${ssh_saved_exit_trap:-}" ]] && eval "$ssh_saved_exit_trap"
[[ -n "${ssh_saved_int_trap:-}" ]] && eval "$ssh_saved_int_trap"
[[ -n "${ssh_saved_term_trap:-}" ]] && eval "$ssh_saved_term_trap"
return 0
}
remove_ssh_artifact() {
local artifact="$1"
[[ -n "$artifact" && ( -e "$artifact" || -L "$artifact" ) ]] || return 0
if rm -f -- "$artifact"; then
return 0
fi
printf 'SSH transaction cleanup failed. Retained artifact: %s\n' "$artifact" >&2
printf ' rm -f -- %q\n' "$artifact" >&2
return 1
}
print_ssh_recovery() {
if (( ssh_had_prior )); then
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
else
printf 'SSH rollback needs manual recovery. No prior drop-in existed.\n' >&2
printf ' rm -f -- %q\n' "$ssh_dropin" >&2
fi
printf ' sshd -t\n' >&2
printf ' systemctl reload %s\n' "$ssh_unit" >&2
}
policy_is_no() {
local policy="$1" setting="$2"
awk -v setting="$setting" '
$1 == setting { count += 1; if ($2 != "no") bad = 1 }
END { exit count != 1 || bad }
' <<<"$policy"
}
effective_ssh_policy_is_hardened() {
local username="$1" root_policy target_policy context
context='host=localhost,addr=127.0.0.1'
root_policy="$(sshd -T -C "user=root,$context")" || return 1
policy_is_no "$root_policy" permitrootlogin || return 1
policy_is_no "$root_policy" passwordauthentication || return 1
policy_is_no "$root_policy" kbdinteractiveauthentication || return 1
target_policy="$(sshd -T -C "user=$username,$context")" || return 1
policy_is_no "$target_policy" passwordauthentication || return 1
policy_is_no "$target_policy" kbdinteractiveauthentication
}
rollback_ssh_transaction() {
local reload_restored="$1" rollback_failed=0
restore_ssh_dropin || rollback_failed=1
sshd -t || rollback_failed=1
if (( reload_restored )); then
systemctl reload "$ssh_unit" || rollback_failed=1
fi
ssh_transaction_state=""
restore_ssh_transaction_traps
if (( rollback_failed )); then
print_ssh_recovery
else
remove_ssh_artifact "$ssh_backup" || true
fi
return 1
}
handle_ssh_transaction_exit() {
if [[ "$ssh_transaction_state" == preparing \
|| ( "$ssh_transaction_state" == activating && -e "$ssh_candidate" ) ]]; then
remove_ssh_artifact "$ssh_candidate" || true
remove_ssh_artifact "$ssh_backup" || true
elif [[ "$ssh_transaction_state" == activating || "$ssh_transaction_state" == activated ]]; then
restore_ssh_dropin || true
fi
}
handle_ssh_transaction_signal() {
local signal_status="$1"
trap - INT TERM
if [[ "$ssh_transaction_state" == preparing \
|| ( "$ssh_transaction_state" == activating && -e "$ssh_candidate" ) ]]; then
ssh_transaction_state=""
restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_candidate" || true
remove_ssh_artifact "$ssh_backup" || true
else
rollback_ssh_transaction 1 || true
fi
exit "$signal_status"
}
harden_server_ssh() {
local username="$1" user_home="$2" sshd_dir ssh_dropin harden ssh_unit
local ssh_candidate="" ssh_backup="" ssh_had_prior=0
local ssh_transaction_state=""
local ssh_saved_exit_trap ssh_saved_int_trap ssh_saved_term_trap
sshd_dir="$(system_path /etc/ssh/sshd_config.d)" || return 1
ssh_dropin="$sshd_dir/00-panama.conf"
if [[ -L "$ssh_dropin" || ( -e "$ssh_dropin" && ! -f "$ssh_dropin" ) ]]; then
printf 'SSH hardening unavailable: %s is not a regular file\n' "$ssh_dropin" >&2
return 2
fi
if ! ssh_unit="$(detect_ssh_unit)"; then
echo "SSH hardening unavailable: neither sshd.service nor ssh.service is installed" >&2
return 2
fi
printf 'Harden sshd (disable root, password, and keyboard-interactive authentication)? [Y/n]: '
read -r harden </dev/tty || harden=""
if [[ "$harden" =~ ^[Nn] ]]; then
return 0
fi
ssh_saved_exit_trap="$(trap -p EXIT)"
ssh_saved_int_trap="$(trap -p INT)"
ssh_saved_term_trap="$(trap -p TERM)"
ssh_transaction_state=preparing
trap 'handle_ssh_transaction_exit' EXIT
trap 'handle_ssh_transaction_signal 130' INT
trap 'handle_ssh_transaction_signal 143' TERM
if ! ssh_candidate="$(umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.tmp)"; then
ssh_transaction_state=""
restore_ssh_transaction_traps
return 1
fi
if ! printf 'PermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\n' >"$ssh_candidate"; then
ssh_transaction_state=""
restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_candidate" || true
return 1
fi
if [[ -e "$ssh_dropin" ]]; then
ssh_had_prior=1
if ! ssh_backup="$(umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.backup)"; then
ssh_transaction_state=""
restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_candidate" || true
return 1
fi
if ! cp -a -- "$ssh_dropin" "$ssh_backup"; then
ssh_transaction_state=""
restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_candidate" || true
remove_ssh_artifact "$ssh_backup" || true
return 1
fi
fi
ssh_transaction_state=activating
if ! mv -f -- "$ssh_candidate" "$ssh_dropin"; then
ssh_transaction_state=""
restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_candidate" || true
remove_ssh_artifact "$ssh_backup" || true
return 1
fi
ssh_candidate=""
ssh_transaction_state=activated
if ! sshd -t || ! effective_ssh_policy_is_hardened "$username"; then
rollback_ssh_transaction 0 || true
return 1
fi
if ! systemctl reload "$ssh_unit"; then
rollback_ssh_transaction 1 || true
return 1
fi
ssh_transaction_state=""
restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_backup" || return 1
echo "Wrote $ssh_dropin; make sure your key works before logging out."
}
# Panama assumes Fedora's repositories and package names. # Panama assumes Fedora's repositories and package names.
if ! grep -qi '^ID=fedora' /etc/os-release 2>/dev/null; then if ! grep -qi '^ID=fedora' /etc/os-release 2>/dev/null; then
echo "This looks like something other than Fedora; Panama only supports Fedora." >&2 echo "This looks like something other than Fedora; Panama only supports Fedora." >&2
@@ -84,83 +504,86 @@ if [[ "$(id -u)" -eq 0 ]]; then
passwd "$username" </dev/tty passwd "$username" </dev/tty
fi fi
# The key that reached root is the key that should reach the user, or the # Do not close root/password access until the account's key is an exact,
# next SSH login has no way in once root logins are closed below. # usable login path. The fixture adapter resolves these logical system paths
user_home="$(getent passwd "$username" | cut -d: -f6)" # beneath a temporary root; ordinary execution receives the original paths.
if [[ -s /root/.ssh/authorized_keys && ! -s "$user_home/.ssh/authorized_keys" ]]; then logical_user_home="$(getent passwd "$username" | cut -d: -f6)"
echo "Copying root's authorized_keys to $username" user_home=""
mkdir -p "$user_home/.ssh" if [[ "$logical_user_home" == /* ]]; then
cp /root/.ssh/authorized_keys "$user_home/.ssh/authorized_keys" user_home="$(system_path "$logical_user_home")" || true
chmod 700 "$user_home/.ssh" fi
chmod 600 "$user_home/.ssh/authorized_keys" bootstrap_home="$user_home"
chown -R "$username:$username" "$user_home/.ssh" if [[ -z "$bootstrap_home" ]]; then
bootstrap_home="$(system_path "/home/$username")"
fi fi
# Offered rather than imposed, defaulting to yes: a VPS keeps its provider's user_ssh_dir="$user_home/.ssh"
# web console, so locking password and root logins out of sshd is user_keys="$user_ssh_dir/authorized_keys"
# recoverable even when it goes wrong. Written as a drop-in so it never if [[ -n "$user_home" && ! -e "$user_keys" && ! -L "$user_keys" \
# fights the distribution's own sshd_config. && ! -L "$user_ssh_dir" ]] && safe_root_authorized_keys; then
SSHD_DROPIN=/etc/ssh/sshd_config.d/90-panama.conf copy_root_key=0
if [[ -f "$SSHD_DROPIN" ]]; then if [[ ! -e "$user_ssh_dir" ]]; then
echo "sshd is already hardened ($SSHD_DROPIN)" copy_root_key=1
elif [[ ! -d "$user_ssh_dir" \
|| "$(stat -Lc '%u:%a' "$user_ssh_dir")" != "$(id -u "$username"):700" ]]; then
echo "SSH hardening unavailable: $username has no safe authorized_keys" >&2
else else
printf 'Harden sshd (disable root login and password auth)? [Y/n]: ' copy_root_key=1
read -r harden </dev/tty || harden=""
if [[ ! "$harden" =~ ^[Nn] ]]; then
printf 'PermitRootLogin no\nPasswordAuthentication no\n' >"$SSHD_DROPIN"
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
echo "Wrote $SSHD_DROPIN; make sure your key works before logging out."
fi fi
if (( copy_root_key )); then
echo "Copying root's authorized_keys to $username"
root_keys="$(system_path /root/.ssh/authorized_keys)"
if ! runuser -u "$username" -- install -d -m 0700 -- "$user_ssh_dir" \
|| ! runuser -u "$username" -- install -m 0600 -- /dev/stdin "$user_keys" \
<"$root_keys"; then
echo "SSH hardening unavailable: could not install root's key for $username" >&2
fi
fi
fi
if safe_authorized_keys "$username" "$user_home"; then
harden_status=0
harden_server_ssh "$username" "$user_home" || harden_status=$?
if (( harden_status != 0 && harden_status != 2 )); then
echo "SSH hardening failed; stopping before install handoff." >&2
exit 1
fi
else
echo "SSH hardening unavailable: $username has no safe authorized_keys" >&2
fi fi
if ! command -v git >/dev/null 2>&1; then if ! command -v git >/dev/null 2>&1; then
echo "Installing git, which the clone needs" echo "Installing git, which the clone needs"
dnf install -y git dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git
fi fi
# Cloned straight into the user's home and owned by them: this is the # Create or advance the checkout as the target user. A root-owned .git in a
# checkout `panama update` will pull from for the life of the machine, and # user's home would break every later update.
# a root-owned .git in a user's home is a wound that never heals. PANAMA_PATH="$bootstrap_home/.local/share/Panama"
PANAMA_PATH="$user_home/.local/share/Panama" BOOTSTRAP_USER="$username"
if [[ -d "$PANAMA_PATH/.git" ]]; then prepare_panama_checkout "$PANAMA_PATH"
echo "Panama is already cloned at $PANAMA_PATH; updating"
runuser -u "$username" -- git -C "$PANAMA_PATH" pull --ff-only \
|| echo "Could not fast-forward; installing from the clone as it is" >&2
else
runuser -u "$username" -- mkdir -p "$user_home/.local/share"
runuser -u "$username" -- git clone "$REPO_URL" "$PANAMA_PATH"
fi
echo "Handing off to install as $username" echo "Handing off to install as $username"
exec runuser -u "$username" -- env PANAMA_PATH="$PANAMA_PATH" \ verified_install_handoff 1
"$PANAMA_PATH/install" --server </dev/tty
fi fi
# git is the one dependency the clone itself needs. Everything else -- gum # git is the one dependency the clone itself needs. Everything else -- gum
# included -- is bootstrapped by `install`. # included -- is bootstrapped by `install`.
if ! command -v git >/dev/null 2>&1; then if ! command -v git >/dev/null 2>&1; then
echo "Installing git, which the clone needs" echo "Installing git, which the clone needs"
sudo dnf install -y git sudo dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git
fi fi
if [[ -d "$PANAMA_PATH/.git" ]]; then prepare_panama_checkout "$PANAMA_PATH"
# An existing clone makes this the recovery command too. Only a fast-forward:
# local work is never rewritten, and a diverged clone still installs from
# what it has rather than stopping someone mid-repair.
echo "Panama is already cloned at $PANAMA_PATH; updating"
git -C "$PANAMA_PATH" pull --ff-only \
|| echo "Could not fast-forward; installing from the clone as it is" >&2
else
git clone "$REPO_URL" "$PANAMA_PATH"
fi
# `curl | bash` and `bash <(curl ...)` can leave stdin as the pipe, and the # A shell invoked from automation can have a pipe as stdin, while the first
# first thing install runs is the interview, which has to be able to ask. # thing install runs is the interview, which has to be able to ask.
# Reattach the terminal when there is one; without one the interview will say # Reattach the terminal when there is one; without one the interview will say
# so itself. # so itself.
# The probe actually opens /dev/tty rather than testing -r: a process with no # The probe actually opens /dev/tty rather than testing -r: a process with no
# controlling terminal passes -r and then fails the redirect. # controlling terminal passes -r and then fails the redirect.
handoff_tty=0
if [[ ! -t 0 ]] && (exec </dev/tty) 2>/dev/null; then if [[ ! -t 0 ]] && (exec </dev/tty) 2>/dev/null; then
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty handoff_tty=1
fi fi
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} verified_install_handoff "$handoff_tty"
@@ -14,7 +14,11 @@ takes to run the suite.
Usage: Usage:
panama-settings-docs write docs/settings.md panama-settings-docs write docs/settings.md
panama-settings-docs --output PATH
write PATH instead
panama-settings-docs --check exit 1 if the committed copy is stale panama-settings-docs --check exit 1 if the committed copy is stale
panama-settings-docs --check --output PATH
exit 1 if PATH is stale
panama-settings-docs --stdout print without writing panama-settings-docs --stdout print without writing
Where each group of settings lives comes from SettingsSearch.qml's routing and Where each group of settings lives comes from SettingsSearch.qml's routing and
@@ -264,7 +268,14 @@ def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--check", action="store_true") parser.add_argument("--check", action="store_true")
parser.add_argument("--stdout", action="store_true") parser.add_argument("--stdout", action="store_true")
parser.add_argument("--output", type=pathlib.Path, default=OUTPUT)
args = parser.parse_args() args = parser.parse_args()
output = (args.output if args.output.is_absolute()
else (pathlib.Path.cwd() / args.output).resolve())
try:
displayed_output = output.relative_to(ROOT)
except ValueError:
displayed_output = output
try: try:
rendered = render(read_entries(), read_routes(), read_titles()) rendered = render(read_entries(), read_routes(), read_titles())
@@ -279,19 +290,19 @@ def main():
return 0 return 0
if args.check: if args.check:
if not OUTPUT.exists(): if not output.exists():
print("panama-settings-docs: docs/settings.md has never been generated", print(f"panama-settings-docs: {displayed_output} has never been generated",
file=sys.stderr) file=sys.stderr)
return 1 return 1
if OUTPUT.read_text() != rendered: if output.read_text() != rendered:
print("panama-settings-docs: docs/settings.md is stale; re-run this " print(f"panama-settings-docs: {displayed_output} is stale; re-run this "
"script and commit the result", file=sys.stderr) "script and commit the result", file=sys.stderr)
return 1 return 1
return 0 return 0
OUTPUT.parent.mkdir(parents=True, exist_ok=True) output.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text(rendered) output.write_text(rendered)
print(f"wrote {OUTPUT.relative_to(ROOT)}") print(f"wrote {displayed_output}")
return 0 return 0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,722 @@
# Verification Gate Remediation Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan.
**Goal:** Make Panama's contract gate honest, safe by default when requested, diagnostic on failure, and capable of proving the later repository-audit fixes.
**Architecture:** Replace the incomplete desktop-hijacking ledger with one explicit manifest covering every collected contract. `bin/panama` remains the public runner: it parses that manifest, applies capability policy, executes each selected contract under a bounded timeout, and preserves its diagnostics. Contracts that currently depend on the calendar, tracked files, the live settings store, or source-text inspection are changed to exercise disposable fixtures and real public commands.
**Tech Stack:** Bash 5, Python 3 standard library, Git fixture repositories, existing Panama contract executables, Quickshell test harnesses.
**Spec:** `docs/superpowers/specs/2026-08-26-repository-audit-remediation-design.md`, Package 1: Verification gate.
## Global Constraints
- Preserve the user's existing `config/bash/.bashrc` changes. Do not stage or edit that file.
- Do not run plain `panama test` during implementation. It is intentionally interactive once this plan lands and several entries manipulate the live desktop.
- Do not start or restart the production Quickshell, Hyprland, or system services.
- Use `apply_patch` for every repository edit.
- Test public behavior. Source inspection may enforce a narrow declarative invariant, but must not substitute for executing `bin/panama` or a helper.
- Keep every contract self-cleaning on success, failure, signal, and timeout.
- Commit after each task with only that task's files staged.
---
## Task 1: Replace the desktop ledger with a complete contract manifest
**Files:**
- Create: `tests/contracts.manifest`
- Create: `tests/setup/contract-manifest-contract`
- Delete: `tests/setup/desktop-hijacking-contract`
- Delete: `tests/desktop-hijacking`
- Modify: `README.md`
### Step 1: Write the failing manifest contract
Create `tests/setup/contract-manifest-contract` as an executable Bash contract. It must independently discover contracts exactly as the runner does:
```bash
while IFS= read -r path; do
[[ -x "$path" || "$path" == *_test.py ]] || continue
printf 'tests/%s\n' "${path#"$repo_dir/tests/"}"
done < <(find "$repo_dir/tests" -type f \
-not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
```
Parse non-comment manifest lines as two whitespace-separated fields:
```text
<comma-separated-capabilities> <repo-relative-contract-path>
```
Assert all of the following:
- every discovered contract appears exactly once;
- every manifest path is discovered by the runner;
- paths are sorted lexicographically;
- `hermetic` appears alone;
- non-hermetic entries use only `live-host`, `live-compositor`, `live-desktop`, `network`, and `privileged`;
- every non-hermetic entry has a directly preceding comment explaining the external effect;
- no path or capability is duplicated on one line;
- the manifest contains at least one entry for each capability except `privileged`, which is supported but not currently required by a contract.
Include a parser-fixture section in the same contract. Copy the manifest parser logic into a temporary malformed manifest for each rejection case (missing contract, stale path, duplicate path, unknown capability, `hermetic,network`, unsorted paths, and uncommented non-hermetic entry) and assert each fixture fails with a message naming the defect.
Run:
```bash
chmod +x tests/setup/contract-manifest-contract
tests/setup/contract-manifest-contract
```
Expected: FAIL because `tests/contracts.manifest` does not exist.
### Step 2: Add the complete manifest
Add one line for each contract discovered by the command in Step 1. Start every path not named below as `hermetic`. Apply this audited non-hermetic classification exactly:
| Capabilities | Contract |
|---|---|
| `live-compositor,live-desktop` | `tests/quickshell/activity-indicator-contract` |
| `live-desktop` | `tests/quickshell/activity-state-contract` |
| `live-host` | `tests/quickshell/adwaita-accent-contract` |
| `live-host` | `tests/quickshell/application-volume-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/calendar-agenda-contract` |
| `live-host` | `tests/quickshell/calendar-agenda-helper-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/cheatsheet-contract` |
| `live-host` | `tests/quickshell/connectivity-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/control-center-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/control-center-services-contract` |
| `live-desktop` | `tests/quickshell/default-apps-roles-contract` |
| `live-host` | `tests/quickshell/default-apps-family-contract` |
| `live-desktop` | `tests/quickshell/display-arrangement-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/displays-contract` |
| `live-host` | `tests/quickshell/disks-contract` |
| `live-desktop` | `tests/quickshell/dock-position-contract` |
| `live-compositor` | `tests/quickshell/enum-hypr-map-contract` |
| `network` | `tests/setup/extras-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/focus-session-contract` |
| `live-desktop` | `tests/quickshell/focus-session-expiry` |
| `live-desktop` | `tests/quickshell/focus-session-restart` |
| `live-host` | `tests/quickshell/firewall-contract` |
| `live-host` | `tests/quickshell/gaming-contract` |
| `live-desktop` | `tests/quickshell/health-ui-contract` |
| `live-compositor` | `tests/hypr/keybind-categories-contract` |
| `live-compositor` | `tests/quickshell/keybind-rebind-contract` |
| `live-compositor` | `tests/quickshell/keybinds-contract` |
| `network` | `tests/quickshell/home-assistant-helper-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/my-home-settings-contract` |
| `live-host` | `tests/quickshell/overview-window-actions-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/overview-live-actions` |
| `live-compositor,live-desktop` | `tests/quickshell/overview-search-contract` |
| `live-desktop` | `tests/quickshell/panama-action-ipc-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/phone-page-contract` |
| `live-host` | `tests/quickshell/polkit-agent-contract` |
| `live-host` | `tests/quickshell/printers-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/scratchpad-shelf-contract` |
| `live-compositor` | `tests/quickshell/schema-hypr-shape-contract` |
| `live-desktop` | `tests/quickshell/screen-intelligence-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/settings-hyprland-write-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/settings-pages-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/settings-system-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/settings-window-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/settings-write-sweep-contract` |
| `live-host` | `tests/quickshell/sharing-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/signal-glass-contract` |
| `live-host` | `tests/quickshell/snapshots-contract` |
| `live-host` | `tests/quickshell/sound-page-contract` |
| `live-host` | `tests/quickshell/ssh-keys-contract` |
| `live-desktop` | `tests/quickshell/status-events-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/switcher-contract` |
| `live-host` | `tests/quickshell/updates-contract` |
| `live-host` | `tests/quickshell/user-accounts-contract` |
| `live-host` | `tests/setup/hardware-predicates-contract` |
| `live-compositor,live-desktop` | `tests/quickshell/welcome-contract` |
For each non-hermetic line, move the useful explanation from `tests/desktop-hijacking` directly above the entry. Add concise comments for the newly discovered entries describing the exact network, compositor-read, or window-mapping behavior.
Run:
```bash
tests/setup/contract-manifest-contract
```
Expected: PASS with the discovered and manifested counts equal.
### Step 3: Remove the old ledger and update the documented count
Delete `tests/desktop-hijacking` and `tests/setup/desktop-hijacking-contract`. Update the exact contract count in `README.md`; removing one executable and adding one executable keeps the current count at 185 at this point.
Run:
```bash
tests/setup/readme-contract
git diff --check
```
Expected: both PASS.
### Step 4: Commit
```bash
git add tests/contracts.manifest tests/setup/contract-manifest-contract \
tests/setup/desktop-hijacking-contract tests/desktop-hijacking README.md
git commit -m "Test: Classify every contract capability"
```
---
## Task 2: Make `panama test` enforce the manifest and preserve diagnostics
**Files:**
- Create: `tests/setup/test-runner-contract`
- Modify: `bin/panama`
- Modify: `tests/contracts.manifest`
- Modify: `README.md`
### Step 1: Write a public runner contract
Create `tests/setup/test-runner-contract`. Build a temporary repository containing a copy of `bin/panama`, a minimal `tests/contracts.manifest`, and executable fixture contracts named `pass-contract`, `fail-contract`, `stderr-contract`, `hang-contract`, `host-contract`, `desktop-contract`, and `network-contract`. Initialize it as a Git repository so the copied CLI behaves like the real installation.
The fixtures must record executions beneath the temporary directory and produce these behaviors:
```bash
# pass-contract
printf 'pass stdout\n'
# fail-contract
printf 'failure stdout\n'
printf 'failure stderr\n' >&2
exit 7
# stderr-contract
printf 'warning on success\n' >&2
# hang-contract
trap 'printf terminated >"$PANAMA_TEST_FIXTURE/terminated"; exit 124' TERM
while :; do sleep 1; done
```
Assert the copied `panama` executable provides these public behaviors:
1. `panama test --safe` runs only `hermetic` fixtures and reports each skipped capability count, including `live-host`.
2. `panama test desktop` in a non-TTY fails without running the fixture and tells automation to pass `--allow live-desktop`.
3. `panama test --allow live-desktop desktop` runs it without a prompt.
4. Multiple `--allow` flags are repeatable and capability-specific; allowing desktop does not allow network.
5. An unknown flag, a second pattern, an unknown capability, and `--safe --allow live-desktop` are usage errors.
6. `PANAMA_TEST_TIMEOUT_SECONDS=1 panama test hang` terminates the fixture, reports a timeout, and exits nonzero.
7. A failed contract prints both captured stdout and captured stderr.
8. A successful contract with stderr prints a warning and the captured stderr.
9. Successful stdout stays hidden to keep the normal run scannable.
10. A pattern matching only skipped contracts is a distinct error from no pattern match.
11. Capture files are removed after success and failure.
12. `panama contracts <file>` labels hits with all manifest capabilities.
Run:
```bash
chmod +x tests/setup/test-runner-contract
tests/setup/test-runner-contract
```
Expected: FAIL against the current runner at the first manifest-policy assertion.
### Step 2: Replace ledger parsing with manifest parsing
In `bin/panama`, replace `DESKTOP_HIJACKING_LEDGER` and `hijacking_entries` with:
```bash
CONTRACT_MANIFEST="tests/contracts.manifest"
CONTRACT_CAPABILITIES=(hermetic live-host live-compositor live-desktop network privileged)
contract_manifest_entries() {
local line capabilities path
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}"
read -r capabilities path _ <<<"$line"
[[ -n "${capabilities:-}" && -n "${path:-}" ]] || continue
printf '%s\t%s\n' "$path" "$capabilities"
done < "$PANAMA_DIR/$CONTRACT_MANIFEST"
}
```
Do not make a missing or malformed manifest mean “everything is safe.” Before either `cmd_test` or `cmd_contracts` consumes manifest entries, the public CLI independently validates the complete actual manifest: exactly two fields, known/non-empty/non-duplicate capabilities, exclusive `hermetic`, unique sorted paths, the executable/`*_test.py` fixture-excluded discovery set, and a non-empty directly preceding comment for every non-hermetic entry. Missing discovered contracts and stale manifest paths are fatal before selection or execution. Keep `tests/setup/contract-manifest-contract` as an independent validator rather than sourcing runtime code; a later contract cannot protect earlier execution.
### Step 3: Implement argument and capability policy
Parse position-independent arguments with one optional pattern and repeatable `--allow <capability>`. Apply these rules:
- `--safe` means only `hermetic` entries;
- `--safe` and any `--allow` together are a usage error;
- `--allow hermetic` is a usage error because hermetic contracts need no grant;
- plain `panama test` selects all matching manifest entries;
- selected non-hermetic capabilities missing from `--allow` require confirmation only when stdin and stderr are TTYs;
- non-TTY execution fails before running anything and names every missing capability;
- the prompt lists selected contract count and each external capability; default is no;
- there is no `--allow-all` path.
Use associative arrays for selected grants, manifest capabilities, and skipped counts so ordering stays deterministic.
### Step 4: Bound execution and retain output
At the beginning of `cmd_test`, validate:
```bash
local timeout_seconds="${PANAMA_TEST_TIMEOUT_SECONDS:-180}"
[[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || {
err 'PANAMA_TEST_TIMEOUT_SECONDS must be a positive integer.'
return 2
}
```
The public CLI process must own contract supervision. Check that `mktemp -d` succeeds before constructing any capture path, then install top-level `EXIT`, `INT`, and `TERM` cleanup around the checked directory. Launch the active `timeout` asynchronously and retain its PID/process-group ownership. On INT or TERM sent to the exact CLI PID, signal the active timeout/process group, wait for it, remove capture storage, and exit 130 or 143. On normal completion, remove capture storage and clear the temporary traps without changing the aggregate test status. For each contract, run either `python3 path` or the executable through:
```bash
timeout --signal=TERM --kill-after=5 "$timeout_seconds" \
"${runner[@]}" >"$stdout_file" 2>"$stderr_file"
```
Capture the exit status explicitly instead of relying on `set -e`. Treat status 124 or 137 as timeout. On failure, print a labeled stdout section when nonempty and a labeled stderr section when nonempty. On success, suppress stdout; if stderr is nonempty, call `warn` and print it. Continue through the selected suite and return nonzero if any contract failed or timed out.
### Step 5: Update `panama contracts`
Load the same manifest mapping and render labels such as:
```text
tests/quickshell/settings-pages-contract [live-compositor,live-desktop]
tests/setup/readme-contract [hermetic]
```
The grep-based relationship search remains unchanged. Its capability label must come only from the manifest.
### Step 6: Make the tests pass and update the count
Add `hermetic tests/setup/test-runner-contract` to `tests/contracts.manifest`. Update the README count from 185 to 186.
Run:
```bash
tests/setup/contract-manifest-contract
tests/setup/test-runner-contract
tests/setup/readme-contract
./bin/panama test --safe test-runner
```
Expected: all PASS. The final command reports one passing contract and does not request permission.
### Step 7: Commit
```bash
git add bin/panama tests/contracts.manifest tests/setup/test-runner-contract README.md
git commit -m "Fix: Make contract execution safe and diagnostic"
```
---
## Task 3: Remove the calendar-pinned agent usage failure
**Files:**
- Modify: `tests/quickshell/agent-usage-contract`
### Step 1: Prove the current fixture is date-dependent
Run:
```bash
tests/quickshell/agent-usage-contract
```
Expected before the change: FAIL on `todayTotalTokens` when the current day is not 2026-08-25.
### Step 2: Generate the session path and timestamps dynamically
Replace the fixed `2026/08/25` directory and timestamps with local-day fixture values, matching the collector's `datetime.now()` and `astimezone()` behavior:
```bash
session_day="$(date +%Y/%m/%d)"
session_date="$(date +%Y-%m-%d)"
session_offset="$(date +%:z)"
mkdir -p "$work/codex/sessions/$session_day"
jq -nc --arg timestamp "${session_date}T10:00:00${session_offset}" \
'{type:"token_count",timestamp:$timestamp,payload:{type:"token_count",info:{total_token_usage:{input_tokens:999999,output_tokens:999999},last_token_usage:{input_tokens:1200,cached_input_tokens:1000,output_tokens:300}}}}'
```
Write two records at local `10:00:00` and `10:05:00` with `session_offset`, preserving the existing expectation of 3,000 last-turn tokens. Do not add a production clock override: only the fixture was wrong.
### Step 3: Verify and commit
Run:
```bash
tests/quickshell/agent-usage-contract
./bin/panama test --safe agent-usage
```
Expected: both PASS with `todayTotalTokens == 3000`.
```bash
git add tests/quickshell/agent-usage-contract
git commit -m "Test: Make agent usage fixtures date independent"
```
---
## Task 4: Replace regex secret scanning with semantic fixtures
**Files:**
- Create: `tests/server/scan-tracked-secrets.py`
- Create: `tests/server/fixtures/secrets/clean/compose.yml`
- Create: `tests/server/fixtures/secrets/clean/.env.example`
- Create: `tests/server/fixtures/secrets/clean/README.md`
- Create: `tests/server/fixtures/secrets/leaked/compose.yml`
- Create: `tests/server/fixtures/secrets/leaked/.env.example`
- Modify: `tests/server/compose-secrets-contract`
### Step 1: Add false-positive and true-positive fixtures
The clean fixture must include:
- Compose interpolation such as `POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`;
- `CHANGE_ME`, empty values, and boolean switches in `.env.example`;
- prose containing `password: example`, `secret key`, and the current server README sentence that triggered the audit.
The leaked fixture must include:
- a literal YAML credential `POSTGRES_PASSWORD: fixture-should-be-rejected`;
- a literal env credential `API_TOKEN=fixture-should-be-rejected`.
- a plain-text PEM private-key header with an exact `path:line: private key` finding;
- plain-text synthetic `sk-ant-`, minimum-supported-length `ghp_`, and `xoxb-` signatures, each with an exact `path:line: provider token` finding.
Add clean plain-text near-misses for each signature. Keep semantic fixture credentials intentionally invalid and signature fixtures clearly synthetic while still matching the supported signature shapes.
### Step 2: Write the scanner with only the Python standard library
`tests/server/scan-tracked-secrets.py` takes a root directory followed by tracked paths on argv. It must:
- parse `.env` and `.env.example` lines as key/value records;
- parse YAML Compose files by indentation-aware `KEY: VALUE` / `- KEY=VALUE` environment entries and top-level secret-bearing scalar assignments;
- treat `${VAR}`, `${VAR:-default}`, empty strings, `CHANGE_ME`, and booleans as non-secret;
- ignore keys ending `_FILE`, `_PATH`, `_NAME`, or `_KEY_NAME`;
- scan all text file types only for PEM private-key headers and known provider-token signatures;
- never interpret Markdown credential-shaped prose as a YAML assignment;
- print `path:line: key` for semantic leaks and return 1 when any are found.
Keep the parser deliberately scoped to Compose and env syntax; do not add a general YAML dependency that the installer does not provide.
### Step 3: Make the contract prove both directions
Before scanning the real tracked `server/` paths, have `tests/server/compose-secrets-contract` call the scanner on the clean fixture and require success, then on each leaked fixture and require failure with the expected key named. Finally pass the actual paths from:
```bash
git -C "$repo_dir" ls-files 'server/**' 'server/*'
```
Keep the existing `.gitignore` and tracked-`.env` assertions.
Run:
```bash
tests/server/compose-secrets-contract
```
Expected: PASS; the README prose is accepted while both literal fixture secrets are rejected.
### Step 4: Commit
```bash
git add tests/server/scan-tracked-secrets.py tests/server/fixtures/secrets \
tests/server/compose-secrets-contract
git commit -m "Test: Scan compose secrets by data shape"
```
---
## Task 5: Make the update contract execute the real CLI
**Files:**
- Modify: `tests/setup/update-command-contract`
### Step 1: Replace the source-body assertions with a failing CLI fixture
Keep the existing `install --upgrade` stage and package-hash assertions, but replace the `sed`/`grep` inspection of `cmd_update` and `cmd_sync` with disposable Git repositories:
1. create a bare `origin.git`;
2. create an `upstream` clone with a copied `bin/panama` and a stub executable `install`;
3. push the initial branch;
4. clone it to `machine`;
5. append an upstream commit;
6. run `machine/bin/panama update` with `PANAMA_UPDATE_FIXTURE_LOG` consumed by the copied stub installer.
The installer stub must record its argv and optionally fail according to `PANAMA_UPDATE_INSTALL_RC`.
Add assertions that the real CLI:
- fast-forwards a clean machine clone to the new upstream commit;
- invokes the fixture installer with `--upgrade`;
- returns the installer's nonzero status;
- leaves `panama sync` separate from the installer path;
- on a dirty-tree stash-pop conflict, leaves no conflict markers and preserves a recoverable stash.
Do not yet assert that installation happens after WIP restoration. Package 3 of the approved design changes that ordering and will extend this same executable fixture with the new failing assertion before changing production code.
### Step 2: Repair status propagation in the contract harness
Change `run_install` so it returns the installer's status rather than `cat`'s status:
```bash
local status=0
PATH="$root/shim:$PATH" PANAMA_PATH="$root" PANAMA_RAN="$root/ran" \
XDG_STATE_HOME="$root/state" bash "$root/install" "$@" \
>"$root/out" 2>&1 || status=$?
cat "$root/ran"
return "$status"
```
Replace the indented-assignment parser with a tolerant anchored expression:
```python
match = re.match(r'^\s*STAGES=\((.*)\)\s*$', line)
```
Fail explicitly when the assignment is not found. Do not run parser code in process substitution whose exit status is ignored.
### Step 3: Verify and commit
Run:
```bash
tests/setup/update-command-contract
./bin/panama test --safe update-command
```
Expected: both PASS and the fixture log proves `bin/panama update` actually ran.
```bash
git add tests/setup/update-command-contract
git commit -m "Test: Exercise updates through the real command"
```
---
## Task 6: Stop contracts from mutating tracked files or reading live preferences
**Files:**
- Modify: `config/dot/quickshell/scripts/panama-settings-docs`
- Modify: `tests/quickshell/settings-docs-contract`
- Modify: `tests/quickshell/settings-sync-contract`
### Step 1: Add a failing output-path contract for generated settings docs
Extend `tests/quickshell/settings-docs-contract` to copy the committed document to a temporary path, append drift to the copy, and run:
```bash
"$generator" --check --output "$scratch/settings.md"
```
Capture `git diff -- docs/settings.md` before and after the contract and require byte-for-byte equality. Run the contract now.
Expected: FAIL because the generator does not accept `--output` and the current test still edits the tracked document.
### Step 2: Add a real output seam to the generator
Add:
```python
parser.add_argument("--output", type=pathlib.Path, default=OUTPUT)
```
Resolve relative explicit paths against the current working directory, use that path for write/check behavior, and keep the existing default unchanged. `--stdout` must remain mutually exclusive with writing but may coexist with no explicit output.
Update the contract so the stale-copy check never writes `docs/settings.md`. Its trap should only remove the temporary directory.
Run:
```bash
tests/quickshell/settings-docs-contract
git diff --exit-code -- docs/settings.md
```
Expected: PASS and no diff.
### Step 3: Seed settings-sync from a fixture instead of the live settings store
In `tests/quickshell/settings-sync-contract`, set `XDG_CONFIG_HOME="$work/source-config"` before the first export and create a representative `panama/settings.json` there from schema-valid values. Include at least:
- one boolean;
- one integer;
- one numeric enum;
- one string;
- one setting omitted from the import bundle to prove merge semantics.
Remove every read of `${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json` that occurs before the temporary home is established. Keep the allow-list completeness assertion, but compare against the explicit fixture rather than the user's preferences.
Run:
```bash
tests/quickshell/settings-sync-contract
```
Expected: PASS without reading or writing the real settings store.
### Step 4: Commit
```bash
git add config/dot/quickshell/scripts/panama-settings-docs \
tests/quickshell/settings-docs-contract tests/quickshell/settings-sync-contract
git commit -m "Test: Isolate generated docs and settings fixtures"
```
---
## Task 7: Isolate the dock geometry contract
**Files:**
- Modify: `tests/quickshell/dock-position-contract`
### Step 1: Write the safety assertions before changing the harness
At contract start, record whether `config/dot/quickshell/dock-position-probe.qml` exists and its checksum if present. At every exit, assert the source tree matches that initial state. Also fail if the contract reads `${HOME}/.config/panama/settings.json`.
Run:
```bash
tests/quickshell/dock-position-contract
```
Expected: FAIL because the contract creates its probe inside the tracked QML tree and reads live settings.
### Step 2: Copy the QML tree and seed deterministic settings
After creating `work`, copy the Quickshell tree to a fixture root:
```bash
fixture_shell="$work/quickshell"
cp -a "$shell_dir/." "$fixture_shell/"
probe="$fixture_shell/dock-position-probe.qml"
```
Write the probe only to `$probe`. Seed `$work/config/panama/settings.json` with the schema defaults needed by the dock plus the position under test; do not copy the user's settings. Run each `qs -p` invocation from `$fixture_shell` with `XDG_CONFIG_HOME="$work/config"`.
The contract still maps isolated panel surfaces into the current Wayland session. Keep its manifest capability as `live-desktop`: moving its files and settings into temporary storage removes mutation risk but does not make the visible surfaces hermetic.
### Step 3: Verify isolation under an explicit desktop grant
Run:
```bash
tests/quickshell/dock-position-contract
tests/setup/contract-manifest-contract
./bin/panama test --allow live-desktop dock-position
```
Expected: all PASS, the source-tree checksum is unchanged, and the manifest entry remains `live-desktop`.
### Step 4: Commit
```bash
git add tests/quickshell/dock-position-contract
git commit -m "Test: Isolate dock geometry fixtures"
```
---
## Task 8: Update operator documentation and run the Package 1 gate
**Files:**
- Modify: `README.md`
- Modify: `.claude/skills/panama/SKILL.md`
- Modify: `skills/panama-desktop/SKILL.md`
- Modify: `bin/panama`
### Step 1: Update the public command documentation
Document:
```text
panama test --safe
panama test --allow live-host updates
panama test --allow live-compositor keybinds
PANAMA_TEST_TIMEOUT_SECONDS=300 panama test --safe
```
State that:
- `tests/contracts.manifest` classifies every contract;
- `--safe` means hermetic only;
- plain full runs prompt in a terminal before any non-hermetic contract;
- automation must grant each required capability with repeatable `--allow`;
- failures print captured stdout/stderr and every contract has a default 180-second outer timeout.
Remove current claims that `--safe` is defined by `tests/desktop-hijacking`. Update `bin/panama`'s top comment, usage text, `cmd_test` comments, and `cmd_contracts` comments to match behavior. Leave historical design specs unchanged.
### Step 2: Run focused verification
```bash
bash -n bin/panama \
tests/setup/contract-manifest-contract \
tests/setup/test-runner-contract \
tests/setup/update-command-contract \
tests/quickshell/agent-usage-contract \
tests/quickshell/settings-docs-contract \
tests/quickshell/settings-sync-contract \
tests/server/compose-secrets-contract
python3 -m py_compile \
config/dot/quickshell/scripts/panama-settings-docs \
tests/server/scan-tracked-secrets.py
tests/setup/contract-manifest-contract
tests/setup/test-runner-contract
tests/setup/update-command-contract
tests/quickshell/agent-usage-contract
tests/quickshell/settings-docs-contract
tests/quickshell/settings-sync-contract
tests/server/compose-secrets-contract
tests/setup/readme-contract
git diff --check
```
Expected: every command exits 0.
### Step 3: Run the complete hermetic gate
```bash
./bin/panama test --safe
```
Expected: all selected hermetic contracts pass; the summary reports skipped counts for `live-host`, `live-compositor`, `live-desktop`, and `network`; no confirmation appears; no production shell or service is restarted.
If a manifest-labeled hermetic contract reaches the network, live compositor, live settings, root prompt, or tracked-file write, stop and reclassify or isolate it before continuing. Do not grant a capability merely to make this gate green.
### Step 4: Confirm worktree scope
```bash
git status --short
git diff -- config/bash/.bashrc
```
Expected: the pre-existing `.bashrc` modification remains unstaged and unchanged from its pre-plan state; only Package 1 files are staged or committed.
### Step 5: Commit
```bash
git add README.md .claude/skills/panama/SKILL.md skills/panama-desktop/SKILL.md bin/panama
git commit -m "Docs: Explain contract capability gates"
```
### Step 6: Record the package handoff
Capture these facts in the implementation summary before beginning Package 2:
- final hermetic pass/fail/skip counts;
- every non-hermetic capability (`live-host`, `live-compositor`, `live-desktop`, `network`, and `privileged`) and its contract count;
- focused verification commands and exit codes;
- confirmation that `config/bash/.bashrc` stayed out of all commits;
- any contract retained as non-hermetic after an attempted isolation, with the exact reason.
@@ -0,0 +1,406 @@
# Server firewall transaction implementation plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make `setup-server` establish Cloudflare-scoped public web access and WireGuard-only admin access as one recoverable firewalld transaction.
**Architecture:** Put validation, desired-state calculation, journaling, and rollback in one standard-library Python command. Keep `setup-server` as the public orchestrator that verifies firewalld and invokes the updater; test both through stateful curl/systemd/firewalld command adapters with separate permanent/runtime state. Production endpoint URLs stay hardcoded, so no environment variable can redirect policy input.
**Tech Stack:** Python 3 standard library (`argparse`, `dataclasses`, `fcntl`, `hashlib`, `ipaddress`, `json`, `os`, `pathlib`, `signal`, `subprocess`), Bash 5, curl/firewalld/systemd command adapters, Panama contract runner.
**Spec:** `docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md`
## Global constraints
- Do not call the host's `firewall-cmd`, mutate firewalld/systemd, contact Cloudflare from a contract, or apply a real server policy.
- `setup-server` must fail unless firewalld is installed, enabled, and active after one `enable --now` attempt.
- Validate both Cloudflare families completely before any privileged mutation. One bad family changes nothing.
- Public 80/443 must use dedicated Panama ipsets and exactly four rich rules. Port 81 belongs only to a unique WireGuard-only zone; no WireGuard zone leaves it closed.
- Never remove unrelated user-managed rules or service-specific ports. Only exact old direct 80/443/81 rules are eligible for the documented legacy migration.
- Keep old runtime rules until one successful reload. Roll back every failed transaction and retain the journal when rollback cannot be verified.
- Run the repository updater as the normal user. Only narrow `systemctl`/`firewall-cmd` mutations use sudo.
- No privileged timer or background service is added.
- The new contract is hermetic and runs under `panama test --safe`.
- Preserve `/home/gib/.local/share/Panama/config/bash/.bashrc` outside this worktree.
## File map
- `server/scripts/update-firewall`: parser, range validation, zone/policy inspection, desired state, journal, mutation, reload/readback, rollback.
- `server/firewall/cloudflare-v4.cidrs`: committed current canonical IPv4 ranges.
- `server/firewall/cloudflare-v6.cidrs`: committed current canonical IPv6 ranges.
- `tests/setup/server-firewall-contract`: stateful public `setup-server` fixture.
- `tests/setup/fixtures/firewall/`: valid/invalid/refreshed CIDR response data.
- `setup/scripts/setup-server`: required firewalld postconditions and updater call.
- `setup/packages/server-packages`: document firewalld as a required package, not a best-effort extra.
- `tests/contracts.manifest`: one new hermetic contract.
- `README.md`, `server/README.md`: exact exposure, refresh, and rollback policy.
---
### Task 1: Require firewalld and establish the first desired policy
**Files:**
- Create: `server/scripts/update-firewall`
- Create: `server/firewall/cloudflare-v4.cidrs`
- Create: `server/firewall/cloudflare-v6.cidrs`
- Create: `tests/setup/server-firewall-contract`
- Create: `tests/setup/fixtures/firewall/valid-v4.cidrs`
- Create: `tests/setup/fixtures/firewall/valid-v6.cidrs`
- Modify: `setup/scripts/setup-server:40-60`
- Modify: `setup/packages/server-packages`
- Modify: `tests/contracts.manifest`
**Interfaces:**
- Consumes: `server/scripts/update-firewall apply`, `server/scripts/update-firewall --validate-only DIRECTORY`, `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, PATH adapters, and committed bootstrap CIDRs.
- Produces: `main(argv) -> int`, `parse_cidrs(text, family) -> tuple[str, ...]`, `generation_name(family, cidrs) -> str`, `run_firewall(args, permanent=True)`, validation-only mode, and first-run policy application.
- [ ] **Step 1: Create current canonical range fixtures**
Use the official 2026-08-27 Cloudflare list as both committed bootstrap and initial valid fixture.
`cloudflare-v4.cidrs` must contain exactly:
```text
103.21.244.0/22
103.22.200.0/22
103.31.4.0/22
104.16.0.0/13
104.24.0.0/14
108.162.192.0/18
131.0.72.0/22
141.101.64.0/18
162.158.0.0/15
172.64.0.0/13
173.245.48.0/20
188.114.96.0/20
190.93.240.0/20
197.234.240.0/22
198.41.128.0/17
```
`cloudflare-v6.cidrs` must contain exactly:
```text
2400:cb00::/32
2405:8100::/32
2405:b500::/32
2606:4700::/32
2803:f800::/32
2a06:98c0::/29
2c0f:f248::/32
```
The production refresh endpoints are exactly `https://www.cloudflare.com/ips-v4` and `https://www.cloudflare.com/ips-v6`. The contract's PATH `curl` adapter returns fixture bytes for only those exact URLs; no environment variable or config can replace them, and the contract never contacts either endpoint.
- [ ] **Step 2: Build the stateful command model and red first-run cases**
Store fixture state in JSON:
```json
{
"systemctl": {"installed": true, "enabled": true, "active": true},
"permanent": {
"defaultZone": "public",
"zones": {
"public": {"interfaces": ["eth0"], "target": "default", "services": ["ssh"], "ports": ["80/tcp", "443/tcp", "81/tcp", "2222/tcp"], "richRules": []},
"wireguard": {"interfaces": ["wg0"], "target": "default", "services": [], "ports": [], "richRules": []}
},
"ipsets": {}
},
"runtime": {}
}
```
The Python `firewall-cmd` stub must support only the exact query/mutation argv used by the updater, append JSON argv arrays to `calls.jsonl`, keep permanent/runtime separate, copy permanent to runtime on reload, and reject unknown argv with status `97`. The curl stub accepts only `--fail --silent --show-error --location --connect-timeout 10 --max-time 30 --max-filesize 65536 --output PATH URL`, logs argv, and copies the selected fixture response. `systemctl` supports `list-unit-files`, `is-enabled`, `is-active`, and `enable --now`. `sudo` logs and execs. Stub linger, sysctl, podman, and user-systemctl paths so the real `setup-server` finishes without host access.
Write red cases:
```text
firewalld missing -> nonzero; zero firewall mutations
enable/start failure -> nonzero; zero firewall mutations
postcheck inactive/disabled -> nonzero; zero firewall mutations
valid first run -> content-addressed v4/v6 ipsets, four rich rules, 81 only in wireguard, legacy direct 80/443/81 removed, ssh and 2222 preserved, one check-config and one reload
no wireguard zone -> 80/443 policy succeeds, 81 absent, exact closed-port diagnostic
```
- [ ] **Step 3: Run the contract and confirm current setup is red**
```bash
bash -n setup/scripts/setup-server tests/setup/server-firewall-contract
tests/setup/server-firewall-contract
```
Expected: nonzero because current setup treats inactive firewalld as success and opens all three ports directly.
- [ ] **Step 4: Implement validation, desired state, and first apply**
Start the updater with:
```python
def parse_cidrs(text: str, family: int) -> tuple[str, ...]:
if len(text.encode("utf-8")) > 65536:
raise PolicyError("CIDR response exceeds 65536 bytes")
lines = text.splitlines()
if not lines:
raise PolicyError("CIDR list is empty")
parsed: list[str] = []
for raw in lines:
if not raw or raw != raw.strip() or any(ch.isspace() for ch in raw):
raise PolicyError(f"invalid CIDR line: {raw!r}")
network = ipaddress.ip_network(raw, strict=True)
if network.version != family or network.prefixlen == 0 or str(network) != raw:
raise PolicyError(f"invalid IPv{family} CIDR: {raw}")
parsed.append(raw)
if len(parsed) != len(set(parsed)):
raise PolicyError("CIDR list contains duplicates")
return tuple(parsed)
def generation_name(family: int, cidrs: tuple[str, ...]) -> str:
digest = hashlib.sha256(("\n".join(cidrs) + "\n").encode()).hexdigest()[:12]
return f"panama-cf{family}-{digest}"
```
Build exactly these rich-rule strings, substituting the content-addressed generation name:
```text
rule family="ipv4" source ipset="V4_GENERATION" port port="80" protocol="tcp" accept
rule family="ipv4" source ipset="V4_GENERATION" port port="443" protocol="tcp" accept
rule family="ipv6" source ipset="V6_GENERATION" port port="80" protocol="tcp" accept
rule family="ipv6" source ipset="V6_GENERATION" port port="443" protocol="tcp" accept
```
Implement `main(argv)` with only two accepted forms: `apply` and `--validate-only DIRECTORY`. Validation-only mode reads `DIRECTORY/cloudflare-v4.cidrs` and `DIRECTORY/cloudflare-v6.cidrs`, runs the same strict parser used by apply, prints the two generation names, and exits without consulting systemd, the network, config/state directories, sudo, or firewalld. Unknown or combined arguments exit nonzero with usage.
Query default/public and WireGuard zones before mutation. Apply new permanent ipsets/rules, exact legacy removal, `--check-config`, one `--reload`, and readback. The first task may use an in-memory snapshot; Task 3 adds durable recovery.
Replace the old port loop in `setup-server` with exact firewalld installation/state checks: `command -v firewall-cmd`, `systemctl list-unit-files firewalld.service`, one `sudo systemctl enable --now firewalld.service`, then successful `systemctl is-enabled firewalld.service` and `systemctl is-active firewalld.service`. Only after those postconditions invoke:
```bash
"$PANAMA_PATH/server/scripts/update-firewall" apply
```
- [ ] **Step 5: Add the manifest entry and verify first-run behavior**
```text
# Server firewall policy runs against stateful systemctl/firewall-cmd adapters.
hermetic tests/setup/server-firewall-contract
```
Run:
```bash
bash -n setup/scripts/setup-server tests/setup/server-firewall-contract
python3 -m py_compile server/scripts/update-firewall
server/scripts/update-firewall --validate-only server/firewall
tests/setup/server-firewall-contract
tests/setup/contract-manifest-contract
./bin/panama test --safe server-firewall
git diff --check
```
- [ ] **Step 6: Commit the required first policy**
```bash
git add server/scripts/update-firewall server/firewall tests/setup/fixtures/firewall \
tests/setup/server-firewall-contract setup/scripts/setup-server \
setup/packages/server-packages tests/contracts.manifest
git commit -m "Fix: Establish scoped server firewall policy"
```
---
### Task 2: Reject bad ranges, unsafe zones, and foreign exposure
**Files:**
- Modify: `server/scripts/update-firewall`
- Modify: `tests/setup/server-firewall-contract`
- Create: `tests/setup/fixtures/firewall/refreshed-v4.cidrs`
- Create: `tests/setup/fixtures/firewall/refreshed-v6.cidrs`
- Create: `tests/setup/fixtures/firewall/invalid-*.cidrs`
**Interfaces:**
- Consumes: `parse_cidrs`, content generations, first-run apply, the exact PATH curl adapter, and stored zone config.
- Produces: `load_zone_config`, `select_zones`, `find_exposure_conflicts`, refresh fallback, and idempotent convergence.
- [ ] **Step 1: Add exhaustive red validation and selection tables**
Fixture-invalid classes are exact:
```text
empty
blank-line
leading-space
trailing-token
comment
host-bits-set
wrong-family
slash-zero
duplicate
non-UTF8
oversized-65537-bytes
valid-v4-plus-invalid-v6
invalid-v4-plus-valid-v6
```
For each, assert byte-for-byte permanent/runtime/config/journal equality and zero mutation argv. Add zone cases for multiple `wg*` zones, public equal to WireGuard, stored missing zone, and WireGuard zone containing `wg0` plus `eth1`; all fail before mutation. Zero WireGuard candidates remains the safe success from Task 1.
Add conflicts: public `http`/`https` service, port range covering 80/443, ACCEPT zone target, and unrelated rich rule accepting 80/443. Assert a diagnostic with exact `firewall-cmd --zone=ZONE --list-all` inspection command and no deletion.
- [ ] **Step 2: Add red idempotence and two-family refresh cases**
Run the same valid policy twice. The second run must have no mutating `firewall-cmd`, check-config, or reload calls. Then provide valid refreshed v4/v6 lists and assert both generations change in one transaction; one changed plus one invalid changes neither.
Simulate curl network failure. With a valid installed policy, assert warning plus no changes. With no policy, assert the committed bootstrap pair is used.
- [ ] **Step 3: Run the contract and confirm missing validation behavior**
```bash
tests/setup/server-firewall-contract
```
Expected: nonzero on malformed, conflict, refresh, and idempotence cases not yet implemented.
- [ ] **Step 4: Implement strict refresh, zone config, conflict detection, and no-op convergence**
Fetch both hardcoded official endpoints without sudo using curl argv `--fail --silent --show-error --location --connect-timeout 10 --max-time 30 --max-filesize 65536 --output PART URL`. Use a private destination, verify its actual size is at most 65,536 bytes, and decode with strict ASCII before calling `parse_cidrs`. Treat the pair as one candidate and remove partial files on every exit/signal. Parse config with explicit `key=value` names and no shell execution.
Before mutation, compute normalized current and desired Panama-owned state. If identical, print `Server firewall policy already current` and return without check-config/reload. Detect foreign broad exposure but allow unrelated ports/services such as SSH and 2222. Remove only exact direct legacy ports.
Write selected-zone config to a private `.part` and rename it only after successful policy readback. Omit `wireguard_zone` when no candidate exists.
- [ ] **Step 5: Run focused checks and commit**
```bash
python3 -m py_compile server/scripts/update-firewall
tests/setup/server-firewall-contract
./bin/panama test --safe server-firewall
git diff --check
```
```bash
git add server/scripts/update-firewall tests/setup/server-firewall-contract \
tests/setup/fixtures/firewall
git commit -m "Fix: Validate firewall ranges and ownership"
```
---
### Task 3: Journal and roll back interrupted firewall transactions
**Files:**
- Modify: `server/scripts/update-firewall`
- Modify: `tests/setup/server-firewall-contract`
**Interfaces:**
- Consumes: normalized current/desired state and mutation adapter from Tasks 1-2.
- Produces: `PolicySnapshot`, `write_pending`, `restore_snapshot`, `verify_snapshot`, signal-safe rollback, and pending-journal recovery.
- [ ] **Step 1: Add red failure injection and journal cases**
The firewall stub accepts `FAIL_ONCE_JSON`, an exact normalized argv array. Inject one failure at each mutation boundary: create ipset, add entry, add rich rule, add 81, remove legacy port, remove old rule, remove old ipset, check-config, and reload. Separately inject permanent/runtime readback mismatch, selected-zone config write/rename failure, and pending-journal deletion failure after reload; each is still a failed transaction and must restore the snapshot.
For every case assert updater nonzero, permanent/runtime restored byte-for-byte, rollback reload performed when needed, and journal removed only after verified restoration. Add rollback-reload failure: journal remains with mode `0600`, diagnostic names its absolute path and exact retry command.
Seed a valid `pending.json` before invocation and assert restoration happens before curl or new-policy evaluation. Seed malformed, symlinked, foreign-owned, and mode-wrong journals; assert fail closed without mutation.
Start an apply process, block one mutation, send TERM to the updater's exact PID, release the stub, and require status `143`, restored state, and no orphan process.
- [ ] **Step 2: Run the contract and confirm current failure paths are red**
```bash
tests/setup/server-firewall-contract
```
Expected: nonzero because the updater has no durable journal or exact-PID signal rollback.
- [ ] **Step 3: Implement checked journal and rollback ownership**
Use a frozen dataclass with JSON round-trip methods:
```python
@dataclasses.dataclass(frozen=True)
class PolicySnapshot:
public_zone: str
wireguard_zone: str | None
panama_ipsets: dict[str, tuple[str, ...]]
panama_rich_rules: tuple[str, ...]
legacy_ports: tuple[str, ...]
wireguard_has_81: bool
```
Acquire `${state}/firewall/lock` with `fcntl.flock(LOCK_EX | LOCK_NB)`. Reject symlinked state components. Write `pending.json.part` with mode `0600`, fsync file and directory, then rename before the first mutation.
Start each mutating sudo/firewalld subprocess in its own process group and track its leader PID. INT/TERM handlers signal that exact group, wait for it, call `restore_snapshot`, verify permanent and runtime equality after rollback reload, then exit 130/143. The contract asserts the recorded child is gone before it returns. Normal completion clears handlers before deleting the journal.
At startup, a valid pending journal is restored and verified before network/config evaluation. Invalid journal ownership/mode/shape fails closed and prints inspection instructions.
- [ ] **Step 4: Verify all injected failures and signals**
```bash
python3 -m py_compile server/scripts/update-firewall
tests/setup/server-firewall-contract
./bin/panama test --safe server-firewall
git diff --check
```
- [ ] **Step 5: Commit recovery behavior**
```bash
git add server/scripts/update-firewall tests/setup/server-firewall-contract
git commit -m "Fix: Roll back firewall transactions"
```
---
### Task 4: Document policy and run the complete firewall gate
**Files:**
- Modify: `README.md:56-64`
- Modify: `server/README.md:1-90`
- Modify: `.claude/skills/panama/SKILL.md`
- Modify: `skills/panama-sudo/SKILL.md` if it documents server setup
- Modify: `tests/setup/readme-contract`
- Modify: `tests/server/containers-shape-contract` only if it asserts the old global 81 policy
**Interfaces:**
- Consumes: final updater behavior and its exact operator diagnostics.
- Produces: accurate exposure/refresh/rollback documentation and Package 2 firewall verification evidence.
- [ ] **Step 1: Write documentation assertions before prose**
Require README/server README to state: firewalld required, Cloudflare-only 80/443, dedicated IPv4/IPv6 ipsets, WireGuard-only 81, no automatic service ports, invalid refresh preserves last known good, one recoverable transaction, and no privileged timer. Reject the old statement that setup simply opens 80/443/81.
- [ ] **Step 2: Confirm old documentation fails**
```bash
tests/setup/readme-contract
```
- [ ] **Step 3: Update docs without live-cutover claims**
Explain that `setup-server` and server upgrades refresh the policy; first setup may use committed official ranges; missing WireGuard leaves 81 closed; conflicts require manual inspection; fixture tests model rollback but do not apply host rules.
- [ ] **Step 4: Run the firewall plan gate**
```bash
bash -n setup/scripts/setup-server tests/setup/server-firewall-contract
python3 -m py_compile server/scripts/update-firewall
server/scripts/update-firewall --validate-only server/firewall
tests/setup/server-firewall-contract
tests/setup/role-contract
tests/setup/readme-contract
tests/server/containers-shape-contract
./bin/panama test --safe
git diff --check
```
Expected: 134 hermetic contracts pass after the SSH, provenance, and firewall contracts exist; non-hermetic skip counts remain unchanged.
- [ ] **Step 5: Commit documentation and final fixture adjustments**
```bash
git add README.md server/README.md .claude/skills/panama/SKILL.md \
skills/panama-sudo/SKILL.md tests/setup/readme-contract \
tests/server/containers-shape-contract
git commit -m "Docs: Explain the server firewall transaction"
```
@@ -0,0 +1,380 @@
# SSH hardening transaction implementation plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make `boot --server` refuse unsafe SSH hardening and atomically roll back failed validation or reloads.
**Architecture:** Keep production logic in the standalone `boot` file because it runs before the repository exists. Exercise the public `boot --server` command through a PTY, a temporary filesystem root, and PATH command adapters; never call private functions directly or touch host SSH state.
**Tech Stack:** Bash 5, Python 3 standard library for the PTY driver, OpenSSH/systemd command adapters, Panama contract runner.
**Spec:** `docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md`
## Global constraints
- Preserve the public `boot --server` command and the non-root desktop bootstrap path.
- Do not reload a real SSH service, edit `/etc/ssh`, create a real account, or run a live privileged check.
- A missing or unsafe target key, missing SSH unit, declined prompt, or unsupported
pre-existing Panama drop-in keeps root/password access unchanged and continues bootstrap.
- Candidate validation or reload failure restores the prior drop-in and stops before clone/install handoff.
- Exact safe state is target ownership plus `.ssh` mode `0700` and `authorized_keys`
mode `0600`; symlinks, root UID, relative homes, blank/comment-only keys,
malformed non-comment lines, and foreign ownership are refused.
- The installed policy is `00-panama.conf` with `PermitRootLogin no`,
`PasswordAuthentication no`, and `KbdInteractiveAuthentication no`.
- Before reload, `sshd -t` and root/target `sshd -T -C` checks must prove the
desired effective policy. Earlier main-config precedence therefore fails closed.
- Root-key destination creation and writing run as the target UID. Revalidation
follows, and no same-named primary group is assumed.
- Existing regular drop-ins retain complete metadata on rollback. Symlinks,
directories, FIFOs, and other non-regular objects make hardening unavailable.
- Transaction traps are armed before the first candidate or backup artifact.
- `PANAMA_BOOT_FIXTURE_ROOT` is accepted only by a real non-root process whose stubbed `id -u` reports root. Real root plus that variable must fail closed.
- The new public contract is hermetic and must run under `panama test --safe`.
- Preserve the user's unstaged `/home/gib/.local/share/Panama/config/bash/.bashrc` change outside this worktree.
## File map
- `boot`: account/key preconditions, test-only filesystem adapter, SSH unit detection, atomic drop-in transaction, rollback.
- `tests/setup/root-server-bootstrap-contract`: PTY and stateful command/filesystem fixture for the real public command.
- `tests/contracts.manifest`: one `hermetic` entry for the new executable contract.
- `tests/setup/boot-contract`: retain the non-root clone/handoff behavior; adjust only if the later verified-bootstrap plan changes it.
- `tests/setup/role-contract`: retain role/stage assertions.
- `README.md`: describe key-gated transactional hardening without claiming a live reload was tested.
---
### Task 1: Gate hardening on a verified login path
**Files:**
- Create: `tests/setup/root-server-bootstrap-contract`
- Modify: `boot:52-115`
- Modify: `tests/contracts.manifest`
- Test: `tests/setup/root-server-bootstrap-contract`
**Interfaces:**
- Consumes: public `boot --server`, `PANAMA_BOOT_FIXTURE_ROOT`, PTY answers `gib`, password already set, and `Y` for hardening.
- Produces: `system_path ABSOLUTE_PATH`, `safe_authorized_keys USER HOME`, and `harden_server_ssh USER HOME`; later tasks extend the last function with the transaction.
- [ ] **Step 1: Build the public fixture and write the failing precondition table**
Create a checked temporary tree with `root/etc/ssh/sshd_config.d`, `root/root/.ssh`, `root/home/gib/.ssh`, `bin`, `calls`, and a stub cloned installer. Use Python's `pty.openpty()` to start:
```python
env = {
**os.environ,
"PATH": f"{stub_dir}:/usr/bin:/bin",
"PANAMA_BOOT_FIXTURE_ROOT": fixture_root,
"PANAMA_PATH": f"{fixture_root}/home/gib/.local/share/Panama",
"HOME": f"{fixture_root}/root",
}
process = subprocess.Popen(
["bash", boot, "--server"],
stdin=slave,
stdout=slave,
stderr=slave,
env=env,
start_new_session=True,
)
os.write(master, b"gib\nY\n")
```
PATH stubs must log one shell-escaped argv vector per line. `id -u` with no username reports `0`; `id -u root` reports `0`; `id -u gib` reports `1000`; `id -nG gib` prints `gib wheel`; `passwd -S gib` prints `gib PS`; `getent passwd gib` prints the logical absolute home `/home/gib`; `runuser` materializes the clone/install handoff without changing users. `boot` resolves that logical home beneath the fixture root through `system_path`. Stub `stat` for fixture ownership metadata plus `dnf`, `git`, `sshd`, and `systemctl`; any unexpected command exits `97`.
Run one table row per unsafe state:
```text
missing
empty
comment-only
malformed-key
mixed-valid-and-malformed-key
malformed-root-key
ssh-directory-symlink
authorized-keys-symlink
directory-wrong-mode
file-wrong-mode
directory-wrong-owner
file-wrong-owner
root-target-account
relative-home
```
For every row assert status `0`, a diagnostic containing `SSH hardening unavailable`, no `sshd -t`, no `systemctl reload`, no drop-in change, and a recorded install handoff. Add safe existing-key and safe root-key-copy rows that currently reach the unsafe direct-write path and therefore fail the new expected command ordering.
- [ ] **Step 2: Run the contract and confirm the red behavior**
Run:
```bash
bash -n tests/setup/root-server-bootstrap-contract
tests/setup/root-server-bootstrap-contract
```
Expected: nonzero findings showing current `boot` writes/reloads without the required key checks and has no fixture-root support.
- [ ] **Step 3: Add the guarded filesystem adapter and key checks**
Add these shapes near the root branch, using `stat -Lc` only after rejecting symlinks:
```bash
BOOT_ROOT="${PANAMA_BOOT_FIXTURE_ROOT:-}"
if [[ -n "$BOOT_ROOT" && "$EUID" -eq 0 ]]; then
echo "boot: PANAMA_BOOT_FIXTURE_ROOT is test-only" >&2
exit 1
fi
system_path() {
local path="$1"
[[ "$path" == /* ]] || return 2
printf '%s%s\n' "$BOOT_ROOT" "$path"
}
safe_authorized_keys() {
local username="$1" user_home="$2" uid ssh_dir keys
uid="$(id -u "$username")" || return 1
[[ "$uid" =~ ^[0-9]+$ && "$uid" != 0 && "$user_home" == /* ]] || return 1
ssh_dir="$user_home/.ssh"
keys="$ssh_dir/authorized_keys"
[[ -d "$ssh_dir" && ! -L "$ssh_dir" && -f "$keys" && ! -L "$keys" ]] || return 1
[[ "$(stat -Lc '%u:%a' "$ssh_dir")" == "$uid:700" ]] || return 1
[[ "$(stat -Lc '%u:%a' "$keys")" == "$uid:600" ]] || return 1
valid_authorized_keys "$keys"
}
```
`valid_authorized_keys` skips blank/comment lines, requires at least one remaining
line, and runs `ssh-keygen -l` on every remaining line. Resolve the target home,
`/root/.ssh/authorized_keys`, and `/etc/ssh/sshd_config.d` through `system_path`.
A copyable root key must be a non-symlinked regular file owned by UID 0, mode
`0600`, whose non-comment lines all parse. Never overwrite an existing
`authorized_keys`. When it is absent, either create a missing `.ssh` or require
an existing `.ssh` to already be a real directory owned by the target UID with
mode `0700`. Run final directory creation and key writing as the target UID,
then revalidate ownership, modes, and key parsing. Do not chown the destination
or assume the user's primary group is named after the user. If
`safe_authorized_keys` still fails, print the unavailable message and skip the
prompt/transaction.
- [ ] **Step 4: Add the hermetic manifest entry and run focused checks**
Insert the sorted manifest line with a directly preceding comment:
```text
# Root bootstrap runs entirely against a temporary filesystem and PATH adapters.
hermetic tests/setup/root-server-bootstrap-contract
```
Run:
```bash
bash -n boot tests/setup/root-server-bootstrap-contract
tests/setup/contract-manifest-contract
tests/setup/root-server-bootstrap-contract
tests/setup/boot-contract
tests/setup/role-contract
./bin/panama test --safe root-server-bootstrap
git diff --check
```
Expected: all pass; the public fixture proves unsafe keys do not invoke `sshd` or reload while bootstrap still hands off.
- [ ] **Step 5: Commit the precondition gate**
```bash
git add boot tests/setup/root-server-bootstrap-contract tests/contracts.manifest
git commit -m "Fix: Gate SSH hardening on a verified key"
```
---
### Task 2: Make the drop-in transaction recoverable
**Files:**
- Modify: `boot:42-137`
- Modify: `tests/setup/root-server-bootstrap-contract`
- Test: `tests/setup/root-server-bootstrap-contract`
**Interfaces:**
- Consumes: `safe_authorized_keys`, `system_path`, the fixture's `sshd`/`systemctl` state files, and a detected service name.
- Produces: `detect_ssh_unit`, `restore_ssh_dropin`, and a complete `harden_server_ssh USER HOME` transaction returning 0 only after validation and reload.
- [ ] **Step 1: Add failing success and rollback scenarios**
Extend the command state with `SSHD_RESULTS` and `RELOAD_RESULTS`, consumed one result per call. Add exact cases:
```text
success-without-prior-dropin: validate=0 reload=0
success-replaces-prior-dropin: validate=0 reload=0
candidate-invalid: validate=1,0 reload=<none>
effective-root-policy-conflict: syntax=0 root-policy=conflict rollback-validate=0 reload=<none>
effective-target-policy-conflict: syntax=0 root-policy=safe target-policy=conflict rollback-validate=0 reload=<none>
candidate-reload-fails: validate=0,0 reload=1,0
rollback-validation-fails: validate=0,1 reload=1
rollback-reload-fails: validate=0,0 reload=1,1
```
Assert the desired three-line content; syntax and root/target effective validation
before reload; only the detected unit; content and complete metadata restoration;
restored validation/reload ordering; nonzero status and no install handoff on every
transactional failure; no `*.tmp`/`*.backup` residue on success; and retained backup
or no-prior-file removal instructions when rollback fails. Add declined-hardening,
missing-unit, symlink/directory/FIFO drop-in, target-UID normalization, candidate and
backup preparation signals, and actual-root fixture-guard cases.
- [ ] **Step 2: Run the focused contract and confirm it fails on current code**
Run:
```bash
tests/setup/root-server-bootstrap-contract
```
Expected: nonzero because current code writes the final path directly, never validates, guesses units through reload failure, and reports success after failed reload.
- [ ] **Step 3: Implement unit detection, atomic install, and rollback**
Use explicit transaction state and traps. The core shape is:
```bash
detect_ssh_unit() {
local unit
for unit in sshd.service ssh.service; do
systemctl cat "$unit" >/dev/null 2>&1 && { printf '%s\n' "$unit"; return 0; }
done
return 1
}
restore_ssh_dropin() {
if (( ssh_had_prior )); then
local restore
restore="$(mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.restore)" || return 1
cp -a -- "$ssh_backup" "$restore"
mv -f -- "$restore" "$ssh_dropin"
else
rm -f -- "$ssh_dropin"
fi
}
```
Reject an existing `00-panama.conf` unless it is a non-symlink regular file. Save
the prior `EXIT`, `INT`, and `TERM` traps and arm state-aware preparation cleanup
before creating any artifact. Create the candidate with
`umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.tmp`, write the exact
three-line desired content, and preserve an existing final file with `cp -a` in a
collision-safe same-directory `mktemp` name ending `.backup`, not `.conf`.
Atomically activate with `mv -f`. Preparation signals remove known artifacts without
touching the final path; activated signals restore, validate, and reload. Every
success or handled failure path restores the prior traps before returning.
After activation:
```bash
if ! sshd -t; then
restore_ssh_dropin
sshd -t || rollback_failed=1
return 1
fi
if ! effective_ssh_policy_is_hardened "$username"; then
restore_ssh_dropin
sshd -t || rollback_failed=1
return 1
fi
if ! systemctl reload "$ssh_unit"; then
restore_ssh_dropin
sshd -t || rollback_failed=1
systemctl reload "$ssh_unit" || rollback_failed=1
return 1
fi
```
`effective_ssh_policy_is_hardened` uses `sshd -T -C` for root and target
contexts. Root must report all three denials; target must report both authentication
denials. On clean success clear the transaction state, restore traps, and remove the
backup. On rollback failure with a prior file, keep the metadata-preserving backup
and print its absolute path plus validation/reload commands. With no prior file,
print `rm -f -- /etc/ssh/sshd_config.d/00-panama.conf`, `sshd -t`, and the detected
reload command. Do not continue to clone/install after a transactional failure.
- [ ] **Step 4: Verify failure status, cleanup, and old public behavior**
Run:
```bash
bash -n boot tests/setup/root-server-bootstrap-contract
tests/setup/root-server-bootstrap-contract
tests/setup/boot-contract
tests/setup/role-contract
./bin/panama test --safe root-server-bootstrap
git diff --check
```
Expected: all pass; every rollback scenario preserves the previous drop-in and the safe success path validates before one reload.
- [ ] **Step 5: Commit the transaction**
```bash
git add boot tests/setup/root-server-bootstrap-contract
git commit -m "Fix: Roll back failed SSH hardening"
```
---
### Task 3: Synchronize operator documentation and close the SSH plan
**Files:**
- Modify: `README.md:40-48`
- Modify: `.claude/skills/panama/SKILL.md`
- Modify: `skills/panama-desktop/SKILL.md` only if it describes root bootstrap
- Test: `tests/setup/readme-contract`
**Interfaces:**
- Consumes: the landed `boot --server` behavior from Tasks 1-2.
- Produces: accurate user-facing preconditions, skip behavior, rollback behavior, and no claim of live-host proof.
- [ ] **Step 1: Write the documentation assertions first**
Extend `tests/setup/readme-contract` to require nearby root-bootstrap prose containing
all of: OpenSSH-parsed target keys, `00-panama.conf`, all three denials, `sshd -t`,
root/target `sshd -T`, atomic installation, reload rollback, and hardening unavailable
without a key. Reject wording that says Panama merely writes the file or that reload
failure is ignored.
- [ ] **Step 2: Run the README contract and confirm the old prose fails**
Run:
```bash
tests/setup/readme-contract
```
Expected: nonzero until README describes the transactional behavior.
- [ ] **Step 3: Update the documentation without claiming a live reload**
State plainly that Panama copies or verifies every target key line with OpenSSH,
offers hardening only with exact safe ownership/modes and a supported regular
drop-in, validates syntax and effective root/target policy, reloads the detected
unit, and restores the previous file with metadata on failure. State that fixture
contracts test these paths and no real daemon reload runs under `panama test --safe`.
- [ ] **Step 4: Run the plan gate**
```bash
bash -n boot tests/setup/root-server-bootstrap-contract
tests/setup/root-server-bootstrap-contract
tests/setup/boot-contract
tests/setup/role-contract
tests/setup/readme-contract
./bin/panama test --safe
git diff --check
```
Expected: 132 hermetic contracts pass after adding the new contract; non-hermetic skip counts remain unchanged.
- [ ] **Step 5: Commit the synchronized documentation**
```bash
git add README.md .claude/skills/panama/SKILL.md skills/panama-desktop/SKILL.md tests/setup/readme-contract
git commit -m "Docs: Explain transactional SSH hardening"
```
@@ -0,0 +1,586 @@
# Trusted installation inputs implementation plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Ensure every automatic executable input is publisher-signed or pinned by a reviewed SHA-256 before Panama executes or installs it.
**Architecture:** Add four small provenance helpers plus declarative reviewed pins. Keep vendor-specific decisions in `install-packages`, drive them through one hermetic public contract, and preserve existing installations when verification fails. Finish with a commit-pinned, digest-checked Panama bootstrap.
**Tech Stack:** Bash 5, GnuPG, rpmkeys with a temporary database, SHA-256, DNF5/Flatpak/Git command adapters, Panama contract runner.
**Spec:** `docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md`
## Global constraints
- Never execute fetched shell, install an unverified RPM, or accept a moving `latest` response during automatic setup.
- Download without sudo into a private checked temporary directory. Verify before any sudo, extraction, execution, or target replacement.
- Preserve a known-good installed version on every verification/download failure.
- Use exact complete fingerprints and reviewed per-architecture SHA-256 values from the approved spec.
- Use `curl --connect-timeout 10 --max-time 600`; enforce each configured maximum byte count before verification.
- Parse `setup/provenance/installers.conf` as data. Do not `source`, `eval`, or shell-expand it.
- Claude Desktop repository setup is optional and never automatic.
- No task may mutate the host package database, repository configuration, Flatpak remotes, or live installed tools.
- The Terra command gets at most one disposable Fedora 44 container smoke test with no host mounts, credentials, services, or production state.
- Package 3 owns authenticated `panama update`; Package 5 owns mutable source-app and Neovim inputs.
- Preserve `/home/gib/.local/share/Panama/config/bash/.bashrc` outside this worktree.
## File map
- `setup/lib/artifact-provenance`: fingerprint, digest-download, detached-signature, and temporary-RPM-keyring helpers.
- `setup/provenance/installers.conf`: strict reviewed versions, URLs, SHA-256 values, fingerprints, and maximum sizes.
- `setup/provenance/keys/*`: reviewed ASCII-armored public keys.
- `setup/provenance/README.md`: source, retrieval date, verification command, and rotation notes.
- `tests/setup/package-provenance-contract`: real cryptographic fixture plus stateful curl/sudo/DNF/Flatpak/rpm command adapters.
- `tests/setup/fixtures/provenance/*`: test-only GPG key, tiny signed manifest, good/tampered artifacts, and trusted/untrusted repo data.
- `setup/scripts/install-packages`: vendor-specific verified repository/artifact flows.
- `install`: include provenance and installer behavior in the package-stage hash.
- `setup/scripts/link-vicinae-scripts`: use `npm ci` against the tracked lock.
- `boot`, `README.md`, `tests/setup/boot-contract`, `tests/setup/readme-contract`: verified initial Panama revision and boot digest.
- `tests/contracts.manifest`: one new hermetic contract.
---
### Task 1: Build and prove the provenance helpers
**Files:**
- Create: `setup/lib/artifact-provenance`
- Create: `setup/provenance/installers.conf`
- Create: `setup/provenance/keys/terra44.asc`
- Create: `setup/provenance/keys/claude-code.asc`
- Create: `setup/provenance/keys/bun.asc`
- Create: `setup/provenance/keys/rpmfusion-free.asc`
- Create: `setup/provenance/keys/rpmfusion-nonfree.asc`
- Create: `setup/provenance/keys/hyprland-copr.asc`
- Create: `setup/provenance/keys/flathub.asc`
- Create: `setup/provenance/keys/claude-desktop.asc`
- Create: `setup/provenance/README.md`
- Create: `tests/setup/package-provenance-contract`
- Create: `tests/setup/fixtures/provenance/`
- Modify: `tests/contracts.manifest`
**Interfaces:**
- Consumes: `curl`, `gpg`, `sha256sum`, `rpmkeys`, `stat`, and strict `NAME=value` provenance data.
- Produces: `load_installer_provenance FILE`, `key_fingerprint_matches FILE EXPECTED`, `download_sha256 URL EXPECTED MAX_BYTES DEST`, `verify_detached_signature KEY SIGNATURE CONTENT`, and `rpm_signature_matches PACKAGE KEY EXPECTED`.
- [ ] **Step 1: Create cryptographic fixtures and write failing helper cases**
Generate a test-only key in a temporary `GNUPGHOME`, export its public key, sign a four-line `SHASUMS256.txt`, and commit only the public key, content, detached signature, a good tiny artifact, and a one-byte-tampered artifact. The private key must never enter the repository; the contract does not need it after fixture creation.
Write contract cases that source only `setup/lib/artifact-provenance` and assert:
```text
known fingerprint -> 0
wrong fingerprint -> nonzero
valid detached signature -> 0
wrong content or signature -> nonzero
matching digest/size -> atomic destination created
wrong digest, oversized body, interrupted curl -> destination absent or original bytes preserved
valid RPM signed by fixture key -> 0 through a temporary rpmdb
unsigned/wrong-key RPM -> nonzero and host rpmdb untouched
```
Add provenance-parser cases for unknown key, duplicate key, missing required key, whitespace around the name, shell expansion text, and unsupported architecture.
- [ ] **Step 2: Run the new contract and confirm the missing-helper failure**
```bash
bash -n tests/setup/package-provenance-contract
tests/setup/package-provenance-contract
```
Expected: nonzero because the helper/config do not exist.
- [ ] **Step 3: Implement exact helpers**
Use these signatures and behaviors:
```bash
key_fingerprint_matches() {
local file="$1" expected="$2" actual
actual="$(gpg --batch --with-colons --import-options show-only --import "$file" 2>/dev/null \
| awk -F: '$1 == "fpr" { print $10; exit }')"
[[ "$actual" == "$expected" ]]
}
verify_detached_signature() {
local key="$1" signature="$2" content="$3" home
home="$(mktemp -d)" || return 1
chmod 700 "$home"
GNUPGHOME="$home" gpg --batch --quiet --import "$key" >/dev/null 2>&1 \
&& GNUPGHOME="$home" gpg --batch --verify "$signature" "$content" >/dev/null 2>&1
local status=$?
rm -rf -- "$home"
return "$status"
}
```
`download_sha256` downloads to `DEST.part`, passes `--max-filesize MAX_BYTES`, verifies `stat -c %s <= MAX_BYTES`, compares a lowercase 64-hex digest, then `mv -f` atomically. Its EXIT/INT/TERM cleanup removes only the checked `.part` path.
The installer may define one private `download_bounded URL MAX_BYTES DEST` wrapper for publisher-signed RPMs whose trust assertion is the later RPM signature rather than a reviewed digest. It uses the same curl timeouts, `.part` cleanup, post-download size check, and atomic rename as `download_sha256`; it does not execute or install the result before `rpm_signature_matches` succeeds.
`rpm_signature_matches` creates a private temporary rpmdb, imports only `KEY`, verifies the complete expected primary fingerprint before import, and requires `rpmkeys --dbpath DB --checksig PACKAGE` success with an OpenPGP signature line. It never imports into the host keyring.
`load_installer_provenance` reads with `IFS='=' read -r name value`, accepts only an explicit name allowlist, rejects duplicate/empty values and any line without exactly one `=`, and exports nothing. Store values in one associative array named `INSTALLER_PROVENANCE`.
- [ ] **Step 4: Add reviewed keys and config**
Fetch each key from the exact source URL in the spec to a temporary directory, verify its full fingerprint, and add its exact ASCII-armored content with `apply_patch`. Fill `installers.conf` with these reviewed values and conservative byte caps:
```text
BUN_VERSION=1.4.0
BUN_X86_64_URL=https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-x64.zip
BUN_X86_64_SHA256=2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452
BUN_X86_64_MAX_BYTES=67108864
BUN_AARCH64_URL=https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-aarch64.zip
BUN_AARCH64_SHA256=4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e
BUN_AARCH64_MAX_BYTES=67108864
NODE_VERSION=24.20.0
NODE_X86_64_URL=https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-x64.tar.xz
NODE_X86_64_SHA256=2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2
NODE_X86_64_MAX_BYTES=67108864
NODE_AARCH64_URL=https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-arm64.tar.xz
NODE_AARCH64_SHA256=5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7
NODE_AARCH64_MAX_BYTES=67108864
CODEX_VERSION=0.150.1
CODEX_X86_64_URL=https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-x86_64-unknown-linux-musl.tar.gz
CODEX_X86_64_SHA256=00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17
CODEX_X86_64_MAX_BYTES=134217728
CODEX_AARCH64_URL=https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-aarch64-unknown-linux-musl.tar.gz
CODEX_AARCH64_SHA256=1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639
CODEX_AARCH64_MAX_BYTES=134217728
RUSTDESK_VERSION=1.4.9
RUSTDESK_X86_64_URL=https://github.com/rustdesk/rustdesk/releases/download/1.4.9/rustdesk-1.4.9-0.x86_64.rpm
RUSTDESK_X86_64_SHA256=eb1b053ac5b2f774f2271f7fbbfd2ea475899f7a55135c5e172bc54b9388f108
RUSTDESK_X86_64_MAX_BYTES=134217728
FEDORA_RELEASE=44
RPMFUSION_FREE_RELEASE_URL=https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-44.noarch.rpm
RPMFUSION_FREE_RELEASE_MAX_BYTES=4194304
RPMFUSION_NONFREE_RELEASE_URL=https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm
RPMFUSION_NONFREE_RELEASE_MAX_BYTES=4194304
TERRA_BASEURL=https://repos.fyralabs.com/terra44
HYPRLAND_COPR_BASEURL=https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/
FLATHUB_DESCRIPTOR_URL=https://flathub.org/repo/flathub.flatpakrepo
FLATHUB_DESCRIPTOR_MAX_BYTES=1048576
CLAUDE_CODE_BASEURL=https://downloads.claude.ai/claude-code/rpm/stable
CLAUDE_DESKTOP_BASEURL=https://patrickjaja.github.io/claude-desktop-extra/rpm/
TERRA_FINGERPRINT=AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
CLAUDE_CODE_FINGERPRINT=31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE
BUN_FINGERPRINT=F3DCC08A8572C0749B3E18888EAB4D40A7B22B59
RPMFUSION_FREE_FINGERPRINT=E9A491A3DE247814E7E067EAE06F8ECDD651FF2E
RPMFUSION_NONFREE_FINGERPRINT=79BDB88F9BBF73910FD4095B6A2AF96194843C65
HYPRLAND_COPR_FINGERPRINT=97E23476C89635135407C7D5E9BA41342C4B2995
FLATHUB_FINGERPRINT=6E5C05D979C76DAF93C081354184DD4D907A7CAE
CLAUDE_DESKTOP_FINGERPRINT=825A7D15D78BABE45646D5DF382409F597908867
```
Use the exact hashes and fingerprints from the spec; no value may be resolved through `latest`. The provenance README must list every source URL and the command used to verify it on 2026-08-27.
- [ ] **Step 5: Add the manifest entry and run the focused gate**
```text
# Provenance uses local signed fixtures and stubs every network/package operation.
hermetic tests/setup/package-provenance-contract
```
Run:
```bash
bash -n setup/lib/artifact-provenance tests/setup/package-provenance-contract
tests/setup/package-provenance-contract
tests/setup/contract-manifest-contract
./bin/panama test --safe package-provenance
git diff --check
```
Expected: all pass and no host GPG/RPM state changes.
- [ ] **Step 6: Commit the provenance foundation**
```bash
git add setup/lib/artifact-provenance setup/provenance tests/setup/fixtures/provenance \
tests/setup/package-provenance-contract tests/contracts.manifest
git commit -m "Test: Add installer provenance boundary"
```
---
### Task 2: Verify third-party repository roots
**Files:**
- Modify: `setup/scripts/install-packages:218-276,339-370,396-456`
- Modify: `tests/setup/package-provenance-contract`
- Test: `tests/setup/desktop-first-contract`
- Test: `tests/setup/package-lists-contract`
**Interfaces:**
- Consumes: `INSTALLER_PROVENANCE`, the four helper functions, vendored keys, and command adapters.
- Produces: `install_rpmfusion_repositories`, `install_terra_repository`, `configure_hyprland_repository`, `ensure_flathub_remote`, `install_claude_code`, and `install_claude_desktop_if_trusted`.
- [ ] **Step 1: Add public installer cases for every repository**
Run a fixture copy of `install-packages` with temporary HOME/state and stubbed `sudo`, `dnf`, `rpm`, `rpmkeys`, `curl`, `flatpak`, and `gpg`. Assert exact command-log order and policy:
```text
RPM Fusion: exact Fedora 44 URL -> size cap -> RPM signature -> localpkg_gpgcheck=1 install
Terra: exact F44 key -> pkg_gpgcheck=1 -> repo_gpgcheck=1 -> local gpgkey -> terra-release
COPR: exact baseurl/local key, package gpgcheck=1, explicit metadata-signature exception; no `dnf copr enable`
Flathub: decoded embedded key fingerprint and GPG-enabled remote; mismatch preserves existing remote
Claude Code: exact Anthropic key/repo checks before DNF
Claude Desktop absent/untrusted: one manual message, no download, no DNF, overall success
Claude Desktop trusted existing repo: DNF install only
```
Inject wrong keys, wrong base URLs, GPG flags off, signature failure, and DNF failure. Assert nothing downstream in the dependent transaction runs after a trust-root failure. Require `rpm -E %fedora` to equal the reviewed `FEDORA_RELEASE`; any other release fails before a third-party download or repository mutation.
- [ ] **Step 2: Run the contract and confirm current unsafe paths fail**
```bash
tests/setup/package-provenance-contract
```
Expected: nonzero findings for `--nogpgcheck`, TOFU COPR, unvalidated Flathub, remote-script Claude Desktop, and unverified RPM Fusion URLs.
- [ ] **Step 3: Implement signed repository setup**
Source the helper and load the config from `PANAMA_PATH` at installer start. Download RPM Fusion release RPMs, verify signatures with the matching vendored key, then call:
```bash
sudo dnf install -y --setopt=localpkg_gpgcheck=1 "$free_rpm" "$nonfree_rpm"
```
Replace Terra with `--repofrompath terra,https://repos.fyralabs.com/terra44` plus:
```text
--setopt=terra.pkg_gpgcheck=1
--setopt=terra.repo_gpgcheck=1
--setopt=terra.gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama
```
Stage repo/key files completely before atomic sudo install. Write the COPR repo from reviewed local values rather than `dnf copr enable`, with:
```ini
[panama-hyprland]
name=Panama reviewed Hyprland COPR
baseurl=https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/
enabled=1
gpgcheck=1
repo_gpgcheck=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland
```
The `repo_gpgcheck=0` line is the single audited exception: the publisher returns no `repodata/repomd.xml.asc`, while its RPMs are signed by the pinned project key. The contract rejects this exception for every other repository and still requires package signatures.
Parse the Flathub descriptor as INI data, base64-decode `GPGKey`, verify its fingerprint, and reject `NoGPGVerify=true` or equivalent disabled state. Write Claude Code's stable repository with the vendored local key, `gpgcheck=1`, and `repo_gpgcheck=1`; its publisher provides signed metadata.
For Claude Desktop, inspect only an already configured repo. Trust it only when its base URL equals `https://patrickjaja.github.io/claude-desktop-extra/rpm/`, both GPG checks are `1`, and its `gpgkey` is an existing local file whose complete fingerprint matches the vendored `claude-desktop.asc`. Never curl or run the community setup script. The host's current remote-key configuration is therefore treated as untrusted until an operator configures a local reviewed key. Untrusted/absent configuration logs an optional manual instruction and returns success.
- [ ] **Step 4: Verify ordering and regression contracts**
```bash
bash -n setup/scripts/install-packages tests/setup/package-provenance-contract
tests/setup/package-provenance-contract
tests/setup/desktop-first-contract
tests/setup/package-lists-contract
./bin/panama test --safe package-provenance
git diff --check
```
- [ ] **Step 5: Commit repository trust roots**
```bash
git add setup/scripts/install-packages tests/setup/package-provenance-contract
git commit -m "Fix: Verify third-party package repositories"
```
---
### Task 3: Pin language runtimes and agent tools
**Files:**
- Modify: `setup/scripts/install-packages:100-177,334-394`
- Modify: `tests/setup/package-provenance-contract`
- Modify: `tests/quickshell/declared-dependencies-contract`
**Interfaces:**
- Consumes: `download_sha256`, loaded reviewed pins, supported `uname -m` values `x86_64` and `aarch64`.
- Produces: `install_node`, `install_bun`, `install_claude_code`, `install_codex`, and `install_rustdesk` with verified staging and known-good preservation.
- [ ] **Step 1: Add failing per-architecture and preservation cases**
For each artifact, assert exact URL/digest selection for x86_64 and aarch64, unsupported-architecture refusal before curl, digest mismatch preserving a seeded old version, interrupted download cleanup, atomic replacement, and already-installed exact-version no-op.
Require source scans and public command logs to reject:
```text
curl ... | bash
nvm install --lts
npm install -g pnpm
npm install -g @openai/codex
releases/latest
api.github.com/.../releases/latest
```
RustDesk supports only the reviewed x86_64 RPM. aarch64 records a deliberate soft failure without downloading. pnpm must come from signed Fedora DNF; no network-script/npm fallback is allowed.
- [ ] **Step 2: Run the focused contract and confirm it fails on moving inputs**
```bash
tests/setup/package-provenance-contract
```
Expected: nonzero findings naming each current moving or piped installer.
- [ ] **Step 3: Implement verified atomic installs**
Map architecture once:
```bash
case "$(uname -m)" in
x86_64) artifact_arch=X86_64 ;;
aarch64) artifact_arch=AARCH64 ;;
*) log "Unsupported architecture: $(uname -m)"; return 1 ;;
esac
```
Install Node 24.20.0 into `$NVM_DIR/versions/node/v24.20.0` from a sibling staging directory, reject archive entries outside the expected single top-level directory, require staged `bin/node --version` to print `v24.20.0`, then rename and set nvm's default alias to `24.20.0` without `nvm install`.
For Bun, reject unexpected archive paths, stage the binary at `$HOME/.bun/versions/1.4.0/bin/bun`, require `--version` to print `1.4.0`, then atomically replace a temporary symlink at `$HOME/.bun/bin/bun`. For Codex, reject absolute/parent-traversal tar members, stage the release's `codex` binary at `$HOME/.local/lib/panama/codex/0.150.1/codex`, require `--version` to identify `0.150.1`, then atomically replace `$HOME/.local/bin/codex` through a temporary symlink. Existing version directories must match the reviewed binary/version or cause a soft failure; never delete and recreate an unverified collision.
Install Claude Code through the signed stable repository from Task 2. Download RustDesk's versioned RPM, verify SHA-256, then pass only that local path to DNF. Install pnpm through the signed Fedora package transaction and record a soft failure if unavailable. Every archive extracts into a checked private directory first; no archive writes directly into its final prefix.
Every helper failure appends the same component name to `softly_failed`; `report_soft_failures` keeps the package hash unstamped.
- [ ] **Step 4: Run focused and dependency checks**
```bash
bash -n setup/scripts/install-packages tests/setup/package-provenance-contract
tests/setup/package-provenance-contract
tests/quickshell/declared-dependencies-contract
tests/setup/desktop-first-contract
./bin/panama test --safe package-provenance
git diff --check
```
- [ ] **Step 5: Commit pinned user tools**
```bash
git add setup/scripts/install-packages tests/setup/package-provenance-contract \
tests/quickshell/declared-dependencies-contract
git commit -m "Fix: Pin runtime and agent artifacts"
```
---
### Task 4: Invalidate stale installer state and lock npm installs
**Files:**
- Modify: `install:67-106`
- Modify: `setup/scripts/link-vicinae-scripts:79-97`
- Modify: `tests/setup/launcher-search-contract`
- Modify: `tests/setup/update-command-contract`
- Modify: `tests/setup/package-provenance-contract`
**Interfaces:**
- Consumes: tracked package lists, installer, provenance helper/config/keys, and extension lockfile.
- Produces: `hash_packages` covering every installer trust input and Vicinae `npm ci` behavior.
- [ ] **Step 1: Write failing hash and lockfile assertions**
Run `hash_packages` from a disposable installer copy and assert the digest changes independently when each of these changes:
```text
setup/packages/core-packages
setup/scripts/install-packages
setup/lib/artifact-provenance
setup/provenance/installers.conf
one setup/provenance/keys file
```
In the Vicinae fixture, stub npm and require argv `ci`, not `install`. Seed a lock mismatch and assert nonzero extension-build status with the lockfile byte-for-byte unchanged.
- [ ] **Step 2: Confirm current hash and npm behavior fail**
```bash
tests/setup/package-provenance-contract
tests/setup/launcher-search-contract
```
Expected: current hash ignores installer/provenance changes and extension setup invokes `npm install`.
- [ ] **Step 3: Hash exact inputs and switch to `npm ci`**
Replace the current `find ... -maxdepth 1` stream with a sorted NUL-safe list containing top-level package files, `setup/scripts/install-packages`, `setup/lib/artifact-provenance`, and every regular file under `setup/provenance`. Hash file paths plus contents so renames change the digest.
Change only the extension dependency command to `npm ci`; do not add lockfile repair or update behavior.
- [ ] **Step 4: Verify upgrade and extension behavior**
```bash
bash -n install setup/scripts/link-vicinae-scripts
tests/setup/package-provenance-contract
tests/setup/launcher-search-contract
tests/setup/update-command-contract
./bin/panama test --safe package-provenance
git diff --check
```
- [ ] **Step 5: Commit state invalidation and npm locking**
```bash
git add install setup/scripts/link-vicinae-scripts tests/setup/launcher-search-contract \
tests/setup/update-command-contract tests/setup/package-provenance-contract
git commit -m "Fix: Re-run verified installer inputs"
```
---
### Task 5: Verify the initial Panama revision before handoff
**Files:**
- Modify: `boot:20-166`
- Modify: `tests/setup/boot-contract`
- Modify: `README.md:1-48`
- Modify: `tests/setup/readme-contract`
- Modify: `.claude/skills/panama/SKILL.md`
- Modify: `skills/panama-desktop/SKILL.md`
**Interfaces:**
- Consumes: `PANAMA_BOOT_REVISION` as a full lowercase 40-hex commit and `PANAMA_BOOT_SHA256` as a lowercase 64-hex digest.
- Produces: verified fresh clone at that revision, fast-forward-only clean existing checkout, and documentation pinned to the implementation commit immediately preceding its documentation commit.
- [ ] **Step 1: Replace old permissive boot-contract expectations with red trust cases**
The public fixture must assert:
```text
missing/malformed revision -> no git clone/fetch and no install
fresh clone -> fetch exact revision, resolve HEAD^{commit}, equality, handoff
HEAD mismatch -> nonzero, no install
existing clean ancestor -> fast-forward to exact revision, then install
existing dirty or divergent checkout -> nonzero, no reset, no install
fetch failure -> nonzero, no install
```
Delete the old assertion that a failed pull proceeds with the checkout as-is. Add README assertions rejecting `bash <(curl .../main/boot)` and requiring a commit URL, 40-hex revision, 64-hex SHA-256, `sha256sum -c`, 10-second connect timeout, 30-second total timeout, and 256 KiB maximum.
- [ ] **Step 2: Run boot and README contracts to prove they fail**
```bash
tests/setup/boot-contract
tests/setup/readme-contract
```
Expected: both fail on the mutable branch bootstrap and permissive pull fallback.
- [ ] **Step 3: Implement exact-revision clone/handoff and commit it**
Validate inputs before Git:
```bash
[[ "${PANAMA_BOOT_REVISION:-}" =~ ^[0-9a-f]{40}$ ]] || exit 1
[[ "${PANAMA_BOOT_SHA256:-}" =~ ^[0-9a-f]{64}$ ]] || exit 1
actual_boot_sha="$(sha256sum "${BASH_SOURCE[0]}" | cut -d' ' -f1)"
[[ "$actual_boot_sha" == "$PANAMA_BOOT_SHA256" ]] || exit 1
```
For a fresh destination, initialize/fetch the exact commit, verify `git rev-parse HEAD^{commit}` equality, create local `main` at that commit, and set `branch.main.remote=origin` plus `branch.main.merge=refs/heads/main`. For an existing checkout, require empty `git status --porcelain`, fetch the exact commit, require `git merge-base --is-ancestor HEAD REVISION`, and fast-forward only. Never use reset or execute after mismatch/failure.
Run focused tests, then commit only implementation and contract changes:
```bash
bash -n boot tests/setup/boot-contract
tests/setup/boot-contract
tests/setup/root-server-bootstrap-contract
git add boot tests/setup/boot-contract tests/setup/root-server-bootstrap-contract
git commit -m "Fix: Verify the initial Panama revision"
```
- [ ] **Step 4: Compute the committed boot pin and write the documented command**
Use the implementation commit just created:
```bash
bootstrap_commit="$(git rev-parse HEAD)"
bootstrap_sha="$(git show "$bootstrap_commit:boot" | sha256sum | cut -d' ' -f1)"
```
Write README commands that download
`https://git.gbrown.org/gib/Panama/raw/commit/$bootstrap_commit/boot` to a checked temporary file with `curl --connect-timeout 10 --max-time 30 --max-filesize 262144`, compare `$bootstrap_sha` through `sha256sum -c`, then invoke with both environment values. Use the same verified command for desktop and `--server`; never pipe the response to Bash.
The README contract must parse those literal values, run `git cat-file -e COMMIT^{commit}`, and require:
```bash
test "$(git show "$commit:boot" | sha256sum | cut -d' ' -f1)" = "$documented_sha"
```
- [ ] **Step 5: Update operator skills and run the complete installer plan gate**
```bash
bash -n boot install setup/scripts/install-packages setup/scripts/link-vicinae-scripts \
setup/lib/artifact-provenance tests/setup/package-provenance-contract \
tests/setup/boot-contract
tests/setup/package-provenance-contract
tests/setup/boot-contract
tests/setup/root-server-bootstrap-contract
tests/setup/readme-contract
tests/setup/package-lists-contract
tests/setup/desktop-first-contract
tests/setup/launcher-search-contract
tests/setup/update-command-contract
./bin/panama test --safe
git diff --check
```
Expected: 133 hermetic contracts pass after the SSH and provenance contracts exist; non-hermetic skip counts remain unchanged.
- [ ] **Step 6: Commit the pinned documentation**
```bash
git add README.md tests/setup/readme-contract .claude/skills/panama/SKILL.md \
skills/panama-desktop/SKILL.md setup/provenance/README.md
git commit -m "Docs: Pin the verified Panama bootstrap"
```
---
### Task 6: Smoke-test signed Terra bootstrap in a disposable Fedora 44 container
**Files:**
- Modify: `setup/provenance/README.md`
- Modify: `tests/setup/package-provenance-contract` only if the smoke test exposes a fixture gap
**Interfaces:**
- Consumes: exact Terra key/repo command landed in Task 2.
- Produces: recorded disposable proof or a fail-closed Terra-unavailable implementation; never host installation.
- [ ] **Step 1: Preflight the disposable target**
Require rootless Podman, no bind mounts, a fresh `registry.fedoraproject.org/fedora:44` container, and no forwarded credentials or host sockets. The command may download repository metadata and the `terra-release` package only inside the disposable container.
- [ ] **Step 2: Run the exact key and DNF verification path**
Copy only the vendored Terra key into the container, verify its full fingerprint, then run the exact `--repofrompath` and three `terra.*` GPG settings from Task 2. Query the resulting repo file and package signature settings. Remove the container on exit.
Expected: DNF installs `terra-release` with both package and metadata verification enabled and without `--nogpgcheck`.
- [ ] **Step 3: Apply the fail-closed result**
If the exact command fails, do not weaken GPG settings. Change the installer to print Terra unavailable and exit before initial/desktop/Hyprland transactions; update the fixture expectation to that branch. If it succeeds, make no production change.
- [ ] **Step 4: Record proof and rerun the hermetic gate**
Document the container image, date, exact command, exit status, key fingerprint, and inspected repo settings in `setup/provenance/README.md`. Do not claim host installation.
```bash
tests/setup/package-provenance-contract
./bin/panama test --safe
git diff --check
```
- [ ] **Step 5: Commit the provenance proof**
```bash
git add setup/provenance/README.md setup/scripts/install-packages \
tests/setup/package-provenance-contract
git commit -m "Docs: Record signed Terra bootstrap proof"
```
@@ -0,0 +1,502 @@
# 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-host tests/quickshell/updates-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-host`: reads the real machine or session without changing it or contacting a non-fixture endpoint.
- `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-host --allow live-compositor`; 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`.
Non-hermetic checks are separate, explicitly granted gates after all hermetic work is
green. `live-host` remains read-only; compositor, desktop, and network checks receive
only their named grants. 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 read live host/compositor state or 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.
@@ -0,0 +1,539 @@
# Secure bootstrap and privileged installation design
**Status:** Approved in chat on 2026-08-27
**Parent program:** `docs/superpowers/specs/2026-08-26-repository-audit-remediation-design.md`
## Purpose
This document refines Package 2 after repository-wide reconnaissance found that the
same trust boundary extends beyond the three examples named in the parent design.
Panama must not turn mutable network content into root-capable code. That includes
the initial root bootstrap, package-manager repository bootstraps, downloaded
installers and artifacts, and user-level processes that run while the installer
keeps a live sudo credential.
The package still has three independently testable parts:
1. transactional SSH hardening in `boot --server`;
2. verified bootstrap and installer inputs;
3. transactional server firewall policy.
All three are fixture-tested. This branch does not reload a real SSH daemon, mutate a
real firewall, install a real package, start a service, or apply changes to a server.
## Decisions
- Keep automatic installation when an official signature or a reviewed immutable
digest is available. Otherwise make the component explicit and optional.
- Keep the public `boot --server` interface, but remove the mutable
`bash <(curl .../main/boot)` installation path from the documentation.
- A missing or unsafe target SSH key makes hardening unavailable but does not stop
bootstrap. Validation or reload failure rolls back and stops bootstrap.
- Safe target SSH state means a non-root account, an absolute home, a real `.ssh`
directory owned by the target UID with mode `0700`, and a nonempty regular
`authorized_keys` file owned by the target UID with mode `0600`. Symlinks are
refused. Every nonblank, non-comment key line must parse with OpenSSH tooling.
Panama may create and normalize files it copied from root, but final destination
creation and writing run as the target UID. It does not take ownership of an
unsafe pre-existing target path or assume that the user's primary group has the
same name as the user.
- Failed SSH reload rollback includes restored-config validation and a reload of the
restored configuration, because a command can apply state and still return
nonzero.
- Firewalld is required. Setup attempts to enable and start it, then verifies both
states before any rule mutation.
- Ports 80 and 443 are public only through Cloudflare source ipsets. Port 81 is open
only in a unique WireGuard-only zone. With no WireGuard zone it stays closed and
setup reports that fact; ambiguity or an unsafe mixed-interface zone fails before
mutation.
- Panama removes only its own rules and the exact legacy direct `80/tcp`, `443/tcp`,
and `81/tcp` rules created by the old server installer. Other broad rules are
reported as conflicts, not silently deleted.
- There is no privileged firewall timer. Server installation and upgrades are the
refresh entry points.
- Use narrow verification helpers, not a generalized installer framework.
## Shared trust model
`install` obtains one sudo credential and refreshes it until the run ends. Therefore
code executed as the target user during that window is root-capable in practice.
Panama treats fetched shell, binaries, RPM scriptlets, npm lifecycle code, container
entrypoints, and sourced repository build definitions as executable inputs.
Fetched JSON, repository descriptors, keys, checksums, HTML, icons, and CIDR lists are
data only while Panama parses them without executing them. They still require strict
shape, size, signer, and destination checks before they can authorize an executable
transaction.
Every download follows this order:
1. create a checked private temporary directory;
2. download without sudo with redirects, a 10-second connection timeout, a
600-second total timeout, and the reviewed per-artifact byte limit;
3. verify the expected fingerprint, signature, or reviewed SHA-256;
4. stage the complete result on the destination filesystem;
5. use sudo only for the narrow final package/repository operation that needs it;
6. atomically replace user-owned installed artifacts;
7. remove temporary material on success, failure, INT, and TERM.
Verification failure never falls back to the fetched artifact and never removes a
known-good installed version.
## SSH hardening transaction
### Public flow
The root `boot --server` branch keeps account creation, password setup, key copy,
clone, and target-user handoff in the standalone `boot` file. It adds one private
function, `harden_server_ssh USER HOME`, because no repository helper exists before
the clone.
Before offering hardening, Panama verifies:
- the target account exists, has a numeric UID other than 0, and its `getent` home is
absolute and nonempty;
- neither the home-relative `.ssh` path nor `authorized_keys` is a symlink;
- `.ssh` and `authorized_keys` have the exact ownership and modes in Decisions;
- `authorized_keys` contains at least one nonblank, non-comment line, and OpenSSH
parses every such line as a public key;
- an installed SSH unit is detected, preferring `sshd.service` and falling back to
`ssh.service` only when the first unit is absent.
If the target has no key and root has a safe regular key file, Panama copies only that
file. The target UID creates or normalizes `.ssh` at `0700` and writes
`authorized_keys` at `0600` through an already-open root-key input. Panama then
revalidates exact UID ownership, modes, and OpenSSH key parsing. It does not chown a
target-controlled path or assume a same-named primary group.
If the key preconditions fail, no SSH unit is installed, or the Panama drop-in path
already names a symlink or non-regular object, Panama prints why hardening is
unavailable, keeps root/password authentication unchanged, and continues the
clone/install handoff. Declining the prompt has the same unchanged-state outcome.
### Transaction
The desired drop-in is exactly:
```text
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
```
Panama creates the candidate with `umask 077` and `mktemp` in
`/etc/ssh/sshd_config.d`. Its temporary name does not end in `.conf`, so the normal
include glob cannot activate it early. It refuses a pre-existing Panama path unless
it is a non-symlink regular file. It preserves an existing `00-panama.conf`, including
its ownership, mode, timestamps, ACLs, and extended attributes, in the same directory.
State-aware EXIT/INT/TERM cleanup is armed before the first candidate or backup
artifact, and the candidate is atomically renamed over the final path.
It then runs `sshd -t` against the complete active configuration. Before reload,
`sshd -T -C` must report `permitrootlogin no`, `passwordauthentication no`, and
`kbdinteractiveauthentication no` for the root context. The target-user context must
report both authentication directives as `no`. This fails closed when an earlier
main-config directive wins despite the precedence-safe filename. Panama reloads only
the detected unit after every check passes. Success disarms rollback and removes the
backup.
On validation failure, Panama restores or removes the new drop-in, validates the
restored configuration, and returns nonzero without reloading the rejected candidate.
On reload failure, Panama restores the previous drop-in, validates it, reloads the
restored unit, and returns nonzero. A rollback validation/reload failure preserves the
backup and prints its path plus exact recovery commands. When no prior file existed,
recovery instead instructs the operator to remove `00-panama.conf`, run `sshd -t`,
and reload the detected unit.
Existing drop-ins go through the same desired-content, validation, and reload path;
mere existence is not treated as proof of hardening.
### Hermetic adapter
`PANAMA_BOOT_FIXTURE_ROOT` exists only for the contract. `boot` accepts it only when
the real effective UID is nonzero while the stubbed `id -u` reports root; an actual
root process that sets it exits before mutation. Absolute system paths are resolved
beneath that fixture root. Command adapters remain ordinary PATH stubs.
The public test seam is the real `boot --server` command under a PTY. The fixture
stubs `id`, `passwd`, `getent`, `useradd`, `usermod`, `runuser`, `git`, `dnf`, `sshd`,
and `systemctl`, and provides temporary account and filesystem state.
Required cases are:
- missing, empty, comment-only, malformed, mixed valid/malformed, symlinked,
wrong-owner, and wrong-mode target keys;
- safe root-key copy and safe existing target key;
- target-UID copy normalization with a primary group whose name differs from the user;
- declined hardening and no installed SSH unit;
- pre-existing symlink, directory, and FIFO Panama drop-ins;
- successful initial install and replacement of an existing drop-in;
- invalid syntax and conflicting effective-policy rollback;
- failed reload rollback, including restored validation and reload;
- rollback failure retaining its recovery artifact or printing no-prior-file removal;
- INT/TERM during candidate and backup preparation as well as after activation;
- actual-root rejection of `PANAMA_BOOT_FIXTURE_ROOT` in a user namespace;
- exact command ordering and no install handoff after a transactional failure.
## Verified bootstrap and installer inputs
### Initial Panama bootstrap
README installation instructions use a two-file trust assertion: a full 40-character
Git commit and the SHA-256 of `boot` at that commit. The command downloads the raw file
from the commit URL with a 10-second connection timeout, 30-second total timeout, and
256 KiB maximum, checks SHA-256, and only then executes it.
The verified revision is passed as `PANAMA_BOOT_REVISION`. `boot` requires a full
lowercase hexadecimal commit, fetches that exact object, verifies
`git rev-parse HEAD^{commit}` equality, and only then executes `install`. A fresh
clone creates local `main` at that commit with `origin/main` as its upstream. An
existing checkout must be clean and may only fast-forward to the verified commit;
Panama never resets or overwrites local work. A mismatch, dirty checkout, divergent
history, or fetch failure stops before handoff.
This uses two commits when refreshing the documented bootstrap: the implementation is
committed first; a following documentation commit records the preceding full commit
and `git show COMMIT:boot | sha256sum`. A hermetic contract recomputes the documented
digest from that committed object. A digest copied from the same hosting origin is not
a substitute for a long-lived publisher signing key, so Package 3 owns the stronger
authenticated-update/release-manifest design.
### Verification helpers and provenance data
Create `setup/lib/artifact-provenance` with only these public shell functions:
```text
key_fingerprint_matches FILE EXPECTED_FINGERPRINT
download_sha256 URL EXPECTED_SHA256 MAX_BYTES DESTINATION
verify_detached_signature KEY_FILE SIGNATURE_FILE CONTENT_FILE
rpm_signature_matches PACKAGE_FILE KEY_FILE EXPECTED_FINGERPRINT
```
`rpm_signature_matches` imports only the expected key into a temporary RPM database
and requires `rpmkeys --dbpath ... --checksig` to report a valid package signature;
it does not trust or modify the host RPM keyring.
Create `setup/provenance/installers.conf` as non-executable data. It uses one
`NAME=value` assignment per line, rejects unknown/duplicate keys, and is parsed without
`source` or `eval`. Per-architecture URLs, SHA-256 values, and maximum byte counts are
explicit.
Vendored public keys live under `setup/provenance/keys/`. The accompanying README
records the official source URL, retrieval date, complete primary fingerprint,
verification command, and rotation policy. Runtime verification compares the complete
primary fingerprint, not a short key ID.
As reviewed on 2026-08-27, the trust anchors are:
| Source | Primary fingerprint |
|---|---|
| Terra 44 | `AE09157A4DE88B497EA1D5D300CDAB43DE226D6F` |
| Anthropic Claude Code | `31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE` |
| Bun releases | `F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` |
| RPM Fusion free | `E9A491A3DE247814E7E067EAE06F8ECDD651FF2E` |
| RPM Fusion nonfree | `79BDB88F9BBF73910FD4095B6A2AF96194843C65` |
| lionheartp/Hyprland COPR | `97E23476C89635135407C7D5E9BA41342C4B2995` |
| Flathub | `6E5C05D979C76DAF93C081354184DD4D907A7CAE` |
| Claude Desktop Extra | `825A7D15D78BABE45646D5DF382409F597908867` |
The initial reviewed artifact pins are:
| Artifact | Architecture | SHA-256 |
|---|---|---|
| Bun 1.4.0 | x86_64 | `2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452` |
| Bun 1.4.0 | aarch64 | `4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e` |
| Node 24.20.0 | x86_64 | `2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2` |
| Node 24.20.0 | aarch64 | `5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7` |
| Codex 0.150.1 package | x86_64 musl | `00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17` |
| Codex 0.150.1 package | aarch64 musl | `1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639` |
| RustDesk 1.4.9 RPM | x86_64 | `eb1b053ac5b2f774f2271f7fbbfd2ea475899f7a55135c5e172bc54b9388f108` |
Unsupported architectures fail before download. Updating any version, URL, digest, or
key is an explicit reviewed repository change.
The provenance README cites these publisher-controlled records:
- Terra package instructions, key, and bootstrap limitation:
`https://github.com/terrapkg/packages/blob/frawhide/README.md`,
`https://repos.fyralabs.com/terra44/key.asc`, and
`https://github.com/terrapkg/packages/discussions/7736`;
- Anthropic setup and signing key: `https://code.claude.com/docs/en/setup` and
`https://downloads.claude.ai/keys/claude-code.asc`;
- Bun release and release-key usage:
`https://github.com/oven-sh/bun/releases/tag/bun-v1.4.0` and
`https://github.com/oven-sh/bun/blob/main/dockerhub/distroless/Dockerfile`;
- RPM Fusion keys:
`https://download1.rpmfusion.org/free/fedora/RPM-GPG-KEY-rpmfusion-free-fedora-2020`
and
`https://download1.rpmfusion.org/nonfree/fedora/RPM-GPG-KEY-rpmfusion-nonfree-fedora-2020`;
- Hyprland COPR key:
`https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/pubkey.gpg`;
- Flathub descriptor: `https://flathub.org/repo/flathub.flatpakrepo`;
- Claude Desktop Extra key:
`https://patrickjaja.github.io/claude-desktop-extra/gpg-key.asc`;
- Node release and verification instructions:
`https://github.com/nodejs/node/releases/tag/v24.20.0` and
`https://github.com/nodejs/node/blob/main/README.md`;
- Codex release and signing workflow:
`https://github.com/openai/codex/releases/tag/rust-v0.150.1` and
`https://github.com/openai/codex/blob/main/.github/workflows/rust-release.yml`;
- RustDesk release: `https://github.com/rustdesk/rustdesk/releases/tag/1.4.9`.
### Repository roots
- RPM Fusion release RPMs are downloaded first, verified against the vendored free
and nonfree keys, then installed as local files with
`--setopt=localpkg_gpgcheck=1`.
- Terra installs no package with `--nogpgcheck`. Panama verifies the Terra 44 key,
installs it atomically, and uses `terra.pkg_gpgcheck=1`,
`terra.repo_gpgcheck=1`, and the local pinned key with the official repository.
Failure stops before initial, desktop, or Hyprland package transactions.
- Hyprland COPR configuration is written from reviewed local data with the exact
base URL, `gpgcheck=1`, and the pinned project key. The publisher does not provide
`repomd.xml.asc`, so this repository has an explicit `repo_gpgcheck=0` exception;
package signatures remain mandatory. Panama does not use interactive/TOFU
`dnf copr enable -y`.
- Flathub's descriptor is downloaded as data. Panama decodes and verifies its embedded
primary key and requires GPG verification before adding or retaining the remote.
A mismatch preserves an existing remote and skips Flathub transactions.
The exact Terra DNF command receives a disposable Fedora 44 container smoke test. It
is never tried on the daily-driver host. If the signed bootstrap cannot be made to
work, Terra is reported unavailable and desktop installation stops before packages
that depend on it.
### User tools
- Node 24.20.0 is installed into the nvm version layout from the reviewed per-arch
archive and selected as the default. `nvm install --lts` is removed.
- pnpm comes from Fedora's signed `pnpm` package. If unavailable, Panama reports a
soft failure; it does not run npm or a remote installer as fallback.
- Bun is installed from the reviewed archive after SHA-256 verification; the remote
install script is removed.
- Claude Code uses Anthropic's signed RPM repository after exact key and repository
validation; `claude.ai/install.sh` is removed.
- Codex uses the reviewed per-arch package archive and an atomic user-owned install;
unversioned `npm install -g` is removed.
- RustDesk uses the reviewed versioned RPM URL and SHA-256 before the narrow sudo DNF
install. It never resolves `latest` at runtime.
- Claude Desktop repository setup is never downloaded or executed. If an existing
repository has the expected base URL, vendored-key fingerprint, `gpgcheck=1`, and
`repo_gpgcheck=1`, Panama may install from it. Otherwise it prints one optional
manual step and continues successfully.
- Vicinae extensions use `npm ci`; lock mismatch fails the extension build without
modifying the tracked lockfile.
Changing installer code, provenance data, keys, or package lists invalidates the
installer-stage hash so the next upgrade re-runs verification.
### Hermetic contract
`tests/setup/package-provenance-contract` runs real `gpg`/`sha256sum` against a test
key and signed tiny fixture manifest. It stubs curl, sudo, DNF, rpmkeys, Git, and
Flatpak. It proves:
- correct signature/digest success and verify-before-install ordering;
- wrong fingerprint, wrong signer, bad signature, absent checksum, truncation,
tampering, unsupported architecture, and interrupted download refusal;
- known-good target preservation and temporary cleanup;
- exact RPM Fusion, Terra, COPR, Flathub, Claude Code, and Claude Desktop policies;
- no `curl | shell`, unpinned npm global install, moving `latest` lookup, or
unverified root package input remains in the base installer;
- both x86_64 and aarch64 select only their reviewed URL and digest.
The existing `boot`, README, package-list, desktop-first, apps, launcher-search, and
update-command contracts are updated where their old expectations contradict the new
trust boundary.
## Server firewall transaction
### Files and policy
Create `server/scripts/update-firewall` as a standard-library Python executable and
invoke it from the real `setup/scripts/setup-server` stage. It is server-only and does
not reuse the desktop `panama-firewall` helper.
The repository carries LF-terminated canonical CIDR files:
- `server/firewall/cloudflare-v4.cidrs`
- `server/firewall/cloudflare-v6.cidrs`
They begin with the current official ranges published at
`https://www.cloudflare.com/ips-v4` and `https://www.cloudflare.com/ips-v6`.
Each refresh uses a 10-second connection timeout and 30-second total timeout, and
downloads at most 64 KiB per response without sudo. It requires:
- valid UTF-8/ASCII with one value per line;
- no blanks, whitespace, comments, or trailing fields;
- `ipaddress.ip_network(value, strict=True)` success;
- matching file family, canonical string form, no `/0`, no duplicate, and nonempty
IPv4 and IPv6 sets.
The pair is indivisible. One invalid response changes nothing. With an existing valid
Panama policy, refresh failure warns and preserves it. On first setup, the validated
committed pair is the fallback.
### Desired state
Content-addressed ipsets are named:
```text
panama-cf4-<first 12 SHA-256 hex>
panama-cf6-<first 12 SHA-256 hex>
```
The selected public zone receives exactly four Panama-owned rich rules: IPv4 and IPv6
sources for each of `80/tcp` and `443/tcp`. The selected WireGuard zone receives only
`81/tcp`. Panama adds no service-specific port.
Zone selection is stored without shell evaluation in
`${XDG_CONFIG_HOME:-$HOME/.config}/panama/server-firewall.conf`:
```text
public_zone=public
wireguard_zone=wireguard
```
On first run, the public zone is firewalld's default zone. The WireGuard zone is the
unique active zone whose interfaces are all named `wg*`. Zero matches leaves port 81
closed and omits the setting. More than one match, identical public/WireGuard zones,
or a candidate containing a non-WireGuard interface fails before mutation. Explicit
stored zones must still exist and satisfy those invariants.
Before mutation, Panama refuses unrelated services, port ranges, ACCEPT zone targets,
or rich rules that broadly admit 80/443. It prints exact inspection commands. It
removes the exact direct 80/443/81 legacy rules only during the documented migration.
### Recoverable transaction
The updater acquires a nonblocking user-state lock and writes a checked journal to
`${XDG_STATE_HOME:-$HOME/.local/state}/panama/firewall/pending.json` containing only
Panama-owned rules/ipsets, the legacy direct ports, the WireGuard 81 rule, and selected
zones.
It then:
1. creates and fills the new permanent generation ipsets;
2. adds the four new public rules;
3. adds WireGuard 81 when a valid zone exists;
4. removes old Panama rules, exact legacy ports, and unreferenced Panama ipsets;
5. runs `firewall-cmd --check-config`;
6. performs one reload;
7. reads permanent and runtime state back;
8. writes selected-zone configuration and removes the journal only after equivalence.
Old live rules remain active until the single reload. A partial permanent transaction
therefore does not create an outage. Any error restores the snapshot and reloads.
Rollback success still returns nonzero. Rollback failure retains the journal and
prints its path plus the exact retry command. A later run restores a pending journal
before considering new inputs.
`setup-server` attempts `sudo systemctl enable --now firewalld`, then requires both
`is-enabled` and `is-active`. Missing commands or failed postconditions stop before
the updater. The updater itself is never run through sudo; only its narrow systemctl
and firewall-cmd mutations use sudo.
### Stateful contract
`tests/setup/server-firewall-contract` invokes the real `setup-server`. Its stateful
stubs keep separate permanent/runtime JSON and exact argv logs. `firewall-cmd` supports
only the queried zone, service, port, rich-rule, ipset, check-config, and reload
operations. Reload copies permanent to runtime. Exact one-shot failure selectors leave
state unchanged.
Required cases are:
- missing/inactive firewalld that cannot become enabled and active;
- first setup with legacy rules, exact desired ipsets/rules, WireGuard-only 81,
unrelated SSH/manual rules preserved, and one reload;
- every malformed CIDR class and indivisible-pair behavior;
- no WireGuard zone, ambiguous zones, identical zones, and mixed interfaces;
- unrelated broad exposure conflict refusal;
- reload failure with successful rollback and rollback-reload failure with journal;
- identical second run with no mutation/reload and valid two-family refresh;
- interrupted journal recovery before new evaluation.
## Scope ledger amendments
Reconnaissance added findings that the parent ledger did not name. Ownership is:
| Finding | Package |
|---|---|
| Mutable root bootstrap and mutable initial checkout | 2 |
| Unverified RPM Fusion/COPR/Flathub trust roots | 2 |
| Moving Node/pnpm/Bun/Claude/Codex/RustDesk inputs | 2 |
| Mutable npm install during Vicinae extension build | 2 |
| Unauthenticated fetched revision used by `panama update` | 3 |
| Mutable source-app build inputs | 5 |
| Mutable Neovim bootstrap/plugin graph | 5 |
Package 3 must define an authenticated approved revision or signed release manifest
before fetched Panama code reaches `install --upgrade`. Package 5 must pin or refuse
the ChatGPT source application and Neovim bootstrap/plugin graph. Package 3 retains
container image trust, and Package 4 retains the Whisper image/model pins already
assigned by the parent design.
## Documentation
Package 2 updates:
- README bootstrap commands and root-server narrative;
- README/server README firewall policy and port exposure;
- installer comments that currently defend moving or unsigned inputs;
- package/provenance documentation and key-rotation procedure;
- Panama development/operator skills when command behavior changes.
Documentation never claims a real host was cut over or a live reload succeeded.
## Verification
Each implementation plan uses red-green cycles through public interfaces. The package
gate includes:
```bash
bash -n boot install setup/scripts/install-packages setup/scripts/setup-server \
setup/scripts/link-vicinae-scripts setup/lib/artifact-provenance \
tests/setup/root-server-bootstrap-contract \
tests/setup/package-provenance-contract \
tests/setup/server-firewall-contract
python3 -m py_compile server/scripts/update-firewall
tests/setup/root-server-bootstrap-contract
tests/setup/package-provenance-contract
tests/setup/server-firewall-contract
tests/setup/boot-contract
tests/setup/role-contract
tests/setup/readme-contract
tests/setup/package-lists-contract
tests/setup/desktop-first-contract
tests/setup/launcher-search-contract
tests/setup/update-command-contract
./bin/panama test --safe
git diff --check
```
If available, ShellCheck covers every touched shell file. The signed Terra command
also receives one disposable Fedora 44 container smoke test. The container has no host
mounts, host package database, system bus, SSH daemon, firewall access, or production
credentials.
## Completion criteria
Package 2 is complete when:
- no unverified mutable network response is executed by root or while relying on the
installer's sudo keepalive;
- the initial Panama revision and boot digest are checked before handoff;
- every automatic third-party executable input is signature-verified or pinned by a
reviewed immutable SHA-256;
- untrusted Claude Desktop setup is optional and never automatic;
- SSH hardening cannot remove the available login path and rolls back every tested
validation/reload failure;
- server firewall setup establishes the documented Cloudflare/WireGuard policy or
returns nonzero without losing the last known good policy;
- the full hermetic gate passes without live capability grants;
- independent package review has no unresolved Critical or Important finding.
+142 -17
View File
@@ -22,7 +22,7 @@
# * setup-identity and install-hardware are dropped. They exist only to # * setup-identity and install-hardware are dropped. They exist only to
# consume interview answers -- git identity, NVIDIA, Secure Boot, firmware # consume interview answers -- git identity, NVIDIA, Secure Boot, firmware
# -- and every one of those is a first-run decision. # -- and every one of those is a first-run decision.
# * install-packages runs only when the package lists actually changed. # * install-packages runs only when its tracked installation inputs changed.
# * Migrations always run rather than baseline. See the migrations block. # * Migrations always run rather than baseline. See the migrations block.
# #
# Everything else is shared on purpose: the sudo keepalive, the per-stage # Everything else is shared on purpose: the sudo keepalive, the per-stage
@@ -64,12 +64,15 @@ done
source "$PANAMA_PATH/bin/ascii" source "$PANAMA_PATH/bin/ascii"
# ── Have the package lists changed? ────────────────────────────────────────── # ── Have the installation inputs changed? ───────────────────────────────────
# #
# install-packages is the slow stage -- a dnf metadata refresh, a Flathub # install-packages is the slow stage -- a dnf metadata refresh, a Flathub
# round-trip, and a transaction that resolves to "nothing to do" almost every # round-trip, and a transaction that resolves to "nothing to do" almost every
# time. On an upgrade it is worth running only when the lists it reads actually # time. On an upgrade it is worth running only when its package lists or
# changed, so this hashes them and remembers the result. # reviewed installer trust inputs changed, so this hashes them and remembers
# the result. The framed, sorted stream includes top-level package files, the
# package-stage adapter, the provenance helper, and regular provenance files;
# both relative paths and bytes are part of the state.
# #
# A content hash rather than a git range, because Panama is developed in place: # A content hash rather than a git range, because Panama is developed in place:
# a package added to a list and not yet committed must still install. A range # a package added to a list and not yet committed must still install. A range
@@ -85,25 +88,121 @@ source "$PANAMA_PATH/bin/ascii"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
PACKAGES_HASH="$STATE_DIR/packages-hash" PACKAGES_HASH="$STATE_DIR/packages-hash"
hash_packages() { _collect_package_inputs() {
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -exec sha256sum {} + \ local destination="$1" raw="${1}.raw"
| sort | sha256sum | cut -d' ' -f1 [[ -d "$PANAMA_PATH/setup/packages" \
&& ! -L "$PANAMA_PATH/setup/packages" \
&& -d "$PANAMA_PATH/setup/provenance" \
&& ! -L "$PANAMA_PATH/setup/provenance" ]] || return 1
{
printf '%s\0' \
"$PANAMA_PATH/setup/scripts/install-packages" \
"$PANAMA_PATH/setup/lib/artifact-provenance" \
"$PANAMA_PATH/setup/lib/extras-catalog" \
"$PANAMA_PATH/setup/lib/machine-role" || exit 1
# extras/ is deliberately excluded. A symlink or other non-directory
# object at this level is still an input error, not something discovery may
# silently omit.
find "$PANAMA_PATH/setup/packages" -mindepth 1 -maxdepth 1 \
! -type d -print0 || exit 1
find "$PANAMA_PATH/setup/provenance" -mindepth 1 \
! -type d -print0 || exit 1
} >"$raw" || return 1
LC_ALL=C sort -z "$raw" >"$destination"
} }
_write_package_manifest() {
local inputs="$1" destination="$2" file relative digest
: >"$destination" || return 1
while IFS= read -r -d '' file; do
[[ -f "$file" && ! -L "$file" && -r "$file" ]] || return 1
relative="${file#"$PANAMA_PATH"/}"
[[ "$relative" != "$file" ]] || return 1
digest="$(sha256sum -- "$file" | awk '{ print $1 }')" || return 1
[[ "$digest" =~ ^[0-9a-f]{64}$ ]] || return 1
printf '%s\0%s\0' "$relative" "$digest" >>"$destination" || return 1
done <"$inputs"
}
# Read every input twice from the same enumerated set. A file or path that
# changes while the snapshot is built cannot produce a receipt.
hash_packages() (
local work="" inputs_before inputs_after manifest_before manifest_after
trap '[[ -z "$work" ]] || rm -rf -- "$work"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
work="$(mktemp -u -d -t panama-packages-hash.XXXXXX)" || exit 1
if ! mkdir -m 700 -- "$work"; then
work=""
exit 1
fi
inputs_before="$work/inputs-before"
inputs_after="$work/inputs-after"
manifest_before="$work/manifest-before"
manifest_after="$work/manifest-after"
_collect_package_inputs "$inputs_before" || exit 1
_write_package_manifest "$inputs_before" "$manifest_before" || exit 1
_collect_package_inputs "$inputs_after" || exit 1
cmp -s -- "$inputs_before" "$inputs_after" || exit 1
_write_package_manifest "$inputs_after" "$manifest_after" || exit 1
cmp -s -- "$manifest_before" "$manifest_after" || exit 1
sha256sum -- "$manifest_before" | awk '{ print $1 }'
)
PACKAGE_START_HASH=""
packages_needed() { packages_needed() {
local current_hash recorded_hash
current_hash="$(hash_packages)" || return 2
PACKAGE_START_HASH="$current_hash"
(( FORCE_PACKAGES )) && return 0 (( FORCE_PACKAGES )) && return 0
(( UPGRADE )) || return 0 (( UPGRADE )) || return 0
[[ -r "$PACKAGES_HASH" ]] || return 0 [[ -r "$PACKAGES_HASH" ]] || return 0
[[ "$(hash_packages)" != "$(cat "$PACKAGES_HASH")" ]] recorded_hash="$(cat "$PACKAGES_HASH")" || return 2
[[ "$current_hash" != "$recorded_hash" ]]
} }
# Written only after the stage succeeds, mirroring the rule panama-migrate # Written only after the stage succeeds, mirroring the rule panama-migrate
# documents for its markers: a step that did not complete has not happened, and # documents for its markers: a step that did not complete has not happened, and
# recording it as done hides it forever. # recording it as done hides it forever.
record_packages_hash() { record_packages_hash() (
local expected_hash="$1" current_hash temporary_hash=""
trap '[[ -z "$temporary_hash" ]] || rm -f -- "$temporary_hash"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
[[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]] || return 1
current_hash="$(hash_packages)" || return 1
[[ "$current_hash" == "$expected_hash" ]] || return 1
mkdir -p "$STATE_DIR" mkdir -p "$STATE_DIR"
hash_packages >"$PACKAGES_HASH" temporary_hash="$(mktemp -u "$STATE_DIR/.packages-hash.XXXXXX")" || return 1
} umask 077
if ! (set -o noclobber; : >"$temporary_hash") 2>/dev/null; then
temporary_hash=""
return 1
fi
printf '%s\n' "$expected_hash" >"$temporary_hash" || return 1
mv -f -- "$temporary_hash" "$PACKAGES_HASH" || return 1
temporary_hash=""
)
# Repository trust is checked before the installer can reach its bootstrap DNF.
# Status 78 is reserved for a trust-root failure and is propagated unchanged so
# no later stage, especially install-hardware, can invoke DNF with that repo.
TERRA_TRUST_FAILURE_STATUS=78
trust_preflight="$PANAMA_PATH/setup/scripts/install-packages"
if [[ ! -x "$trust_preflight" ]]; then
printf 'install: package repository trust preflight is unavailable\n' >&2
exit "$TERRA_TRUST_FAILURE_STATUS"
fi
if "$trust_preflight" --trust-preflight; then
:
else
trust_status=$?
printf 'install: package repository trust preflight failed\n' >&2
exit "$trust_status"
fi
# ── The interview ──────────────────────────────────────────────────────────── # ── The interview ────────────────────────────────────────────────────────────
# #
@@ -137,7 +236,8 @@ if (( ! UPGRADE )); then
fi fi
if (( ${#bootstrap[@]} > 0 )); then if (( ${#bootstrap[@]} > 0 )); then
echo "Installing what the setup questions are built on: ${bootstrap[*]}" echo "Installing what the setup questions are built on: ${bootstrap[*]}"
sudo dnf install -y "${bootstrap[@]}" >/dev/null || { sudo dnf install -y --repo=fedora --repo=updates \
--from-repo=fedora,updates "${bootstrap[@]}" >/dev/null || {
echo "Could not install ${bootstrap[*]}, so the setup questions cannot be asked." >&2 echo "Could not install ${bootstrap[*]}, so the setup questions cannot be asked." >&2
exit 1 exit 1
} }
@@ -200,7 +300,12 @@ gsettings set org.gnome.desktop.session idle-delay 0 2>/dev/null || true
# is unset and each stage takes the empty-answer path it already documents -- # is unset and each stage takes the empty-answer path it already documents --
# which is why this is a flag rather than a rewrite of seven stage scripts. # which is why this is a flag rather than a rewrite of seven stage scripts.
if (( ! UPGRADE )); then if (( ! UPGRADE )); then
PANAMA_ANSWERS="$(mktemp -t panama-answers.XXXXXX)" PANAMA_ANSWERS="$(mktemp -u -t panama-answers.XXXXXX)" || exit 1
umask 077
if ! (set -o noclobber; : >"$PANAMA_ANSWERS") 2>/dev/null; then
PANAMA_ANSWERS=""
exit 1
fi
export PANAMA_ANSWERS export PANAMA_ANSWERS
if ! PANAMA_ROLE_PRESET="$ROLE_PRESET" "$PANAMA_PATH/setup/scripts/interview"; then if ! PANAMA_ROLE_PRESET="$ROLE_PRESET" "$PANAMA_PATH/setup/scripts/interview"; then
@@ -289,16 +394,36 @@ for stage in "${STAGES[@]}"; do
script="$PANAMA_PATH/setup/scripts/$stage" script="$PANAMA_PATH/setup/scripts/$stage"
[[ -x "$script" ]] || continue [[ -x "$script" ]] || continue
printf '\n=== %s ===\n' "$stage" printf '\n=== %s ===\n' "$stage"
if [[ "$stage" == install-packages ]] && ! packages_needed; then if [[ "$stage" == install-packages ]]; then
package_start_hash=""
package_state_status=0
packages_needed || package_state_status=$?
package_start_hash="$PACKAGE_START_HASH"
if (( package_state_status == 1 )); then
echo "The package lists have not changed since the last run; skipping." echo "The package lists have not changed since the last run; skipping."
echo "Run with --packages to install them anyway." echo "Run with --packages to install them anyway."
continue continue
elif (( package_state_status != 0 )); then
failed+=("$stage")
printf '!!! %s could not read its tracked installation inputs\n' "$stage" >&2
continue
fi
fi
if "$script"; then
if [[ "$stage" == install-packages ]]; then
if ! record_packages_hash "$package_start_hash"; then
failed+=("$stage")
printf '!!! %s could not record its tracked installation inputs\n' "$stage" >&2
fi
fi
else
stage_status=$?
if [[ "$stage" == install-packages && "$stage_status" -eq "$TERRA_TRUST_FAILURE_STATUS" ]]; then
printf '!!! %s stopped on an untrusted package repository\n' "$stage" >&2
exit "$stage_status"
fi fi
if ! "$script"; then
failed+=("$stage") failed+=("$stage")
printf '!!! %s failed\n' "$stage" >&2 printf '!!! %s failed\n' "$stage" >&2
elif [[ "$stage" == install-packages ]]; then
record_packages_hash
fi fi
done done
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env bash
# Reviewed installer inputs are data, never shell code. Consumers source this
# library, load the data through its parser, and verify each fetched artifact
# before any privileged operation can see it.
declare -gA INSTALLER_PROVENANCE=()
_primary_key_fingerprints() (
local home="" gpg_output
trap '[[ -z "$home" ]] || rm -rf -- "$home"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
home="$(mktemp -u -d -t panama-gpg.XXXXXX)" || exit 1
if ! mkdir -m 700 -- "$home"; then
home=""
exit 1
fi
gpg_output="$(GNUPGHOME="$home" gpg --batch --with-colons \
--import-options show-only --import "$1" 2>/dev/null)" || exit 1
awk -F: '$1 == "pub" { primary = 1; next } primary && $1 == "fpr" { print $10; primary = 0 }' \
<<<"$gpg_output"
)
key_fingerprint_matches() {
local file="$1" expected="$2" output
local -a primary_fingerprints=()
output="$(_primary_key_fingerprints "$file")" || return 1
[[ -n "$output" ]] || return 1
mapfile -t primary_fingerprints <<<"$output"
[[ ${#primary_fingerprints[@]} -eq 1 && "${primary_fingerprints[0]}" == "$expected" ]]
}
_key_has_one_primary() {
local output
local -a primary_fingerprints=()
output="$(_primary_key_fingerprints "$1")" || return 1
[[ -n "$output" ]] || return 1
mapfile -t primary_fingerprints <<<"$output"
[[ ${#primary_fingerprints[@]} -eq 1 ]]
}
verify_detached_signature() {
local key="$1" signature="$2" content="$3" home
_key_has_one_primary "$key" || return 1
(
home=""
trap '[[ -z "$home" ]] || rm -rf -- "$home"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
home="$(mktemp -u -d -t panama-gpg.XXXXXX)" || exit 1
if ! mkdir -m 700 -- "$home"; then
home=""
exit 1
fi
GNUPGHOME="$home" gpg --batch --quiet --import "$key" >/dev/null 2>&1 \
&& GNUPGHOME="$home" gpg --batch --verify "$signature" "$content" >/dev/null 2>&1
)
}
download_sha256() {
local url="$1" expected="$2" max_bytes="$3" destination="$4" actual directory filename
[[ "$expected" =~ ^[0-9a-f]{64}$ ]] || return 1
actual="$expected"
directory="$(dirname -- "$destination")"
filename="$(basename -- "$destination")"
(
local part=""
trap '[[ -z "$part" ]] || rm -f -- "$part"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
[[ "$max_bytes" =~ ^[1-9][0-9]*$ ]] || exit 1
[[ -n "$destination" && -d "$directory" ]] || exit 1
umask 077
part="$(mktemp -u "$directory/.${filename}.part.XXXXXX")" || exit 1
if ! (set -o noclobber; : >"$part") 2>/dev/null; then
part=""
exit 1
fi
curl --fail --location --connect-timeout 10 --max-time 600 \
--max-filesize "$max_bytes" --output "$part" "$url" \
|| exit 1
[[ -f "$part" ]] || exit 1
[[ "$(stat -c %s "$part")" -le "$max_bytes" ]] || exit 1
[[ "$(sha256sum "$part" | awk '{ print $1 }')" == "$actual" ]] || exit 1
mv -f -- "$part" "$destination"
)
}
rpm_signature_matches() (
local package="$1" key="$2" expected="$3" home="" db output
trap '[[ -z "$home" ]] || rm -rf -- "$home"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
key_fingerprint_matches "$key" "$expected" || exit 1
home="$(mktemp -u -d -t panama-rpm-signature.XXXXXX)" || exit 1
if ! mkdir -m 700 -- "$home"; then
home=""
exit 1
fi
db="$home/rpmdb"
mkdir -m 700 "$db" || exit 1
rpmkeys --dbpath "$db" --import "$key" >/dev/null 2>&1 || exit 1
output="$(rpmkeys --dbpath "$db" --checksig --verbose "$package" 2>&1)" \
|| exit 1
grep -Eqi 'OpenPGP.*signature.*: OK' <<<"$output"
)
load_installer_provenance() {
local file="$1" line name value required
local -A parsed=() allowed=() required_values=()
for name in \
BUN_VERSION BUN_X86_64_URL BUN_X86_64_SHA256 BUN_X86_64_BINARY_SHA256 BUN_X86_64_MAX_BYTES \
BUN_AARCH64_URL BUN_AARCH64_SHA256 BUN_AARCH64_BINARY_SHA256 BUN_AARCH64_MAX_BYTES \
NODE_VERSION NODE_X86_64_URL NODE_X86_64_SHA256 NODE_X86_64_BINARY_SHA256 NODE_X86_64_MAX_BYTES \
NODE_AARCH64_URL NODE_AARCH64_SHA256 NODE_AARCH64_BINARY_SHA256 NODE_AARCH64_MAX_BYTES \
CODEX_VERSION CODEX_X86_64_URL CODEX_X86_64_SHA256 CODEX_X86_64_BINARY_SHA256 CODEX_X86_64_MAX_BYTES \
CODEX_AARCH64_URL CODEX_AARCH64_SHA256 CODEX_AARCH64_BINARY_SHA256 CODEX_AARCH64_MAX_BYTES \
RUSTDESK_VERSION RUSTDESK_X86_64_URL RUSTDESK_X86_64_SHA256 RUSTDESK_X86_64_MAX_BYTES \
FEDORA_RELEASE RPMFUSION_FREE_RELEASE_URL RPMFUSION_FREE_RELEASE_MAX_BYTES \
RPMFUSION_NONFREE_RELEASE_URL RPMFUSION_NONFREE_RELEASE_MAX_BYTES TERRA_BASEURL \
HYPRLAND_COPR_BASEURL FLATHUB_DESCRIPTOR_URL FLATHUB_DESCRIPTOR_MAX_BYTES \
CLAUDE_CODE_BASEURL CLAUDE_DESKTOP_BASEURL TERRA_FINGERPRINT CLAUDE_CODE_FINGERPRINT \
BUN_FINGERPRINT RPMFUSION_FREE_FINGERPRINT RPMFUSION_NONFREE_FINGERPRINT \
HYPRLAND_COPR_FINGERPRINT FLATHUB_FINGERPRINT CLAUDE_DESKTOP_FINGERPRINT; do
allowed["$name"]=1
required_values["$name"]=1
done
[[ -r "$file" ]] || return 1
while IFS= read -r line || [[ -n "$line" ]]; do
[[ "$line" == *=* && "${line#*=}" != *=* ]] || return 1
IFS='=' read -r name value <<<"$line"
[[ -n "${allowed[$name]:-}" && -n "$value" && -z "${parsed[$name]:-}" ]] || return 1
[[ "$value" != *'$('* && "$value" != *'`'* ]] || return 1
parsed["$name"]="$value"
done < "$file"
for required in "${!required_values[@]}"; do
[[ -n "${parsed[$required]:-}" ]] || return 1
done
INSTALLER_PROVENANCE=()
for name in "${!parsed[@]}"; do
INSTALLER_PROVENANCE["$name"]="${parsed[$name]}"
done
}
+191
View File
@@ -0,0 +1,191 @@
# Installer provenance
Reviewed on 2026-08-27. `installers.conf` is data, not a shell fragment: the
installer reads it only with `load_installer_provenance`. Its versions, URLs,
SHA-256 values, byte limits, and complete primary fingerprints are reviewed
inputs. No installer path may substitute a `latest` URL or execute fetched
content before the applicable verification succeeds.
## Key retrieval and verification
Each command below was run in a private temporary directory on 2026-08-27.
The resulting armored public key is vendored under `keys/`; each output was
checked with the listed complete primary fingerprint before it was committed.
The verification commands use Panama's status-preserving helper: it captures
GPG's output only after GPG succeeds, then requires exactly one primary key.
```bash
source setup/lib/artifact-provenance
key_fingerprint_matches KEY.asc EXPECTED_COMPLETE_PRIMARY_FINGERPRINT
```
| Key | Source URL | Expected primary fingerprint | Verification command |
| --- | --- | --- | --- |
| Terra 44 | `https://repos.fyralabs.com/terra44/key.asc` | `AE09157A4DE88B497EA1D5D300CDAB43DE226D6F` | `key_fingerprint_matches terra44.asc AE09157A4DE88B497EA1D5D300CDAB43DE226D6F` |
| Anthropic Claude Code | `https://downloads.claude.ai/keys/claude-code.asc` | `31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE` | `key_fingerprint_matches claude-code.asc 31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE` |
| Bun releases | `https://keys.openpgp.org/vks/v1/by-fingerprint/F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` | `F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` | `key_fingerprint_matches bun.asc F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` |
| RPM Fusion free | `https://download1.rpmfusion.org/free/fedora/RPM-GPG-KEY-rpmfusion-free-fedora-2020` | `E9A491A3DE247814E7E067EAE06F8ECDD651FF2E` | `key_fingerprint_matches rpmfusion-free.asc E9A491A3DE247814E7E067EAE06F8ECDD651FF2E` |
| RPM Fusion nonfree | `https://download1.rpmfusion.org/nonfree/fedora/RPM-GPG-KEY-rpmfusion-nonfree-fedora-2020` | `79BDB88F9BBF73910FD4095B6A2AF96194843C65` | `key_fingerprint_matches rpmfusion-nonfree.asc 79BDB88F9BBF73910FD4095B6A2AF96194843C65` |
| lionheartp/Hyprland COPR | `https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/pubkey.gpg` | `97E23476C89635135407C7D5E9BA41342C4B2995` | `key_fingerprint_matches hyprland-copr.asc 97E23476C89635135407C7D5E9BA41342C4B2995` |
| Flathub | `https://flathub.org/repo/flathub.flatpakrepo` | `6E5C05D979C76DAF93C081354184DD4D907A7CAE` | `key_fingerprint_matches flathub.asc 6E5C05D979C76DAF93C081354184DD4D907A7CAE` |
| Claude Desktop Extra | `https://patrickjaja.github.io/claude-desktop-extra/gpg-key.asc` | `825A7D15D78BABE45646D5DF382409F597908867` | `key_fingerprint_matches claude-desktop.asc 825A7D15D78BABE45646D5DF382409F597908867` |
The retrieval command for every direct key was:
```bash
curl --fail --location --connect-timeout 10 --max-time 60 --output KEY.asc SOURCE_URL
```
## Reviewed publisher records and artifact locations
The key sources above are supported by these publisher-controlled records:
- `https://github.com/terrapkg/packages/blob/frawhide/README.md`
- `https://github.com/terrapkg/packages/discussions/7736`
- `https://code.claude.com/docs/en/setup`
- `https://github.com/oven-sh/bun/releases/tag/bun-v1.4.0`
- `https://github.com/oven-sh/bun/blob/main/dockerhub/distroless/Dockerfile`
- `https://github.com/nodejs/node/releases/tag/v24.20.0`
- `https://github.com/nodejs/node/blob/main/README.md`
- `https://github.com/openai/codex/releases/tag/rust-v0.150.1`
- `https://github.com/openai/codex/blob/main/.github/workflows/rust-release.yml`
- `https://github.com/rustdesk/rustdesk/releases/tag/1.4.9`
The reviewed immutable artifact and repository URLs are:
- `https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-x64.zip`
- `https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-aarch64.zip`
- `https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-x64.tar.xz`
- `https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-arm64.tar.xz`
- `https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-x86_64-unknown-linux-musl.tar.gz`
- `https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-aarch64-unknown-linux-musl.tar.gz`
- `https://github.com/rustdesk/rustdesk/releases/download/1.4.9/rustdesk-1.4.9-0.x86_64.rpm`
- `https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-44.noarch.rpm`
- `https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm`
- `https://repos.fyralabs.com/terra44`
- `https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/`
- `https://downloads.claude.ai/claude-code/rpm/stable`
- `https://patrickjaja.github.io/claude-desktop-extra/rpm/`
Artifact digests were checked with `sha256sum ARTIFACT` against the lowercase
64-hex values in `installers.conf`; all runtime downloads also enforce the
recorded byte limit before atomic replacement.
The `*_BINARY_SHA256` values are the SHA-256 digests of the exact executable
bytes selected from each verified runtime archive. They were derived without
executing the artifacts: `tar -xO` selected Node's `bin/node` and Codex's
`bin/codex`, while `unzip -p` selected Bun's `bun`. Runtime version directories
record both the reviewed archive digest and this reviewed installed-byte digest
in `.panama-provenance`; later runs require the receipt and re-hash the installed
binary before treating an existing directory as trusted.
## Panama bootstrap
The public desktop and server commands use the same committed `boot` blob:
```text
commit 0d1841cf86975d52485c94c0caa5d1b67e5c54a1
SHA-256 fd8dcd70d1266da109972df346e3d85d1037f6f6e1942b0157064412ab892372
URL https://git.gbrown.org/gib/Panama/raw/commit/0d1841cf86975d52485c94c0caa5d1b67e5c54a1/boot
```
The digest was derived from the committed blob with:
```bash
git show 0d1841cf86975d52485c94c0caa5d1b67e5c54a1:boot | sha256sum
```
That implementation commit contains only `boot` and its boot/root-server
contracts. The following documentation commit records the pin and does not
contribute bytes to it. Rotate the bootstrap pin only through the same two-step
sequence: commit the boot implementation first, derive its committed blob
digest, then update the command and this ledger in a second commit.
## Rotation policy
Do not replace a key on an automated update. A key rotation is a reviewed
repository change: obtain the new key from the publisher record, independently
confirm its complete primary fingerprint, and update every independent pin site
in one review:
- the armored key under `setup/provenance/keys/`;
- its fingerprint in `setup/provenance/installers.conf`;
- the matching `_require_policy_value` literal in
`setup/scripts/install-packages`;
- independent fingerprint expectations and command-log fixtures in
`tests/setup/package-provenance-contract`;
- this retrieval and evidence ledger at `setup/provenance/README.md`.
Until all sites agree, verification fails closed and preserves any known-good
destination. Add or update a focused contract whenever verification behavior
changes.
## Historical container-only Terra 44 signed-bootstrap proof
On 2026-08-27, a single disposable rootless Podman container validated Terra's
then-reviewed signed bootstrap without changing the host package database,
host keyring, or host repository files. This is retained historical publisher
evidence; Panama's runtime installer no longer installs `terra-release`.
Podman reported `rootless=true`, `runtime=crun`, and a
user graph root. The fresh image was
`registry.fedoraproject.org/fedora@sha256:62f199d1eb34170a7bb2277485676d89c0e91aae4086151c4043062cce51c77c`
(`sha256:87d8a4a90c0457689db68624cac1026fb2201cbdc1e99cc5455a8f8876118498`).
The container (`5fc8fa42bb85afb3b57b336ca29b58a32fad50d460583329a4e910cc29fb4d2d`)
had no mounts and was removed automatically after `podman stop`.
Before copying the only host file admitted to the container,
`keys/terra44.asc`, this status-preserving host check accepted the complete
primary fingerprint `AE09157A4DE88B497EA1D5D300CDAB43DE226D6F`:
```bash
source setup/lib/artifact-provenance
key_fingerprint_matches setup/provenance/keys/terra44.asc \
AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
```
Its SHA-256 was
`3febbaadfa43509c22edc440d58c970705e502cdd0f347a75d5005562fd6b3d8`.
The container commands were:
```bash
podman create --name panama-terra-proof-20260827 --rm --pull=always \
registry.fedoraproject.org/fedora:44 sleep infinity
podman cp setup/provenance/keys/terra44.asc \
panama-terra-proof-20260827:/tmp/terra44.asc
podman start panama-terra-proof-20260827
podman exec panama-terra-proof-20260827 /bin/bash -lc '
sha256sum /tmp/terra44.asc
install -Dm0644 /tmp/terra44.asc /etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama
dnf install -y --repofrompath terra,https://repos.fyralabs.com/terra44 \
--setopt=terra.pkg_gpgcheck=1 \
--setopt=terra.repo_gpgcheck=1 \
--setopt=terra.gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama \
terra-release
'
```
The retained command output records the copied key's SHA-256 and DNF's
successful `terra-release-44-9.noarch` transaction. The command itself pins the
temporary Terra base URL and local staged key and enables package and repository
signature checks. It does not include a separate post-install fingerprint or
effective-repository query, so this ledger makes no independent post-check
claim. Production publishes the reviewed root-staged key/repository pair
directly and commits it only after the effective-repository post-check
succeeds; failure restores the prior pair. Publisher-only package transactions
use a fresh command-line repository identity, the reviewed base URL, and a
newly fingerprint-verified private root key snapshot.
Although the command runner returned after 30 seconds while DNF was still
loading metadata, Podman's retained event log records the exact command's
completion. This read-only recovery command was run after container removal:
```bash
podman events --since '2026-08-27T10:55:00-04:00' --until '2026-08-27T11:02:00-04:00' \
--filter container=5fc8fa42bb85afb3b57b336ca29b58a32fad50d460583329a4e910cc29fb4d2d \
--format json
```
The first `exec` event, at `timeNano=1787842633591543881`, is the documented
key-install and DNF command. Its matching first `exec_died` event, at
`timeNano=1787842671276003275`, records `ContainerExitCode:0`. No retry or
second container was used, and no stronger post-check evidence is retained.
+50
View File
@@ -0,0 +1,50 @@
BUN_VERSION=1.4.0
BUN_X86_64_URL=https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-x64.zip
BUN_X86_64_SHA256=2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452
BUN_X86_64_BINARY_SHA256=33d56b070be6a9e3da0ab013038b43d1645d0534ca811ecdba4472599117eb4b
BUN_X86_64_MAX_BYTES=67108864
BUN_AARCH64_URL=https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-aarch64.zip
BUN_AARCH64_SHA256=4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e
BUN_AARCH64_BINARY_SHA256=086c4121c8738a8e0f5ed730e8a461bc3973b4444e372ddb77aef9a747fa2ae9
BUN_AARCH64_MAX_BYTES=67108864
NODE_VERSION=24.20.0
NODE_X86_64_URL=https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-x64.tar.xz
NODE_X86_64_SHA256=2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2
NODE_X86_64_BINARY_SHA256=89af8424dd53e560b1933f87ba650d8bf57c83ca5a04600eefb31f416aabbae7
NODE_X86_64_MAX_BYTES=67108864
NODE_AARCH64_URL=https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-arm64.tar.xz
NODE_AARCH64_SHA256=5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7
NODE_AARCH64_BINARY_SHA256=23a5637c2470fde09fcc1acc77c1b92e04e3d7e3e6e80ff7df6f5831958d1477
NODE_AARCH64_MAX_BYTES=67108864
CODEX_VERSION=0.150.1
CODEX_X86_64_URL=https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-x86_64-unknown-linux-musl.tar.gz
CODEX_X86_64_SHA256=00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17
CODEX_X86_64_BINARY_SHA256=abf1bb1643a79f73aa78ee627e111e02d4f8c98f25813a0cf6ce277709664386
CODEX_X86_64_MAX_BYTES=134217728
CODEX_AARCH64_URL=https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-aarch64-unknown-linux-musl.tar.gz
CODEX_AARCH64_SHA256=1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639
CODEX_AARCH64_BINARY_SHA256=7a49aabe11fd95a1c968d79e16b5f1b17c3219002c5f7129d467f415f8460feb
CODEX_AARCH64_MAX_BYTES=134217728
RUSTDESK_VERSION=1.4.9
RUSTDESK_X86_64_URL=https://github.com/rustdesk/rustdesk/releases/download/1.4.9/rustdesk-1.4.9-0.x86_64.rpm
RUSTDESK_X86_64_SHA256=eb1b053ac5b2f774f2271f7fbbfd2ea475899f7a55135c5e172bc54b9388f108
RUSTDESK_X86_64_MAX_BYTES=134217728
FEDORA_RELEASE=44
RPMFUSION_FREE_RELEASE_URL=https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-44.noarch.rpm
RPMFUSION_FREE_RELEASE_MAX_BYTES=4194304
RPMFUSION_NONFREE_RELEASE_URL=https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm
RPMFUSION_NONFREE_RELEASE_MAX_BYTES=4194304
TERRA_BASEURL=https://repos.fyralabs.com/terra44
HYPRLAND_COPR_BASEURL=https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/
FLATHUB_DESCRIPTOR_URL=https://flathub.org/repo/flathub.flatpakrepo
FLATHUB_DESCRIPTOR_MAX_BYTES=1048576
CLAUDE_CODE_BASEURL=https://downloads.claude.ai/claude-code/rpm/stable
CLAUDE_DESKTOP_BASEURL=https://patrickjaja.github.io/claude-desktop-extra/rpm/
TERRA_FINGERPRINT=AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
CLAUDE_CODE_FINGERPRINT=31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE
BUN_FINGERPRINT=F3DCC08A8572C0749B3E18888EAB4D40A7B22B59
RPMFUSION_FREE_FINGERPRINT=E9A491A3DE247814E7E067EAE06F8ECDD651FF2E
RPMFUSION_NONFREE_FINGERPRINT=79BDB88F9BBF73910FD4095B6A2AF96194843C65
HYPRLAND_COPR_FINGERPRINT=97E23476C89635135407C7D5E9BA41342C4B2995
FLATHUB_FINGERPRINT=6E5C05D979C76DAF93C081354184DD4D907A7CAE
CLAUDE_DESKTOP_FINGERPRINT=825A7D15D78BABE45646D5DF382409F597908867
+15
View File
@@ -0,0 +1,15 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: F3DC C08A 8572 C074 9B3E 1888 8EAB 4D40 A7B2 2B59
Comment: Robobun <[email protected]>
xjMEY9GQFhYJKwYBBAHaRw8BAQdAkppAqaXl0RkROz6NfdvlwYd2UuUVfHLk2NNY
IzEdnT/NGVJvYm9idW4gPHJvYm9idW5Ab3Zlbi5zaD7CkwQTFgoAOxYhBPPcwIqF
csB0mz4YiI6rTUCnsitZBQJj0ZAWAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4H
AheAAAoJEI6rTUCnsitZ96UBAMvwCLD6Ud1RZkpvvnGUU+idHt2hcNmYU2d0XxDI
HQ0rAQCA9VFOtjZefQkfhDzgIgnEEdSFXaMiyY+D+LP8awNMCs44BGPRkBYSCisG
AQQBl1UBBQEBB0BpfePYSOEx8PihYkNXjlK5YT89CGHjEK5etleaB9i6OwMBCAfC
eAQYFgoAIBYhBPPcwIqFcsB0mz4YiI6rTUCnsitZBQJj0ZAWAhsMAAoJEI6rTUCn
sitZvhAA/j4SQxOCLheRG86A2181WAP4qLS1qxSw+fCf28DgiPfbAQCL0kcel+M9
qbRIlMnwn6TwlQgN9w1qqlSnA9CbKXT9Aw==
=zRMz
-----END PGP PUBLIC KEY BLOCK-----
+29
View File
@@ -0,0 +1,29 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGnK73ABEACnbytJXkjweYrwIr0aLEFRlH+C0nF44KxFc7gQmJ6PjSPMGZAD
dxZcaixU7zZl8WxEpVO0wLmIH8cf2zGOdyuZg1Yaugk1vHb2b8WBhAGCQJdPgB8W
XquedepEYtk56uP/gCoTjJDUZluEGBHnlnuujSJ4orxEdhSykEoAUfJZGEILPpMd
bphFt/Sn+Eb/TxM5jpKPdwnv8AShNF/1mZU1fWTQq9tRKJUakZj04gdaDFElQXak
CtTij+GT6yoYCARSHwGO+PC/Pr6q4tc+D7LRjxSBvUWDoFSmlqb/PJ1hj9D/7I2O
e4XXniAPWMR56KvxHlzOzrNQdJujbJdSkCwh1ZijkSd3y8ayW5WYUTGdRab99NUw
agzlabe/VVF6kzJ0Scn5q3PihB2Y9Bwo0CKnkYk7a7KT77EWv0Kkq+VHmOtqX3a2
hhX+b6a6ve9rzJ1qZYGj+obv/C3Sx1LzUjAfqVy7RJDf2uAoP5t2g8u/TkSpUxhM
VEjZBkSxYZhMyzQM6t8IgkUfnSrIPTHixbDWARZ4beMOBjxyPZK1nP7OOrNR3TkK
JtwLMQAabURCDnL0PjS0iwBTU4jtumBD1XSULyWuoTvMljrpQr1nV1oDyOt0OLqa
KA2McWtd9PdXhC8y2EIg7TmrTlJLfHYbdmkiCYj4J49Q8HWkN/6WE+RTUwARAQAB
tD5BbnRocm9waWMgQ2xhdWRlIENvZGUgUmVsZWFzZSBTaWduaW5nIDxzZWN1cml0
eUBhbnRocm9waWMuY29tPokCUQQTAQoAOxYhBDHd3iTd+rZ59C170rqpKf8afsrO
BQJpyu9wAhsPBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJELqpKf8afsrO
l5IP/2I8X1dFy5xYczWB/coIxGjuzS/V6ByZGZZEJsbr04pmuHiFUykJqPGWGQ6q
U0YF5iEwvEkaagS5m7DzhSEf3FM3Cgafax/6d70tar9Vr1D+w6uPfxetu7u/WYJp
aolIsdh5fTrBh9zSM1Njl8FM8wG8CwZQjS33Oa7d8cwRkgdUWbt6LXgz+cTQNuBn
BgW6Ks7oZFI25dfu0ojDR+aDFJg4+4wZoyDLPvJz1SIrJ5WFGs67zsx9SfS3yZnf
XKmBe+f0dUy+GJ2nFZrXFf99+c0dPEHYO8DCeAHZizjkFrdYtUHdDU0YDYEGkLJa
bE+pgcpkHf5EvsZzHsyDbl95W/eh7pcXMbwkN+W4CBYUE9X4uHhqzWaC5yAVRWUA
1BJ9V4LjZfHPLEJt0I3TxzXiEg9/BVeaTYq9RjaxIFo9Nfk158HqJY6SA5jslBlx
Gv/No8u+xVcze2UJyGVfEIUfm92+0UAIkny3+5cuVV0ICzJxXlXj0CnLM9Lt50wE
p3suVwuBEviCbZ08eAH1Ht8gbBdSsiOkIU8CX3v/scwHHx5q0+NBL6xLrQObg13a
tRXBlKObfElkPN3lTUbUnJOW4U8uSjH8VRP+AujKWMDFe7x0zCs+iYY1mTOvbrTS
9n3CmZUmbynZ+E/QWNENpW/pDNZdWFy43PASmML5FHu4m9Sn
=oqMI
-----END PGP PUBLIC KEY BLOCK-----
+56
View File
@@ -0,0 +1,56 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGmVzwkBEAClNOGTBwWpfRynhbaMotnz7VFu1mJPG3zt2ntzbvLiaaJWSeRC
2/Ax86xndydREZVboelTaKGaLQZ+/kYdmT5GARXQ/C1D8ORwoGLxbfgwNSDWpqUa
yoE25ewXNQTnjiMJ79pr8HaRp/GHhKAkn0TLIlBCUn44hqDCH4Lr5ykJeiGoLKD7
TCOsjmnWWUvhKkdSiug4HARQ2z/7rAtDhmErGy+l4Ut5SBcvyHdoth5PPtVDyQXp
YVgxLoL4ExS6A7dL/WMfOeXPi2z6fM7p9jB2Hhwi3adejfhm73A4lwLs5N51dKud
uzBPS3PNsGo8GmrFXe5EMhPTAbFMhFyVRBRFPFrQFWqZ5FVJOo5S3ehoryIGvjgc
mur92WLJvpMvPFWWO/Qq00jkJf9ID14JPAaF44O8CV8+JsJVjVSfqKaTag5JxYJ0
NgGK/GREFXyZsbvs7V6C+qdEV1V4shBAPyn+h6MKpNDfjUUXnSIrR1ubU5k9g3mo
JFX51gsnAJnZ6ZSIKx9MdxFhtEOrNubaXgmmm6x/4OTIN3yOyl4YvVjg4F3a4jzI
hyYlh8L+1oZBYIXS6+i/ZQG6h0jBElLDYPvfBxSmpK3RBW0J7c6qhlPbS91TO6Td
kF9TIBoQdUB9V2qpfmmHVxmn4pf9Mro+MT4+kXFwMO1nV7nsJzmht6wTOQARAQAB
tFNDbGF1ZGUgRGVza3RvcCBMaW51eCAoY2xhdWRlLWRlc2t0b3AtYmluIHJlcG8g
c2lnbmluZyBrZXkpIDxwYXRyaWNramFqYWFAZ21haWwuY29tPokCUgQTAQoAPAMb
LwQFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQSCWn0V14ur5FZG1d84JAn1l5CI
ZwUCamj6GwIZAQAKCRA4JAn1l5CIZ9dRD/kBQX6M7xBTU7GvVWfOxDY7sFKUP6YV
axJQNYVZ645UOJY895HuW/kreasHKlZO+0RlX4j0rb3PFey/NHakwmIN3Ru0d4dn
2UHTHXxAidfselMZ1avGpbyjLpKvtcjr+/8w59/+bvrA5/vpJpGDP2o8GT8415cv
827fd2fEoMYMrI538HNwTzXURoErJkiL6ZGOPJqxLkEY5JLk4bL7ChiSkOgCBZ6H
R/wciLK/flRkNivXxbTfDBjLEeJAPQkTyE/pK/itCX+dzcoOXmIXnz97J8gJPDyq
LhTUdYhPZpCsIDJSIOYV2MnvjSaxGnIohawtirF+HXnkc3ICx7EcfRG0UVR8mZZm
ULxBYjKrgxFyOpNExRp860HOnM/VNK8La2l+yiC7GsyJ28bSHbYj7GAoT8NsufAf
TFhiseaJmTOOKVc0AQ9yfkhIuV3nzjf+fIvwxU/CAo1EKzyeqVW/mkcvDdRl+C0x
0+80pyne8XGJCdBYsZ1GYhrDbFdZcgWYL++Z0yXmH8EUrQfM1C1UQ+cVbTItnVqR
Ifv4sjRCKSPw588Tex34XhyFp1IaJ3x9Q+dXGAfyanop6KUULIRbqmCss787l04V
deLpnZ+jly0iFj6LeqOX15q+XgZaAiJ7WRLIDdW66ezCYmrkl5gS0RPZdg2/YBNa
LStIMzZrPQS90bREQ2xhdWRlIERlc2t0b3AgTGludXggPGNsYXVkZS1kZXNrdG9w
LWxpbnV4QHVzZXJzLm5vcmVwbHkuZ2l0aHViLmNvbT6JAlAEMAEKADoWIQSCWn0V
14ur5FZG1d84JAn1l5CIZwUCamj60RwdAGFkZGVkIGEgcmVhbCBlbWFpbCBhZGRy
ZXNzAAoJEDgkCfWXkIhn9KoP/148Cl+w5Q9S0rQOWfg3zfhk4mRYBvb+3lHzVhpH
XzyilZfi2rf0xnmcDQJDIaYbi14HOqDiWmNDDUlARU6/R7lPuQMh0C3zM2MY/VpW
MyxZVHW2xOWVWw/X8WfhULMnTiTHhVU1bWzrK4NnjqUNUore71PqP9RUWeky/24o
ZyAoZrmOvm9dZw30/69oPEbckGeEWZ96FY7aoyafl+xrQS22qJoST3aHzLWBYnUD
HPjgcHohIfU050zoyd5oktEW98i6NfJdA9di8zVLUVSCH22Z9fX3gVIPf8bF9u4d
HQrHRAGjCAma1V77mEaeGcWrTGspruqtz6YrWCRH1hVW5ybJ+nsNdCPxEErpkiFm
PBJPwlEdpNyPlWHoawNMYOazM64v1P0g9Db5ykRAFwCf0GxgOb/uca8LIAXJ1ieU
2V4nF71zfvMbiVMYJn7aWjf1xIt1mhWRhBbVN33yq3se/Fn2aPxyqELKIgKrgUV0
40Cd2YOS6jC5+8xkcUZm9sPzTw7NT/RJTl1XZGMKRmiNEWMSkaxgKa7WcelveQ5H
I3fIYIfiFjShYA4Dtyn++hT2NnpRduaWaofB7IKPgDte4mtZVJH8EEyAv8N3B689
1+WV1l1RwDxLkkq/poqL6mXf/wA+fiCsevf7VDKlDCPICZd2m35bZPvZK/DUQT9r
nkJ+iQJPBBMBCgA5FiEEglp9FdeLq+RWRtXfOCQJ9ZeQiGcFAmmVzwkDGy8EBQsJ
CAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEDgkCfWXkIhnyfgP/1WWtm3m+cYSBcMH
CdN0t/JlYT//eJ5720X0ZWSqwO+Xr4SiTQ5iMSIcCtV0DhJuuHp3suaHRtNzSHXW
JmP6zVATfLgJUFrFZkoV/2hl9pL0n0+RgSMr4dRPwHiQqFmqA1rUtSN6uH1aX72n
t1THCRzL0LIPPN6L1CVpGSxx3ZwdR8dmlwQbhKw4CMRWXbcZPH7/ali69wnc0qqN
7Gk9O3zoNIJqyDRJM5wHigIrajJGEEF2zEkdxWBvpwtLOpEMqNgrDL/xpbJH2eT8
R7dD+rtYc3E9UnmXP1HkycnvcCPJUfo5c2vNWV9fymesbfYmgEVfIES7sqJapKoN
PQ+ih69qF47g/lTaxXyrKNH2VCnbTydgWKckGN4S66dJChaSNgFm1Cyk9i1POkoW
scuK/D/5PzWVlunSrDaOcxhmHkvukkv+2TENyQc+8l4XIrIYTA0/cdln5LOL4Kxc
tAn4Xv2UKGRQP0ovZ7wm7b033lNQ4EzK+oLXeFHU0IggfWjQHeDXMc3nG8B/Xppe
1IGmWY6Ya3Nnzxhd2fgUkHhdS8lonfai7+WXDYfFHEf5pT+YFDB7IaVmQO5N8wVr
BL4aYI3Q6mxCk1xZ2j9NN1s+8n/LjMxOndMIOe5ZJLeOHzoMx3ThvFA+cqm8aqPj
yD+bFESqa8u1U4uNNayal0aR+Hyb
=rBee
-----END PGP PUBLIC KEY BLOCK-----
+64
View File
@@ -0,0 +1,64 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFlD2sABEADsiUZUOYBg1UdDaWkEdJYkTSZD68214m8Q1fbrP5AptaUfCl8K
YKFMNoAJRBXn9FbE6q6VBzghHXj/rSnA8WPnkbaEWR7xltOqzB1yHpCQ1l8xSfH5
N02DMUBSRtD/rOYsBKbaJcOgW0K21sX+BecMY/AI2yADvCJEjhVKrjR9yfRX+NQE
hDcbXUFRGt9ZT+TI5yT4xcwbvvTu7aFUR/dH7+wjrQ7lzoGlZGFFrQXSs2WI0WaY
HWDeCwymtohXryF8lcWQkhH8UhfNJVBJFgCY8Q6UHkZG0FxMu8xnIDBMjBmSZKwK
Qn0nwzwM2afskZEnmNPYDI8nuNsSZBZSAw+ThhkdCZHZZRwzmjzyRuLLVFpOj3Xr
yXwZcSefNMPDkZAuWWzPYjxS80cm2hG1WfqrG0Gl8+iX69cbQchb7gbEb0RtqNsk
To9DDmO0bNKNnMbzmIJ3/rTbSahKSwtewklqSP/01o0WKZiy+n/RAkUKOFBprjJt
WOZkc8SPXV/rnoS2dWsJWQZhuPPtv3tefdDiEyp7ePrfgfKxuHpZES0IZRiFI4J/
nAUP5bix+srcIxOVqAam68CbAlPvWTivRUMRVbKjJiGXIOJ78wAMjqPg3QIC0GQ0
EPAWwAOzzpdgbnG7TCQetaVV8rSYCuirlPYN+bJIwBtkOC9SWLoPMVZTwQARAQAB
tC5GbGF0aHViIFJlcG8gU2lnbmluZyBLZXkgPGZsYXRodWJAZmxhdGh1Yi5vcmc+
iQJUBBMBCAA+FiEEblwF2XnHba+TwIE1QYTdTZB6fK4FAllD2sACGwMFCRLMAwAF
CwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQQYTdTZB6fK5RJQ/+Ptd4sWxaiAW9
1FFk7+wmYOkEe1NY2UDNJjEEz34PNP/1RoxveHDt43kYJQ23OWaPJuZAbu+fWtjR
YcMBzOsMCaFcRSHFiDIC9aTp4ux/mo+IEeyarYt/oyKb5t5lta6xaAqg7rwt65jW
5/aQjnS4h7eFZ+dAKta7Y/fljNrOznUp81/SMcx4QA5G2Pw0hs4Xrxg59oONOTFG
BgA6FF8WQghrpR7SnEe0FSEOVsAjwQ13Cfkfa7b70omXSWp7GWfUzgBKyoWxKTqz
MN3RQHjjhPJcsQnrqH5enUu4Pcb2LcMFpzimHnUgb9ft72DP5wxfzHGAWOUiUXHb
Aekfq5iFks8cha/RST6wkxG3Rf44Zn09aOxh1btMcGL+5xb1G0BuCQnA0fP/kDYI
Pwh9z22EqwRQOspIcvGeLVkFeIfubxpcMdOfQqQnZtHMCabV5Q/Rk9K1ZGc8M2hl
g8gHbXMFch2xJ0Wu72eXbA/UY5MskEeBgawTQnQOK/vNm7t0AJMpWK26Qg6178Um
RghmeZDj9uNRc3EI1nSbgvmGlpDmCxaAGqaGL1zW4KPW5yN25/qeqXcgCvUjZLI9
PNq3Kvizp1lUrbx7heRiSoazCucvHQ1VHUzcPVLUKKTkoTP8okThnRRRsBcZ1+jI
4yMWIDLOCT7IW3FePr+3xyuy5eEo9a25Ag0EWUPa7AEQALT/CmSyZ8LWlRYQZKYw
417p7Z2hxqd6TjwkwM3IQ1irumkWcTZBZIbBgrSOg6CcXD2oWydCQHWi9qaxhuhE
l2bJL5LskmBcMxVdQeD0LLHd8QUnbnnIby8ocvWN1alPfvJFjCUTrmD22U1ycOzR
w2lIe4kiQONbOZtdWrVImQQSndjFlisitbmlWHvHm2lOOYy8+GJB7YffVV193hmn
BSJffCy4bvkuLxsI+n1DhOzc7MPV3z6HGk4HiEcF0yyt9tCYhpsxHFdBoq2h771H
fAcS0s98EVAqYMFnf9em+4cnYpdI6mhIfS1FQiKl6DBAYA8tT3ggla00DurPo0Jw
X/zN+PaO5h/6O9aCZwV7G6rbkgMuqMergXaf8oP38gr0z+MqWnkfM63Bodq68GP4
l4hd02BoFBbDf38TMuGQB14+twJMdfbAxo2MbgluvQgfwHfZ2ca6gyEY+9s/YD1g
ugLjV+S6CB51WkFNe1z4tAPgJZNxUcKCbeaHNbthl8Hks/pY9RCEseX/EdfzF18e
pbSjJMPh4DPQXbUoFwmyuYcoBOPmvZHNl9hK7B/1RP8w1ZrXk8qdupC0SNbafX72
70B7lMMVImzZetGsM9ypXJ6llhp3FwW09iseNyGJGPsr/dvTMGDXqOPfU/9SAS1L
STY4K9PbRtdrBE318YX8mIk5ABEBAAGJBHIEGAEIACYWIQRuXAXZecdtr5PAgTVB
hN1NkHp8rgUCWUPa7AIbAgUJEswDAAJACRBBhN1NkHp8rsF0IAQZAQgAHRYhBFSm
zd2JGfsgQgDYrFYnAunj7X7oBQJZQ9rsAAoJEFYnAunj7X7oR6AP/0KYmiAFeqx1
4Z43/6s2gt3VhxlSd8bmcVV7oJFbMhdHBIeWBp2BvsUf00I0Zl14ZkwCKfLwbbOR
C2eIxvzJ+QWjGfPhDmS4XUSmhlXxWnYEveSek5Tde+fmu6lqKM8CHg5BNx4GWIX/
vdLi1wWJZyhrUwwICAxkuhKxuP2Z1An48930eslTD2GGcjByc27+9cIZjHKa07I/
aLffo04V+oMT9/tgzoquzgpVV4jwekADo2MJjhkkPveSNI420bgT+Q7Fi1l0X1aF
UniBvQMsaBa27PngWm6xE2ZYvh7nWCdd5g0c0eLIHxWwzV1lZ4Ryx4ITO/VL25It
ECcjhTRdYa64sA62MYSaB0x3eR+SihpgP3wSNPFu3MJo6FKTFdi4CBAEmpWHFW7F
cRmd+cQXeFrHLN3iNVWryy0HK/CUEJmiZEmpNiXecl4vPIIuyF0zgSCztQtKoMr+
injpmQGC/rF/ELBVZTUSLNB350S0Ztvw0FKWDAJSxFmoxt3xycqvvt47rxTrhi78
nkk6jATKGyvP55sO+K7Q7Wh0DXA69hvPrYW2eu8jGCdVGxi6HX7L1qcfEd0378S7
1dZ3g9o6KKl1OsDWWQ6MJ6FGBZedl/ibRfs8p5+sbCX3lQSjEFy3rx6n0rUrXx8U
2qb+RCLzJlmC5MNBOTDJwHPcX6gKsUcXZrEQALmRHoo3SrewO41RCr+5nUlqiqV3
AohBMhnQbGzyHf2+drutIaoh7Rj80XRh2bkkuPLwlNPf+bTXwNVGse4bej7B3oV6
Ae1N7lTNVF4Qh+1OowtGjmfJPWo0z1s6HFJVxoIof9z58Msvgao0zrKGqaMWaNQ6
LUeC9g9Aj/9Uqjbo8X54aLiYs8Z1WNc06jKP+gv8AWLtv6CR+l2kLez1YMDucjm7
v6iuCMVAmZdmxhg5I/X2+OM3vBsqPDdQpr2TPDLX3rCrSBiS0gOQ6DwN5N5QeTkx
mY/7QO8bgLo/Wzu1iilH4vMKW6LBKCaRx5UEJxKpL4wkgITsYKneIt3NTHo5EOua
Yk+y2+Dvt6EQFiuMsdbfUjs3seIHsghX/cbPJa4YUqZAL8C4OtVHaijwGo0ymt9M
WvS9yNKMyT0JhN2/BdeOVWrHk7wXXJn/ZjpXilicXKPx4udCF76meE+6N2u/T+RY
Z7fP1QMEtNZNmYDOfA6sViuPDfQSHLNbauJBo/n1sRYAsL5mcG22UDchJrlKvmK3
EOADCQg+myrm8006LltubNB4wWNzHDJ0Ls2JGzQZCd/xGyVmUiidCBUrD537Wdkn
OYE4FD7P0cHaM9brKJ/M8LkEH0zUlo73bY4XagbnCqve6PvQb5G2Z55qhWphd6f4
B6DGed86zJEa/RhS
=ez56
-----END PGP PUBLIC KEY BLOCK-----
+19
View File
@@ -0,0 +1,19 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBGkTU2cBCACrUmwefenkABmwEy6FURUcZBXs/TPc1270i0ogs1zz3EtLeHhX
mcOsEFSbPhS6ehDKtxie8/kKlPIEpgXPiL5LDA0cFuUjO5uIawfrGwajVlEeBRjn
4oMqInTgxX2fhj6oJpb0IrqspzfZWP7cJQbtefxUNkaANm9ZqIwsLWU0jKYA5sg1
xFyvDryg5gkrtrhGNCM+S9Mn5yB94Aly/aVWIKE+QSiZi8OiGbcYj8z5Hi08ZKvh
UmzvaOtbt8mI06DgCB/YyHzhwceLCpO0rT9PeYi5iCD438xyLbbrxhY55eFmw8gV
cid2uhifWUrc8Gh8bpiO8quMmfOjSAMBTpBnABEBAAG0Rmxpb25oZWFydHBfSHlw
cmxhbmQgKE5vbmUpIDxsaW9uaGVhcnRwI0h5cHJsYW5kQGNvcHIuZmVkb3JhaG9z
dGVkLm9yZz6JAVgEEwEIAEIWIQSX4jR2yJY1E1QHx9XpukE0LEsplQUCaRNTZwMb
LwQFCQlmAYAFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQ6bpBNCxLKZXV
Mgf+O5fYThKr1CVNrPl1j+D7Qm5JtAq0C2KBklaN4IPt3kYeEGmrzH/yBs1O/Crq
pmctbZm92aITz2Z8m/14/lq4v43rG6j9Z/i5SyitQEVFJjR4GRbcheOwEBXRJ4YV
qMZkw/xJSztPoN180Zxew4CFlAUWs6URxFBroid9MyrNpnoob0ENhfFas5lHFFVp
uNleyuznhu7iqjP7K/jV86JvSyPyFDpwVyBr+bMZ1mkHc16KyDf/qSn4RhpjlCC9
qy8SeZsUZPqIURI5/VrFyb44LaBC1CA2RVcuY7NAyEMRZLTJfi+8cuHL8Kvcvz1l
JUpD4thuhsq2VkZtMptBrbxmPw==
=4uMv
-----END PGP PUBLIC KEY BLOCK-----
+29
View File
@@ -0,0 +1,29 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBF2tu8EBEADnI6bmlE7ebLuYSBKJavk7gwX8L2S0lDwtmAFmNcxQ/tAhh5Gx
2RKEneou12pSxav8MvbKOr4IpJLLmuoQMLYkbQRHovgVfDYdtvK9T8tZH51ACtnC
KKr9SucnKhWpDk3/n/djV0I2qSesE6QcJVrh66bT/8nbyIFbbiYLOgE88YAX5Wdj
TkgmYXJ54l1MP/3N64pFlmk6myYCrLh7cibFYLZOW2Xwfq6Go6HOpGn9Cazb+T6m
LALkVPERu2QkcUhMqy/slD5tFFb7DW1gkwnYiu5PKwThW7laZgmw2yAgDV+JccdK
D9ZHALmy9GyQ1ZjDptpa5BObE5vazbuAbSndoIqwaMxCrlqhIYdmqz4m/HJ9BaC0
mRSkT6N9SqytZXFhu5/Ld6+/Ol3b+q28bnV64qQrDH6hgnrRdqCQpm8g7tZFuk5X
JsB/A+EfI2kE6YXqWaGdEx0XcqOv97n6sRZNweOHX3vSM0eLwmM2dpgc7RvMfcqr
73ylZ9CnWVUD6cl+wE8SnGnVVqYau2spZFzKVAcfi/Zwvh6wM7/83XC2mkIHmoFR
OY5aDWFhoFZFgiHHnmDv6kACNmSHb/oYRkvwQ+JhAQu4I9CYw1sxaUDjwtt7a+4I
mBZM8WuvAVLkqnF+MJetiL15/W834HjCNITV03t9593T6Z1Dxpfv4hy7YwARAQAB
tFVSUE0gRnVzaW9uIGZyZWUgcmVwb3NpdG9yeSBmb3IgRmVkb3JhICgyMDIwKSA8
cnBtZnVzaW9uLWJ1aWxkc3lzQGxpc3RzLnJwbWZ1c2lvbi5vcmc+iQJFBBMBCAAv
FiEE6aSRo94keBTn4Gfq4G+OzdZR/y4FAl2tu8ECGwMECwkIBwMVCAoCHgECF4AA
CgkQ4G+OzdZR/y4ZQhAAmF5A4XC9ymd94BFwsbbpCnx2YlfmsZwT1QzBu9njjkH7
MC4THknYe2B/muE5dPu3NseZMzue1Ou4KbMz4wq82731prLRu+iHAxAxJ1qd8whA
QGuRJAg8+YEXKhpwpD/8P/xJo9IRmPxPM+6mQVTlASv34CEIGff1vJr40tNiU53P
PZq9SWD3/uG84PQRmGXetfF2K3NkXqzkvQSM68JZiYR2+wMkoO9f72B7LTBrfkwy
RcFPA7kj65pysB+l2wez03Dh/MyA3LTusd9M6FGiSOUVpQZ+NUFipIisS3vh/Bgp
zMsj1NSsMLjUDcX8stR8GfVgTxSgWwHTNl75XwTZpJOKMoj97kh9zzLwBhZ1W+xo
8s2W7YqVnOUl8rPm7ZbOefGkamNg8bhqcyNIEbHqR5QZVzDBT2AxVcB6jsxSHf5b
sb+KEJff4g6E4fWPA/IYdtJ7DItbVXnkAjqD7ADUh7Xq7pOgfC/4Cledf27x73m+
sdBvKsEBrroAsX/v4z46mQApszkfjTUAXwj2lUT+ujoktJHXqR71jbY0+8JX6Fyw
6ZW0emxR++bt9ksLcsNmjOQP9TmQpi2CW4Z+Ol2tlwtlnKAo6ecx4aacHKg+FYuQ
HTJRq6E6GpCPn1avf1v797RM+3zzw9TYkadfVLIQQ4HYbYzienOgGGporclrtrQ=
=oOVZ
-----END PGP PUBLIC KEY BLOCK-----
@@ -0,0 +1,30 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBF2tvGQBEAC5Q2ePLZZafOkFhYHpGZdRRBCcCd+aiLATofFV8+FjPuPLL/3R
7fx9RRukL+XKs6K9houj/oYVHmBY7II1mgeRzZHo6KygnM9ph3RKqQDse4TR9+VX
rctsBRikNc7GViSoiPHLRAJeTrlwYRjPHYfF64nFtcPYfPIlGZkEG8mrHbTjkh36
NAlqb3XC0cOSsKQV5f4Wn8fAUepYUkTxA74sVHLSDcBRj3fGfizkiHohy4OjNPij
1VVvfUQXIGYwEDnrd3JF5c2o6B4MfH7h1aN+xG7GJTRswgjQtYUayUOySD5mdZ9u
lUNfPrIAvwyTnc1IvoJUGlf8wSqz8NmjTHykUU+f6Dldb4JKNavnYaVlmDH4HfK+
FVdAD/1pG/6HL94clf/g8LR3sQ0KU/UZJKbDA81n1X04OREfqdjr81U84iyKyb8S
+5nwYuJvxoe+wHg+iHAK0CXYel6V1GR51yka8+sETXyEjGvXksPMQDVPGIDzDfPr
QVijtL3/1Pgkuz1ZvvXmuxD94uV2rBvjKl1NFSWNXId2J+vI5omllGHR3qskOHFa
My9IQkbV4sMoycW/fP5xbwGhVi5q5Gjo7h6J7TIzyMf4gl6PJTp0AFhOZAMA/dXY
nLDnw+qz+iq0B3I14JSLvgCH/uSUEMl5970+COK7wmPTU7I3Hq6PMbzvqQARAQAB
tFhSUE0gRnVzaW9uIG5vbmZyZWUgcmVwb3NpdG9yeSBmb3IgRmVkb3JhICgyMDIw
KSA8cnBtZnVzaW9uLWJ1aWxkc3lzQGxpc3RzLnJwbWZ1c2lvbi5vcmc+iQJFBBMB
CAAvFiEEeb24j5u/c5EP1Albair5YZSEPGUFAl2tvGQCGwMECwkIBwMVCAoCHgEC
F4AACgkQair5YZSEPGW0Ig/+NJf5+KzbRNuFvvGURQI7SYmYtFXkrW4n6rLPWeIV
UHvd/ko74aMVds7hTWeC0cLpjRMSPuwp9xjqb6NvQaqcUK4IwHzlXocait2HzSl+
h2jI3/wSQXqNkvNrgD3rkYZZZ/x7EBBTSTRUpFPq3yHA/BBXbZNEvFsXOmFAy5y+
E5iYnfyjYKHWd0ZwIliWWtK+V5TU54WqHqKF5J2iIDgANkLXiyqx6+LJ6Ng0YfCQ
fO7IMfwtgUt34AfrHWnq0S9BW0hmtPvcYjTtveQKCeGfdMcpRRJsOrvaDDKo1Wmr
IcvGO2VwiF9i19ppghXOSy7q51wTlEqtj3PWYhmJYcRq8Jr1SqjGx73QhUPtsF67
g3vjNEm8PE7pj7vg52BJlzkx6yU+hH5ZNBRM5ll4ZjiX+X7EzKa9so83uszuwoQA
mScTwyyQDNeflnUwiSgZc7PEv1i0BYIHVK7VjmamhOWZRHaaYFCc//gcmu10TJLn
ZCGF2ZDkAdUT6EoWBsT/QCgYSFggrjH9lgKqC5ON8+F5DO1RQe84irgz9jjE9+62
kgQgWZ6F2RZm5/R28DHdAetji50XbnmXgAk/u9u2Hw2bVVJfJ0WpEVcPvA1L86SE
8i8p1fmzljwRazZAksk5Zh2QfaM0jlMYHWbKpbXQcX19Uerm7D9IkciZvDAmgBYV
S6Y=
=rOqq
-----END PGP PUBLIC KEY BLOCK-----
+17
View File
@@ -0,0 +1,17 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GopenPGP 2.8.1
Comment: https://gopenpgp.org
xjMEaYpzVxYJKwYBBAHaRw8BAQdAb3DsTkfuyHxBUTJh9KY5K9Zmd3HHNlr3nr7j
DqIfJHbNIFRlcnJhIDQ0IDxzZWN1cml0eUBmeXJhbGFicy5jb20+wr8EExYIAHEF
gmmKc1cDCwkHCZAAzatD3iJtbzUUAAAAAAAcABBzYWx0QG5vdGF0aW9ucy5vcGVu
cGdwanMub3Jn6H9BnkxjSlvFTWI4j1lWagIVCAMWAAICGQECmwMCHgEWIQSuCRV6
TeiLSX6h1dMAzatD3iJtbwAAkxUA/AhjnPTnXX4U50jtWrE8/33CXkR/kMvp8y2m
jR9jrEzPAQCEA1Jx8eBbBo7RySbF1D8AoYGGvdy5Igmsz7/FCq4DAM44BGmKc1cS
CisGAQQBl1UBBQEBB0BKc0gRkEY9/IuABq32DNPxZN0AQo41geDGywcbV47JSQMB
CgnCrgQYFggAYAWCaYpzVwmQAM2rQ94ibW81FAAAAAAAHAAQc2FsdEBub3RhdGlv
bnMub3BlbnBncGpzLm9yZ8nm3oDFHJ/SnE3gwb0ZR04CmwwWIQSuCRV6TeiLSX6h
1dMAzatD3iJtbwAAjYoA/2P25j8wdGP/TdF4mhNiN/6cBNL08/wmqDTJooYy2GP+
AQCJp+Vj5nfTYUO7+6WvES4cFaaZJhY8CsjJwx6k8xhsCA==
=KXwo
-----END PGP PUBLIC KEY BLOCK-----
+6 -1
View File
@@ -53,7 +53,12 @@ if [[ "${PANAMA_NVIDIA:-no}" == yes ]]; then
warn "Secure Boot question, or disable Secure Boot first." warn "Secure Boot question, or disable Secure Boot first."
else else
log "Installing the NVIDIA driver" log "Installing the NVIDIA driver"
if sudo dnf install -y akmod-nvidia xorg-x11-drv-nvidia-cuda; then if sudo dnf install -y \
--repo=fedora --repo=updates \
--repo=rpmfusion-free --repo=rpmfusion-free-updates \
--repo=rpmfusion-nonfree --repo=rpmfusion-nonfree-updates \
--from-repo=rpmfusion-nonfree,rpmfusion-nonfree-updates \
akmod-nvidia xorg-x11-drv-nvidia-cuda; then
# nouveau has to be out of the way before the kernel would otherwise # nouveau has to be out of the way before the kernel would otherwise
# bind it, which is why these are kernel arguments and not a modprobe # bind it, which is why these are kernel arguments and not a modprobe
# drop-in. modeset=1 is what makes the Wayland session work at all. # drop-in. modeset=1 is what makes the Wayland session work at all.
File diff suppressed because it is too large Load Diff
+92 -5
View File
@@ -6,6 +6,75 @@
set -euo pipefail set -euo pipefail
_collect_vicinae_inputs() {
local extension="$1" output="$2"
[[ -d "$extension" && ! -L "$extension" \
&& -f "$extension/package.json" && ! -L "$extension/package.json" \
&& -f "$extension/package-lock.json" && ! -L "$extension/package-lock.json" ]] \
|| return 1
# Everything authored below the extension affects its build. npm's
# dependency tree is the sole exception and is reproduced from the lock.
find "$extension" -mindepth 1 \
\( -path "$extension/node_modules" -prune \) -o \
! -type d -print0 >"$output" || return 1
LC_ALL=C sort -z -o "$output" "$output" || return 1
}
_write_vicinae_manifest() {
local extension="$1" inputs="$2" output="$3"
local input relative digest
: >"$output" || return 1
while IFS= read -r -d '' input; do
[[ -f "$input" && ! -L "$input" && -r "$input" ]] || return 1
relative="${input#"$extension"/}"
[[ "$relative" != "$input" && -n "$relative" ]] || return 1
digest="$(sha256sum -- "$input" | awk '{ print $1 }')" || return 1
[[ "$digest" =~ ^[0-9a-f]{64}$ ]] || return 1
printf '%s\0%s\0' "$relative" "$digest" >>"$output" || return 1
done <"$inputs"
}
_vicinae_extension_digest() (
local extension="${1%/}" work=""
trap '[[ -z "$work" ]] || rm -rf -- "$work"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
work="$(mktemp -u -d -t panama-vicinae-digest.XXXXXX)" || exit 1
if ! mkdir -m 700 -- "$work"; then
work=""
exit 1
fi
_collect_vicinae_inputs "$extension" "$work/inputs.before" || exit 1
_write_vicinae_manifest \
"$extension" "$work/inputs.before" "$work/manifest.before" || exit 1
_collect_vicinae_inputs "$extension" "$work/inputs.after" || exit 1
_write_vicinae_manifest \
"$extension" "$work/inputs.after" "$work/manifest.after" || exit 1
cmp -s -- "$work/inputs.before" "$work/inputs.after" || exit 1
cmp -s -- "$work/manifest.before" "$work/manifest.after" || exit 1
sha256sum -- "$work/manifest.before" | awk '{ print $1 }'
)
_record_vicinae_digest() (
local built="$1" digest="$2" receipt temporary=""
trap '[[ -z "$temporary" ]] || rm -f -- "$temporary"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
[[ -d "$built" && ! -L "$built" ]] || exit 1
receipt="$built/.panama-source-sha256"
temporary="$(mktemp -u "$built/.panama-source-sha256.XXXXXX")" || exit 1
umask 077
if ! (set -o noclobber; : >"$temporary") 2>/dev/null; then
temporary=""
exit 1
fi
printf '%s\n' "$digest" >"$temporary" || exit 1
mv -f -- "$temporary" "$receipt" || exit 1
temporary=""
)
panama_path="${PANAMA_PATH:-$HOME/.local/share/Panama}" panama_path="${PANAMA_PATH:-$HOME/.local/share/Panama}"
vicinae_data_dir="${VICINAE_DATA_DIR:-$HOME/.local/share/vicinae}" vicinae_data_dir="${VICINAE_DATA_DIR:-$HOME/.local/share/vicinae}"
source_dir="$panama_path/config/local/share/vicinae/scripts" source_dir="$panama_path/config/local/share/vicinae/scripts"
@@ -81,18 +150,36 @@ if [[ -d "$extensions_source" ]] && command -v npm >/dev/null 2>&1; then
[[ -f "$extension/package.json" ]] || continue [[ -f "$extension/package.json" ]] || continue
name="$(basename "$extension")" name="$(basename "$extension")"
# Skip a build that would produce what is already there. `npm install` # Skip only when a prior successful build records the digest of both
# alone takes long enough to be worth not repeating on every re-run of # manifests and every source byte. Directory mtimes do not change when
# a stage that is otherwise nearly instant. # an existing source file is edited.
built="$vicinae_data_dir/extensions/$name" built="$vicinae_data_dir/extensions/$name"
if [[ -d "$built" && "$extension/src" -ot "$built" ]]; then receipt="$built/.panama-source-sha256"
if ! source_digest="$(_vicinae_extension_digest "$extension")"; then
printf 'Vicinae extension %s inputs could not be verified; skipping\n' \
"$name" >&2
continue
fi
if [[ -f "$receipt" && ! -L "$receipt" ]] \
&& cmp -s <(printf '%s\n' "$source_digest") "$receipt"; then
printf 'Vicinae extension %s is already built\n' "$name" printf 'Vicinae extension %s is already built\n' "$name"
continue continue
fi fi
printf 'Building Vicinae extension %s\n' "$name" printf 'Building Vicinae extension %s\n' "$name"
if ! (cd "$extension" && npm install --silent >/dev/null 2>&1 && npm run build >/dev/null 2>&1); then if ! (cd "$extension" && npm ci --silent >/dev/null 2>&1 \
&& npm run build >/dev/null 2>&1); then
printf 'Vicinae extension %s did not build; skipping\n' "$name" >&2 printf 'Vicinae extension %s did not build; skipping\n' "$name" >&2
continue
fi
if ! final_digest="$(_vicinae_extension_digest "$extension")" \
|| [[ "$final_digest" != "$source_digest" ]]; then
printf 'Vicinae extension %s changed while building; receipt withheld\n' \
"$name" >&2
continue
fi
if ! _record_vicinae_digest "$built" "$source_digest"; then
printf 'Vicinae extension %s receipt could not be recorded\n' "$name" >&2
fi fi
done done
elif [[ -d "$extensions_source" ]]; then elif [[ -d "$extensions_source" ]]; then
+15 -1
View File
@@ -40,7 +40,21 @@ Other tools that beat raw commands:
- `panama-launch --class '<regex>' -- cmd…` — focus the window if it exists, launch otherwise - `panama-launch --class '<regex>' -- cmd…` — focus the window if it exists, launch otherwise
- `panama-sudo --reason "why" -- cmd…` — root work; load the `panama-sudo` skill first - `panama-sudo --reason "why" -- cmd…` — root work; load the `panama-sudo` skill first
- `panama update` — bring the machine current (pull, repairs, unattended stages); asks nothing - `panama update` — bring the machine current (pull, repairs, unattended stages); asks nothing
- `panama test --safe`verify the desktop's contracts without hijacking the session - `panama test --safe`run only contracts classified as hermetic in
`tests/contracts.manifest`
For a new machine, use the complete verified bootstrap command in `README.md`.
Desktop and server installs use the same commit-addressed `boot` file and
SHA-256; server adds only `--server`. Do not replace that URL with a branch or
pipe its response into Bash. `boot` refuses missing or malformed pins, a boot
digest mismatch, and any dirty, divergent, fetch-failed, or mismatched existing
checkout. Use `panama update` after installation.
A plain `panama test` prompts in a terminal before it starts non-hermetic contracts. Automation
must grant each required capability with a repeatable `--allow`, such as
`panama test --allow live-host updates`. Each contract has a 180-second outer timeout by default,
which a positive `PANAMA_TEST_TIMEOUT_SECONDS` value overrides. Failures print captured stdout
and stderr. Successful stdout stays quiet. Successful stderr is surfaced as a warning.
## Settings ## Settings
+283
View File
@@ -0,0 +1,283 @@
# Contract capability manifest.
#
# Every path collected by `panama test` appears once. `hermetic` contracts use
# only isolated fixtures or source inspection; other entries name the live
# boundary they reach, with the immediately preceding comment stating its effect.
# `privileged` is supported for future contracts but has no current entry.
hermetic tests/hypr/gestures-contract
hermetic tests/hypr/hypr-prefs-contract
hermetic tests/hypr/idle-config-contract
hermetic tests/hypr/idle-defaults-contract
# Reloads and reads the live Hyprland keybind categories from the compositor.
live-compositor tests/hypr/keybind-categories-contract
hermetic tests/hypr/prefs-fallback-contract
hermetic tests/hypr/session-teardown-contract
hermetic tests/hypr/window-rules-contract
hermetic tests/hypr/workspace-rules-contract
hermetic tests/quickshell/accent-controls-contract
hermetic tests/quickshell/accessibility-contract
# Pushes a fixture privacy state into the running shell and opens the activity
# panel over the desktop.
live-compositor,live-desktop tests/quickshell/activity-indicator-contract
# Pushes fixture privacy states and Signal Glass events into the running shell,
# and dismisses the live capsule.
live-desktop tests/quickshell/activity-state-contract
# Reads the host GNOME accent enum through gsettings to verify Panama's accent
# mapping.
live-host tests/quickshell/adwaita-accent-contract
hermetic tests/quickshell/agent-usage-contract
hermetic tests/quickshell/app-library-contract
# Reads the host PipeWire application-volume state through the live Quickshell
# service.
live-host tests/quickshell/application-volume-contract
hermetic tests/quickshell/applications-settings-contract
hermetic tests/quickshell/bar-visibility-contract
hermetic tests/quickshell/battery-contract
hermetic tests/quickshell/bluetooth-discovery-contract
hermetic tests/quickshell/brightness-helper-contract
# Opens the live agenda popover and notification centre, and starts a real focus
# session on the running shell.
live-compositor,live-desktop tests/quickshell/calendar-agenda-contract
# Queries the host calendar helper for real configured sources and upcoming
# events.
live-host tests/quickshell/calendar-agenda-helper-contract
hermetic tests/quickshell/calendar_agenda_bridge_test.py
# Opens and closes the cheatsheet overlay on the running shell.
live-compositor,live-desktop tests/quickshell/cheatsheet-contract
# Reads the host Quickshell networking type metadata to verify the live
# NetworkManager boundary.
live-host tests/quickshell/connectivity-contract
hermetic tests/quickshell/containers-contract
# Maps the Control Center and reads its live compositor layer to verify the
# window is present.
live-compositor,live-desktop tests/quickshell/control-center-contract
# Maps the Control Center service surface and reads the live compositor while
# exercising its IPC services.
live-compositor,live-desktop tests/quickshell/control-center-services-contract
hermetic tests/quickshell/curated-events-policy-contract
hermetic tests/quickshell/declared-assets-contract
hermetic tests/quickshell/declared-dependencies-contract
hermetic tests/quickshell/default-apps-contract
# Reads the host xdg-mime default handlers for the configured application
# families.
live-host tests/quickshell/default-apps-family-contract
# Changes the real default browser through the default-apps helper -- xdg-mime
# and xdg-settings defaults on this machine -- and puts it back.
live-desktop tests/quickshell/default-apps-roles-contract
hermetic tests/quickshell/desktop-style-contract
# Reads the host disk and filesystem snapshot through the production disks
# helper.
live-host tests/quickshell/disks-contract
# Maps the display-arrangement surface and measures its live desktop geometry.
live-desktop tests/quickshell/display-arrangement-contract
hermetic tests/quickshell/display-layout-contract
hermetic tests/quickshell/display-transaction-contract
# Changes the real monitor's mode, position, scale and rotation through the live
# compositor, restoring the display it started from.
live-compositor,live-desktop tests/quickshell/displays-contract
hermetic tests/quickshell/dock-pins-contract
# Reads the live dock window geometry to verify its mapped position.
live-desktop tests/quickshell/dock-position-contract
# Reads Hyprland option descriptions from the live compositor to verify enum
# mappings.
live-compositor tests/quickshell/enum-hypr-map-contract
hermetic tests/quickshell/fingerprint-contract
# Reads the host firewall state through the production firewall helper.
live-host tests/quickshell/firewall-contract
hermetic tests/quickshell/focus-modes-contract
# Starts, pauses, reveals and ends a real focus session, and opens Mission
# Control on the running shell.
live-compositor,live-desktop tests/quickshell/focus-session-contract
# Restarts panama-quickshell.service (or `qs kill`s the shell when unsupervised)
# with a focus session in flight; an interrupted run leaves caffeine latched on.
live-desktop tests/quickshell/focus-session-expiry
# Restarts panama-quickshell.service (or `qs kill`s the shell when unsupervised)
# mid-session to prove a paused focus session survives it.
live-desktop tests/quickshell/focus-session-restart
# Reads the host gaming and graphics state through the production helper.
live-host tests/quickshell/gaming-contract
hermetic tests/quickshell/gnome-handoff-contract
hermetic tests/quickshell/gtk-theme-contract
hermetic tests/quickshell/health-service-contract
# Maps the Health UI test-shell surface to exercise desktop window integration.
live-desktop tests/quickshell/health-ui-contract
hermetic tests/quickshell/home-assistant-config-contract
# Probes the configured Home Assistant endpoint over the network.
network tests/quickshell/home-assistant-helper-contract
hermetic tests/quickshell/home-brightness-slider-contract
hermetic tests/quickshell/home-preferences-contract
hermetic tests/quickshell/home_assistant_bridge_test.py
hermetic tests/quickshell/ipc-targets-contract
hermetic tests/quickshell/kdeconnect-helper-contract
hermetic tests/quickshell/kdeconnect_bridge_test.py
# Reloads the live Hyprland keybind configuration to verify rebinding behavior.
live-compositor tests/quickshell/keybind-rebind-contract
# Reads the live Hyprland bind list to verify every compositor binding is shown.
live-compositor tests/quickshell/keybinds-contract
hermetic tests/quickshell/keyring-helper-contract
hermetic tests/quickshell/layer-margin-contract
hermetic tests/quickshell/lock-screen-helper-contract
hermetic tests/quickshell/lock-screen-service-contract
hermetic tests/quickshell/lock-screen-settings-contract
hermetic tests/quickshell/lock-screen-theme-contract
hermetic tests/quickshell/manual-contract
hermetic tests/quickshell/migrations-contract
# Maps the My Home Settings surface and reads the live compositor during the
# settings flow.
live-compositor,live-desktop tests/quickshell/my-home-settings-contract
hermetic tests/quickshell/network-tools-contract
hermetic tests/quickshell/notification-app-rules-contract
hermetic tests/quickshell/online-accounts-contract
hermetic tests/quickshell/osd-helper-contract
hermetic tests/quickshell/osd-model-contract
hermetic tests/quickshell/osd-ui-contract
hermetic tests/quickshell/overview-keyboard-contract
# Spawns a real kitty window, moves it between your workspaces and into the
# scratchpad, and opens the overview.
live-compositor,live-desktop tests/quickshell/overview-live-actions
# Opens the overview on the running shell and types a search into it.
live-compositor,live-desktop tests/quickshell/overview-search-contract
hermetic tests/quickshell/overview-thumbnail-contract
# Queries the live Quickshell overview-actions IPC surface for window action
# availability.
live-host tests/quickshell/overview-window-actions-contract
hermetic tests/quickshell/palette-contract
hermetic tests/quickshell/panama-action-contract
# Toggles the live caffeine inhibitor and Night Light on the running shell.
live-desktop tests/quickshell/panama-action-ipc-contract
hermetic tests/quickshell/panama-agent-contract
hermetic tests/quickshell/panama-command-install-contract
hermetic tests/quickshell/panama-commands-contract
hermetic tests/quickshell/panama-doctor-contract
hermetic tests/quickshell/per-screen-surface-contract
hermetic tests/quickshell/permissions-contract
hermetic tests/quickshell/phone-messages-contract
# Maps the Phone page test-shell surface and reads the live compositor during
# the page flow.
live-compositor,live-desktop tests/quickshell/phone-page-contract
# Reads the host polkit-agent installation and session service configuration.
live-host tests/quickshell/polkit-agent-contract
hermetic tests/quickshell/polkit-reason-contract
hermetic tests/quickshell/power-page-contract
hermetic tests/quickshell/power-profile-contract
hermetic tests/quickshell/powermenu-contract
hermetic tests/quickshell/preference-schema-contract
# Reads the host printer state through the production printer helper.
live-host tests/quickshell/printers-contract
hermetic tests/quickshell/prism-gallery-contract
hermetic tests/quickshell/privacy-traces-contract
hermetic tests/quickshell/qmldir-registration-contract
# Reads live Hyprland option JSON from the compositor to verify schema shape.
live-compositor tests/quickshell/schema-hypr-shape-contract
# Opens and closes the live overview.
live-compositor,live-desktop tests/quickshell/scratchpad-shelf-contract
# Opens the live screen-intelligence and capture overlays and runs an analysis
# through them.
live-desktop tests/quickshell/screen-intelligence-contract
hermetic tests/quickshell/screen-intelligence-helper-contract
hermetic tests/quickshell/search-routing-contract
hermetic tests/quickshell/secrets-contract
hermetic tests/quickshell/settings-backup-contract
hermetic tests/quickshell/settings-backup-live-contract
hermetic tests/quickshell/settings-buttons-contract
hermetic tests/quickshell/settings-commit-reset-contract
hermetic tests/quickshell/settings-docs-contract
hermetic tests/quickshell/settings-hardcoded-values-contract
# Flips real compositor policy -- gaps, blur, inactive opacity, keyboard layout
# -- on the live compositor and restores it.
live-compositor,live-desktop tests/quickshell/settings-hyprland-write-contract
hermetic tests/quickshell/settings-idiom-contract
hermetic tests/quickshell/settings-jump-contract
hermetic tests/quickshell/settings-nav-contract
hermetic tests/quickshell/settings-ownership-contract
# Maps Settings pages through a test shell and reads the live compositor during
# page routing.
live-compositor,live-desktop tests/quickshell/settings-pages-contract
hermetic tests/quickshell/settings-preferences-contract
hermetic tests/quickshell/settings-search-contract
hermetic tests/quickshell/settings-sidebar-layout-contract
hermetic tests/quickshell/settings-sync-contract
# Applies compositor policy through the production write path against the live
# compositor, and writes preferences to the real settings store (no isolated
# config home).
live-compositor,live-desktop tests/quickshell/settings-system-contract
hermetic tests/quickshell/settings-titlebar-contract
# Opens the real Settings window, routes it between pages, and closes it through
# the compositor.
live-compositor,live-desktop tests/quickshell/settings-window-contract
# Flips every compositor-backed setting to a value it does not hold, one at a
# time, on the live compositor.
live-compositor,live-desktop tests/quickshell/settings-write-sweep-contract
# Reads the host sharing-service snapshot through the production helper.
live-host tests/quickshell/sharing-contract
# Publishes a critical fixture event into the live Signal Glass capsule.
live-compositor,live-desktop tests/quickshell/signal-glass-contract
# Reads the host snapshot state through the production snapshots helper.
live-host tests/quickshell/snapshots-contract
hermetic tests/quickshell/sound-cards-contract
hermetic tests/quickshell/sound-defaults-contract
# Reads the host PipeWire device state through Quickshell's live audio service.
live-host tests/quickshell/sound-page-contract
hermetic tests/quickshell/sound-routing-contract
# Reads the host SSH-agent and key-management availability while keeping test
# keys in a throwaway home.
live-host tests/quickshell/ssh-keys-contract
# Publishes fixture events into the live capsule and toggles Do Not Disturb.
live-desktop tests/quickshell/status-events-contract
# Drives the live window switcher and commits a switch, which moves your focus.
live-compositor,live-desktop tests/quickshell/switcher-contract
hermetic tests/quickshell/theme-catalog-contract
hermetic tests/quickshell/theme-profiles-contract
# Reads the host package-update snapshot through the production updates helper.
live-host tests/quickshell/updates-contract
# Reads the host AccountsService snapshot of local user accounts.
live-host tests/quickshell/user-accounts-contract
hermetic tests/quickshell/video-wallpaper-contract
hermetic tests/quickshell/vpn-contract
hermetic tests/quickshell/wallpaper-policy-contract
hermetic tests/quickshell/wallpaper-service-contract
hermetic tests/quickshell/wallpaper-settings-contract
# Opens and closes the welcome overlay on the running shell.
live-compositor,live-desktop tests/quickshell/welcome-contract
hermetic tests/quickshell/wifi-qr-contract
hermetic tests/quickshell/xkb-presets-contract
hermetic tests/server/compose-secrets-contract
hermetic tests/server/containers-shape-contract
hermetic tests/server/panama-server-contract
hermetic tests/setup/apps-contract
hermetic tests/setup/boot-contract
hermetic tests/setup/contract-manifest-contract
hermetic tests/setup/crash-watch-contract
hermetic tests/setup/desktop-first-contract
hermetic tests/setup/dictation-contract
hermetic tests/setup/dotfile-classification-contract
# Contacts the configured package sources through dnf and Flathub availability
# checks.
network tests/setup/extras-contract
hermetic tests/setup/firefox-chrome-contract
hermetic tests/setup/hardware-contract
# Reads the host hardware predicates after exercising fixture sysfs trees.
live-host tests/setup/hardware-predicates-contract
hermetic tests/setup/hooks-contract
hermetic tests/setup/interview-contract
hermetic tests/setup/launch-or-focus-contract
hermetic tests/setup/launcher-commands-contract
hermetic tests/setup/launcher-search-contract
hermetic tests/setup/lid-contract
hermetic tests/setup/migrations-contract
hermetic tests/setup/nautilus-extensions-contract
hermetic tests/setup/package-lists-contract
# Provenance uses local signed fixtures and stubs every network/package operation.
hermetic tests/setup/package-provenance-contract
hermetic tests/setup/projects-contract
hermetic tests/setup/readme-contract
hermetic tests/setup/role-contract
# Root bootstrap uses a temporary filesystem, PATH adapters, real public-key
# parsing, and a user namespace for the actual-root fixture guard.
hermetic tests/setup/root-server-bootstrap-contract
hermetic tests/setup/skills-contract
hermetic tests/setup/test-runner-contract
hermetic tests/setup/update-command-contract
hermetic tests/setup/user-content-contract
hermetic tests/setup/webapp-contract
-99
View File
@@ -1,99 +0,0 @@
# Contracts that take over the desktop you are sitting in front of.
#
# Most of the suite is hermetic: stub commands on PATH, a throwaway HOME, or a
# Quickshell harness booted with `qs -p` under its own semantic entry file. Those
# are safe to run mid-session and are not listed here.
#
# The ones below are not. Each drives the LIVE shell, compositor or machine --
# it opens overlays over whatever you were doing, restarts the running shell,
# moves your windows, changes your monitor, or rewrites a real default. Run
# during a working session they are, at best, a series of surprises; at worst
# they leave state behind when interrupted.
#
# `panama test --safe` skips exactly this list, which is why every entry's
# comment says what it does to the session rather than what it proves.
# tests/setup/desktop-hijacking-contract checks each path exists and sweeps
# tests/ for hijacking contracts that are missing from here.
#
# One repo-relative path per line.
# Pushes a fixture privacy state into the running shell and opens the activity
# panel over the desktop.
tests/quickshell/activity-indicator-contract
# Pushes fixture privacy states and Signal Glass events into the running shell,
# and dismisses the live capsule.
tests/quickshell/activity-state-contract
# Opens the live agenda popover and the notification centre, and starts a real
# focus session on the running shell.
tests/quickshell/calendar-agenda-contract
# Opens and closes the cheatsheet overlay on the running shell.
tests/quickshell/cheatsheet-contract
# Changes the real default browser through the default-apps helper -- xdg-mime
# and xdg-settings defaults on this machine -- and puts it back.
tests/quickshell/default-apps-roles-contract
# Changes the real monitor's mode, position, scale and rotation through the live
# compositor, restoring the display it started from.
tests/quickshell/displays-contract
# Starts, pauses, reveals and ends a real focus session, and opens Mission
# Control on the running shell.
tests/quickshell/focus-session-contract
# Restarts panama-quickshell.service (or `qs kill`s the shell when unsupervised)
# with a focus session in flight; an interrupted run leaves caffeine latched on.
tests/quickshell/focus-session-expiry
# Restarts panama-quickshell.service (or `qs kill`s the shell when unsupervised)
# mid-session to prove a paused focus session survives it.
tests/quickshell/focus-session-restart
# Spawns a real kitty window, moves it between your workspaces and into the
# scratchpad, and opens the overview.
tests/quickshell/overview-live-actions
# Opens the overview on the running shell and types a search into it.
tests/quickshell/overview-search-contract
# Toggles the live caffeine inhibitor and Night Light on the running shell.
tests/quickshell/panama-action-ipc-contract
# Opens and closes the live overview.
tests/quickshell/scratchpad-shelf-contract
# Opens the live screen-intelligence and capture overlays and runs an analysis
# through them.
tests/quickshell/screen-intelligence-contract
# Flips real compositor policy -- gaps, blur, inactive opacity, keyboard layout
# -- on the live compositor and restores it.
tests/quickshell/settings-hyprland-write-contract
# Applies compositor policy through the production write path against the live
# compositor, and writes preferences to the real settings store (no isolated
# config home).
tests/quickshell/settings-system-contract
# Opens the real Settings window, routes it between pages, and closes it through
# the compositor.
tests/quickshell/settings-window-contract
# Flips every compositor-backed setting to a value it does not hold, one at a
# time, on the live compositor.
tests/quickshell/settings-write-sweep-contract
# Publishes a critical fixture event into the live Signal Glass capsule.
tests/quickshell/signal-glass-contract
# Publishes fixture events into the live capsule and toggles Do Not Disturb.
tests/quickshell/status-events-contract
# Drives the live window switcher and commits a switch, which moves your focus.
tests/quickshell/switcher-contract
# Opens and closes the welcome overlay on the running shell.
tests/quickshell/welcome-contract
+11 -7
View File
@@ -355,13 +355,17 @@ for line in sys.stdin:
STUB STUB
chmod +x "$stub/codex" chmod +x "$stub/codex"
mkdir -p "$work/codex/sessions/2026/08/25" session_day="$(date +%Y/%m/%d)"
cat >"$work/codex/sessions/2026/08/25/session.jsonl" <<'JSONL' session_date="$(date +%Y-%m-%d)"
{"type":"turn_context","payload":{"model":"gpt-5.6-sol"}} session_offset="$(date +%:z)"
{"type":"token_count","timestamp":"2026-08-25T10:00:00Z","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":999999,"output_tokens":999999},"last_token_usage":{"input_tokens":1200,"cached_input_tokens":1000,"output_tokens":300}}}} mkdir -p "$work/codex/sessions/$session_day"
{"type":"token_count","timestamp":"2026-08-25T10:05:00Z","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":1999999,"output_tokens":1999999},"last_token_usage":{"input_tokens":1200,"cached_input_tokens":1000,"output_tokens":300}}}} {
JSONL printf '%s\n' '{"type":"turn_context","payload":{"model":"gpt-5.6-sol"}}'
touch "$work/codex/sessions/2026/08/25/session.jsonl" jq -nc --arg timestamp "${session_date}T10:00:00${session_offset}" \
'{type:"token_count",timestamp:$timestamp,payload:{type:"token_count",info:{total_token_usage:{input_tokens:999999,output_tokens:999999},last_token_usage:{input_tokens:1200,cached_input_tokens:1000,output_tokens:300}}}}'
jq -nc --arg timestamp "${session_date}T10:05:00${session_offset}" \
'{type:"token_count",timestamp:$timestamp,payload:{type:"token_count",info:{total_token_usage:{input_tokens:1999999,output_tokens:1999999},last_token_usage:{input_tokens:1200,cached_input_tokens:1000,output_tokens:300}}}}'
} >"$work/codex/sessions/$session_day/session.jsonl"
codex_record="$( codex_record="$(
PATH="$stub:$PATH" \ PATH="$stub:$PATH" \
@@ -36,7 +36,7 @@ SHELL_WORDS='^(if|then|else|elif|fi|for|while|until|do|done|case|esac|in|functio
# authselect is on the list for the same reason: it manages Fedora's PAM and # authselect is on the list for the same reason: it manages Fedora's PAM and
# nsswitch profiles and arrives with fprintd-pam, realmd and nss-mdns, so the # nsswitch profiles and arrives with fprintd-pam, realmd and nss-mdns, so the
# fingerprint aliases in config/bash can rely on it without declaring it. # fingerprint aliases in config/bash can rely on it without declaring it.
BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|find|xargs|basename|dirname|mkdir|rm|cp|mv|ln|chmod|chown|stat|df|du|date|sleep|env|id|tee|touch|mktemp|readlink|realpath|seq|comm|join|paste|od|file|nl|fold|column|tput|timeout|flock|install|sha256sum|md5sum|base64|nproc|uptime|free|uname|hostname|whoami|ps|pgrep|pkill|kill|killall|lsblk|mount|umount|sudo|su|rpm|dnf|flatpak|git|python3|ss|ip|ls|rfkill|lsof|authselect|setsid|nohup|grub2-mkconfig|sysctl)$' BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|find|xargs|basename|dirname|mkdir|rm|cp|mv|ln|chmod|chown|stat|df|du|date|sleep|env|id|tee|touch|mktemp|readlink|realpath|seq|comm|join|paste|od|file|nl|fold|column|tput|timeout|flock|install|tar|sha256sum|md5sum|base64|nproc|uptime|free|uname|hostname|whoami|ps|pgrep|pkill|kill|killall|lsblk|mount|umount|sudo|su|rpm|dnf|flatpak|git|python3|ss|ip|ls|rfkill|lsof|authselect|setsid|nohup|grub2-mkconfig|sysctl)$'
# bootctl and coredumpctl ship in systemd-udev, which every Fedora install # bootctl and coredumpctl ship in systemd-udev, which every Fedora install
# carries -- it is the udev half of systemd, not an optional tool. Declaring # carries -- it is the udev half of systemd, not an optional tool. Declaring
@@ -45,10 +45,10 @@ BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|fi
SESSION='^(systemctl|busctl|journalctl|loginctl|hostnamectl|localectl|systemd-inhibit|systemd-run|udevadm|bootctl|coredumpctl|gsettings|dconf|dbus-send|dbus-monitor|hyprctl|qs|quickshell|gnf|panama|wl-copy|wl-paste)$' SESSION='^(systemctl|busctl|journalctl|loginctl|hostnamectl|localectl|systemd-inhibit|systemd-run|udevadm|bootctl|coredumpctl|gsettings|dconf|dbus-send|dbus-monitor|hyprctl|qs|quickshell|gnf|panama|wl-copy|wl-paste)$'
# Installed by install-packages itself rather than by a package list. Two # Installed by install-packages itself rather than by a package list. Two
# reasons, both deliberate: bun, claude and codex have no RPM or flatpak at # reasons, both deliberate: bun and codex use reviewed release archives,
# all (codex comes through npm), and node, npm and pnpm come from nvm on # Claude Code and pnpm use signed DNF repositories, and node/npm use a reviewed
# purpose -- a dnf nodejs earlier on PATH would win every per-project # Node archive inside nvm's version directory. A system nodejs earlier on PATH
# `nvm use`, which is the whole point of having nvm. # would win every per-project `nvm use`, which is the whole point of having nvm.
# Anything added here needs a matching install block and a stated reason. # Anything added here needs a matching install block and a stated reason.
SELF_INSTALLED='^(bun|claude|codex|node|npm|pnpm)$' SELF_INSTALLED='^(bun|claude|codex|node|npm|pnpm)$'
+170 -18
View File
@@ -27,8 +27,7 @@
# slot from a real icon rather than assuming one -- a DockItem is taller # slot from a real icon rather than assuming one -- a DockItem is taller
# than it is wide, so a constant is wrong on one of the two orientations. # than it is wide, so a constant is wrong on one of the two orientations.
# #
# The geometry checks launch isolated shells against a temporary config. The # The geometry checks launch isolated shells against a temporary config.
# real settings are read to build them and never written.
set -uo pipefail set -uo pipefail
@@ -47,8 +46,138 @@ for path in "$dock" "$body" "$strip"; do
[[ -r "$path" ]] || fail "missing $path" [[ -r "$path" ]] || fail "missing $path"
done done
work="$(mktemp -d)" # Keep this contract honest: it must never add a probe to the linked source
trap 'rm -rf "$work" "$shell_dir/dock-position-probe.qml"' EXIT # tree, and every exit path must leave that tree byte-for-byte as it began.
source_probe="$shell_dir/dock-position-probe.qml"
source_probe_existed=0
source_probe_checksum=""
if [[ -e "$source_probe" ]]; then
source_probe_existed=1
source_probe_checksum="$(sha256sum -- "$source_probe")"
fi
source_tree_checksum() {
find "$shell_dir" -type f -print0 \
| LC_ALL=C sort -z \
| xargs -0 sha256sum -- \
| sha256sum
}
source_tree_initial_checksum="$(source_tree_checksum)"
assert_source_tree_unchanged() {
[[ "$(source_tree_checksum)" == "$source_tree_initial_checksum" ]] \
|| { printf 'dock position contract: the Quickshell source tree changed\n' >&2; return 1; }
if (( source_probe_existed )); then
[[ -e "$source_probe" && "$(sha256sum -- "$source_probe")" == "$source_probe_checksum" ]] \
|| { printf 'dock position contract: the source probe changed\n' >&2; return 1; }
else
[[ ! -e "$source_probe" ]] \
|| { printf 'dock position contract: the contract created a source probe\n' >&2; return 1; }
fi
}
fixture_pid=""
stop_fixture() {
[[ -n "$fixture_pid" ]] || return 0
if kill -0 "$fixture_pid" 2>/dev/null; then
kill -TERM "$fixture_pid" 2>/dev/null || true
wait "$fixture_pid" 2>/dev/null || true
fi
fixture_pid=""
}
cleanup() {
local status=$?
local fixture_cleanup='pass'
local source_probe_assertions='pass'
trap - EXIT HUP INT TERM
stop_fixture
if ! rm -rf -- "$work"; then
printf 'dock position contract: could not remove temporary fixture\n' >&2
fixture_cleanup='fail'
status=1
fi
if ! assert_source_tree_unchanged; then
source_probe_assertions='fail'
status=1
fi
if [[ -n "${DOCK_POSITION_CONTRACT_CLEANUP_PROOF:-}" ]]; then
printf 'fixture-cleanup=%s source-and-probe-assertions=%s\n' \
"$fixture_cleanup" "$source_probe_assertions" \
>"$DOCK_POSITION_CONTRACT_CLEANUP_PROOF" || status=1
fi
exit "$status"
}
if ! work="$(mktemp -d)"; then
fail 'could not create a temporary fixture'
fi
[[ -d "$work" ]] || fail 'could not create a temporary fixture'
trap cleanup EXIT HUP INT TERM
fixture_shell="$work/quickshell"
# These checks are deliberately against this harness. A live settings read or
# source-tree probe means the contract itself is unsafe before it maps a panel.
probe_name='dock-position-probe.qml'
source_write_marker="$(printf 'cat >\"$shell_dir/%s\"' "$probe_name")"
home_dollar="$(printf '\044')"
home_tilde="$(printf '\176')"
home_marker="${home_dollar}HOME"
settings_store_spellings=(
"${home_dollar}HOME/.config/panama/settings.json"
"${home_dollar}{HOME}/.config/panama/settings.json"
"${home_tilde}/.config/panama/settings.json"
"${home_dollar}{XDG_CONFIG_HOME:-${home_dollar}HOME/.config}/panama/settings.json"
)
assert_no_real_settings_reference() {
local marker
for marker in "${settings_store_spellings[@]}"; do
grep -Fq "$marker" "$1" && return 1
done
return 0
}
run_settings_path_probes() {
local spelling
local number=0
for spelling in "${settings_store_spellings[@]}"; do
local fixture="$work/settings-path-$number"
printf 'settings=%s\n' "$spelling" >"$fixture"
assert_no_real_settings_reference "$fixture" \
&& fail "the real-settings guard accepted $spelling"
((number += 1))
done
local safe_fixture="$work/settings-path-safe"
printf 'XDG_CONFIG_HOME=%s/config\n' "$work" >"$safe_fixture"
assert_no_real_settings_reference "$safe_fixture" \
|| fail 'the real-settings guard rejected an isolated configuration'
}
run_pre_copy_cleanup_probe() {
[[ "${DOCK_POSITION_CONTRACT_SKIP_PRECOPY_PROBE:-}" == 1 ]] && return
local proof="$work/pre-copy-cleanup-proof"
if DOCK_POSITION_CONTRACT_INJECT_PRECOPY_FAILURE=1 \
DOCK_POSITION_CONTRACT_SKIP_PRECOPY_PROBE=1 \
DOCK_POSITION_CONTRACT_CLEANUP_PROOF="$proof" \
"$0" >"$work/pre-copy-output" 2>&1; then
fail 'the injected pre-copy failure did not fail'
fi
[[ -r "$proof" ]] \
|| fail 'the injected pre-copy failure did not leave cleanup proof'
grep -Fxq 'fixture-cleanup=pass source-and-probe-assertions=pass' "$proof" \
|| fail 'the injected pre-copy failure skipped cleanup or source/probe assertions'
}
unsafe_harness=()
grep -Fq "$source_write_marker" "$0" && unsafe_harness+=('writes its probe in the Quickshell source tree')
assert_no_real_settings_reference "$0" || unsafe_harness+=('reads the user configuration directory')
(( ${#unsafe_harness[@]} == 0 )) \
|| fail "unsafe harness: ${unsafe_harness[*]}"
run_settings_path_probes
[[ "${DOCK_POSITION_CONTRACT_INJECT_PRECOPY_FAILURE:-}" != 1 ]] \
|| fail 'injected pre-copy failure'
run_pre_copy_cleanup_probe
cp -a "$shell_dir/." "$fixture_shell/" \
|| fail 'could not copy the Quickshell fixture'
# ── 3, 4, 6. What can be read ─────────────────────────────────────────────── # ── 3, 4, 6. What can be read ───────────────────────────────────────────────
@@ -111,7 +240,8 @@ PY
# ── 1, 2, 7. Geometry, measured ──────────────────────────────────────────────── # ── 1, 2, 7. Geometry, measured ────────────────────────────────────────────────
cat >"$shell_dir/dock-position-probe.qml" <<'QML' probe="$fixture_shell/$probe_name"
cat >"$probe" <<'QML'
import Quickshell import Quickshell
import QtQuick import QtQuick
import qs.modules.dock import qs.modules.dock
@@ -164,28 +294,50 @@ ShellRoot {
} }
QML QML
settings_source="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json"
[[ -r "$settings_source" ]] || fail 'no settings to build a probe configuration from'
measure() { measure() {
local position="$1" local position="$1"
python3 - "$settings_source" "$work/panama/settings.json" "$position" <<'PY' python3 - "$work/config/panama/settings.json" "$position" <<'PY'
import json, pathlib, sys import json, pathlib, sys
data = json.loads(pathlib.Path(sys.argv[1]).read_text()) out = pathlib.Path(sys.argv[1])
data["dockPosition"] = sys.argv[3] data = {
out = pathlib.Path(sys.argv[2]) "schemaVersion": 1,
"dockAutohide": True,
"dockPosition": sys.argv[2],
"dockScreens": [],
"dockIconSize": 48,
"dockRevealDelayMs": 0,
"dockHideDelayMs": 250,
"dockPinned": [],
}
out.parent.mkdir(parents=True, exist_ok=True) out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(data)) out.write_text(json.dumps(data) + "\n")
PY PY
( cd "$shell_dir" && XDG_CONFIG_HOME="$work" timeout 40 qs -p ./dock-position-probe.qml 2>&1 ) \ local output="$work/$position.output"
| grep -oE '(DOCKGEOM|MASKGEOM) .*' (
cd "$fixture_shell" \
&& XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" \
exec timeout --foreground --kill-after=5s 40s qs -p "./$probe_name"
) >"$output" 2>&1 &
fixture_pid=$!
if ! wait "$fixture_pid"; then
fixture_pid=""
cat "$output" >&2
fail "the $position fixture did not exit cleanly"
fi
fixture_pid=""
geometry="$(grep -oE '(DOCKGEOM|MASKGEOM) .*' "$output")" \
|| fail "the $position fixture produced no geometry"
} }
bottom="$(measure bottom)" geometry=""
measure bottom
bottom="$geometry"
[[ -n "$bottom" ]] || fail 'the bottom dock produced no geometry at all' [[ -n "$bottom" ]] || fail 'the bottom dock produced no geometry at all'
left="$(measure left)" measure left
left="$geometry"
[[ -n "$left" ]] || fail 'the left dock produced no geometry at all' [[ -n "$left" ]] || fail 'the left dock produced no geometry at all'
right="$(measure right)" measure right
right="$geometry"
[[ -n "$right" ]] || fail 'the right dock produced no geometry at all' [[ -n "$right" ]] || fail 'the right dock produced no geometry at all'
# The span checks read the window; the region checks read every line. # The span checks read the window; the region checks read every line.
+40 -6
View File
@@ -25,7 +25,34 @@ fail() {
[[ -x "$generator" ]] || fail 'the generator is missing or not executable' [[ -x "$generator" ]] || fail 'the generator is missing or not executable'
scratch="$(mktemp -d /tmp/panama-docs.XXXXXX)"
trap 'rm -rf "$scratch"' EXIT
git -C "$repo_dir" diff -- docs/settings.md >"$scratch/docs.diff.before" \
|| fail 'could not capture the initial docs/settings.md state'
assert_doc_unchanged() {
git -C "$repo_dir" diff -- docs/settings.md >"$scratch/docs.diff.after" \
&& cmp -s "$scratch/docs.diff.before" "$scratch/docs.diff.after"
}
cleanup() {
status=$?
trap - EXIT HUP INT TERM
if ! assert_doc_unchanged; then
printf 'settings docs contract: the contract changed tracked docs/settings.md\n' >&2
status=1
fi
rm -rf "$scratch"
exit "$status"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
"$generator" --check || fail 'docs/settings.md is stale -- run quickshell/scripts/panama-settings-docs and commit the result' "$generator" --check || fail 'docs/settings.md is stale -- run quickshell/scripts/panama-settings-docs and commit the result'
assert_doc_unchanged || fail 'the default --check changed tracked docs/settings.md'
# A generator that silently emitted nothing would also pass --check against an # A generator that silently emitted nothing would also pass --check against an
# equally empty file, so the output is checked for substance too. # equally empty file, so the output is checked for substance too.
@@ -47,14 +74,21 @@ grep -q 'cursor:zoom_factor' "$doc" \
# A stale copy must be detectable, not merely regenerable. Prove the check # A stale copy must be detectable, not merely regenerable. Prove the check
# actually compares content rather than always returning success. # actually compares content rather than always returning success.
scratch="$(mktemp -d /tmp/panama-docs.XXXXXX)"
trap 'rm -rf "$scratch"' EXIT
cp "$doc" "$scratch/settings.md" cp "$doc" "$scratch/settings.md"
printf '\n<!-- drift -->\n' >>"$doc" printf '\n<!-- drift -->\n' >>"$scratch/settings.md"
if "$generator" --check >/dev/null 2>&1; then if "$generator" --check --output "$scratch/settings.md" >/dev/null 2>&1; then
cp "$scratch/settings.md" "$doc"
fail '--check reported success on a modified file, so staleness would never be caught' fail '--check reported success on a modified file, so staleness would never be caught'
fi fi
cp "$scratch/settings.md" "$doc" assert_doc_unchanged || fail '--check --output changed tracked docs/settings.md'
# An explicit relative destination belongs to the caller's working directory,
# not the repository root.
(
cd "$scratch" || exit 1
"$generator" --output generated.md >/dev/null
) || fail 'a relative --output path did not resolve from the current directory'
assert_doc_unchanged || fail '--output changed tracked docs/settings.md'
cmp -s "$doc" "$scratch/generated.md" \
|| fail 'generation through a relative --output path produced different documentation'
printf 'settings docs contract: PASS (%d settings documented)\n' "$settings" printf 'settings docs contract: PASS (%d settings documented)\n' "$settings"
+43 -13
View File
@@ -29,8 +29,8 @@
# instead of being handed whole to a Text element and to anybody reading # instead of being handed whole to a Text element and to anybody reading
# over a shoulder. # over a shoulder.
# #
# Runs entirely against a temporary config home. The real settings store is read # Runs entirely against temporary config homes: one explicit source fixture and
# for the export and never written. # one isolated import destination. The real settings store is never consulted.
set -uo pipefail set -uo pipefail
@@ -52,6 +52,29 @@ trap 'rm -rf "$work"' EXIT
bundle="$work/export.json" bundle="$work/export.json"
field() { python3 -c "import json,sys; print(json.load(sys.stdin)$1)"; } field() { python3 -c "import json,sys; print(json.load(sys.stdin)$1)"; }
export XDG_CONFIG_HOME="$work/source-config"
source_settings="$XDG_CONFIG_HOME/panama/settings.json"
mkdir -p "$(dirname "$source_settings")"
python3 - "$source_settings" <<'PY'
import json, sys
# A representative source store: every value that should travel is valid for
# its schema type, while the machine-only and unknown values prove the allow
# list does not export whatever else happens to be present.
fixture = {
"blurEnabled": False, # boolean
"gapsIn": 7, # integer
"vrrPolicy": 2, # numeric enum
"weatherLocation": "Fixture Harbor", # string
"displays": {"DP-9": "source-only"},
"lastPage": "appearance",
"schemaVersion": 1,
"someFutureToken": "Bearer fixture-secret",
}
with open(sys.argv[1], "w", encoding="utf-8") as target:
json.dump(fixture, target)
PY
# ── 1 & 2. Export carries taste, not hardware ─────────────────────────────── # ── 1 & 2. Export carries taste, not hardware ───────────────────────────────
"$helper" export "$bundle" >"$work/export-result.json" || fail 'export failed' "$helper" export "$bundle" >"$work/export-result.json" || fail 'export failed'
@@ -61,7 +84,7 @@ reason="$(field "['error']" <"$work/export-result.json")"
[[ "$(stat -c '%a' "$bundle")" == "600" ]] \ [[ "$(stat -c '%a' "$bundle")" == "600" ]] \
|| fail 'the export is readable by other accounts' || fail 'the export is readable by other accounts'
python3 - "$bundle" "$schema" "${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" <<'PY' || fail 'the export carried the wrong things, in one direction or the other' python3 - "$bundle" "$schema" "$source_settings" <<'PY' || fail 'the export carried the wrong things, in one direction or the other'
import json, re, sys import json, re, sys
bundle = json.load(open(sys.argv[1])) bundle = json.load(open(sys.argv[1]))
schema = open(sys.argv[2]).read() schema = open(sys.argv[2]).read()
@@ -105,6 +128,16 @@ PY
# ── 3, 4. Arrival is validated per key, and the types are all covered ─────── # ── 3, 4. Arrival is validated per key, and the types are all covered ───────
export XDG_CONFIG_HOME="$work/config" export XDG_CONFIG_HOME="$work/config"
destination_settings="$XDG_CONFIG_HOME/panama/settings.json"
mkdir -p "$(dirname "$destination_settings")"
python3 - "$destination_settings" <<'PY'
import json, sys
# This valid preference is deliberately absent from the source fixture and
# therefore from the bundle. Import must merge around it rather than replace it.
with open(sys.argv[1], "w", encoding="utf-8") as target:
json.dump({"borderSize": 4}, target)
PY
python3 - "$bundle" "$work/tampered.json" <<'PY' python3 - "$bundle" "$work/tampered.json" <<'PY'
import json, sys import json, sys
@@ -208,18 +241,15 @@ PY
# ── 5. Import merges rather than replaces ─────────────────────────────────── # ── 5. Import merges rather than replaces ───────────────────────────────────
python3 - "$work/config/panama/settings.json" <<'PY'
import json, sys
store = json.load(open(sys.argv[1]))
store["aSettingTheBundleNeverMentions"] = "kept"
json.dump(store, open(sys.argv[1], "w"))
PY
"$helper" import "$bundle" >/dev/null || fail 'second import failed' "$helper" import "$bundle" >/dev/null || fail 'second import failed'
python3 - "$work/config/panama/settings.json" <<'PY' || fail 'import replaced the store instead of merging into it' python3 - "$bundle" "$destination_settings" <<'PY' || fail 'import replaced the store instead of merging into it'
import json, sys import json, sys
store = json.load(open(sys.argv[1])) bundle = json.load(open(sys.argv[1]))["settings"]
if store.get("aSettingTheBundleNeverMentions") != "kept": store = json.load(open(sys.argv[2]))
raise SystemExit('a setting the bundle did not mention was removed') if "borderSize" in bundle:
raise SystemExit('the merge sentinel unexpectedly appeared in the import bundle')
if store.get("borderSize") != 4:
raise SystemExit('a schema-valid setting the bundle did not mention was removed')
PY PY
applied="$("$helper" import "$bundle" | field "['applied']")" applied="$("$helper" import "$bundle" | field "['applied']")"
+37 -26
View File
@@ -24,36 +24,47 @@ note() { findings+=("$1"); }
[[ -d "$server_dir" ]] || { printf 'compose secrets contract: no server/ directory\n' >&2; exit 1; } [[ -d "$server_dir" ]] || { printf 'compose secrets contract: no server/ directory\n' >&2; exit 1; }
# ── 1. Tracked content is clean ────────────────────────────────────────────── # ── 1. Scanner fixtures and tracked content ──────────────────────────────────
#
# Only tracked files: the live .env a cutover briefly leaves in a service
# directory is exactly what the gitignore exists for, and flagging it here
# would punish the ignore for working.
while IFS= read -r file; do scanner="$repo_dir/tests/server/scan-tracked-secrets.py"
path="$repo_dir/$file" fixtures_dir="$repo_dir/tests/server/fixtures/secrets"
[[ -f "$path" ]] || continue
# A secret-bearing key with a literal value. ${VAR} interpolations, empty if ! python3 "$scanner" "$fixtures_dir/clean" \
# values, the CHANGE_ME placeholder, and booleans (ALLOW_EMPTY_PASSWORD=yes compose.yml .env.example README.md signature-near-misses.txt; then
# is a switch, not a credential) are the allowed shapes; anything else note 'the clean secret-scanning fixture was rejected'
# after PASSWORD/SECRET/TOKEN/KEY is treated as a leak. Keys that merely fi
# configure where a secret lives (a *_FILE path, a key NAME) are not
# values. expect_leak() {
local fixture="$1"
local expected="$2"
local output
local status
output="$(python3 "$scanner" "$fixtures_dir/leaked" "$fixture" 2>&1)"
status=$?
if (( status != 1 )); then
note "the leaked $fixture fixture exited $status instead of 1"
elif [[ "$output" != "$expected" ]]; then
note "the leaked $fixture fixture reported '$output' instead of '$expected'"
fi
}
expect_leak compose.yml 'compose.yml:4: POSTGRES_PASSWORD'
expect_leak .env.example '.env.example:1: API_TOKEN'
expect_leak plain-list.yml 'plain-list.yml:4: API_TOKEN'
expect_leak quoted-mapping.yml 'quoted-mapping.yml:4: API_TOKEN'
expect_leak quoted-list.yml 'quoted-list.yml:4: API_TOKEN'
expect_leak pem-private-key.txt 'pem-private-key.txt:1: private key'
expect_leak anthropic-token.txt 'anthropic-token.txt:1: provider token'
expect_leak github-token.txt 'github-token.txt:1: provider token'
expect_leak slack-token.txt 'slack-token.txt:1: provider token'
mapfile -t tracked_server_files < <(git -C "$repo_dir" ls-files 'server/**' 'server/*')
if ! output="$(python3 "$scanner" "$repo_dir" "${tracked_server_files[@]}" 2>&1)"; then
while IFS= read -r hit; do while IFS= read -r hit; do
note "$file looks like it carries a secret: ${hit%%[=:]*}" [[ -n "$hit" ]] && note "$hit"
done < <(grep -inE '(password|secret|token|api_key|private_key|access_key)[a-z0-9_]*[[:space:]]*[:=]' "$path" 2>/dev/null \ done <<< "$output"
| grep -vE '[:=][[:space:]]*["'"'"']?(\$\{|CHANGE_ME|(true|false|yes|no|[01])["'"'"']?[[:space:]]*$|["'"'"']?[[:space:]]*$)' \
| grep -viE '(_file|_path|_name|_key_name)[[:space:]]*[:=]' \
| grep -vE '^[0-9]+:[[:space:]]*#')
if grep -qE 'BEGIN [A-Z ]*PRIVATE KEY' "$path" 2>/dev/null; then
note "$file contains a private key"
fi fi
if grep -qE 'sk-ant-[A-Za-z0-9]|ghp_[A-Za-z0-9]{20}|xox[baprs]-[A-Za-z0-9]' "$path" 2>/dev/null; then
note "$file contains something that looks like an API token"
fi
done < <(git -C "$repo_dir" ls-files 'server/')
# ── 2. The ignore still stands ─────────────────────────────────────────────── # ── 2. The ignore still stands ───────────────────────────────────────────────
# #
@@ -0,0 +1,7 @@
POSTGRES_PASSWORD=CHANGE_ME
API_TOKEN=
ALLOW_EMPTY_PASSWORD=yes
FEATURE_SECRET_ENABLED=false
PRIVATE_KEY_PATH=/run/secrets/private_key
TOKENIZER_MODEL=gpt2
PASSWORDLESS_PROVIDER=webauthn
@@ -0,0 +1,5 @@
This prose is not a Compose assignment: password: example.
It also says secret key without defining one.
The server README sentence that triggered the audit is reproduced here:
under `server/` carries anything that looks like a secret: this repository is
public, and the gitignore is a seatbelt, not the brakes.
@@ -0,0 +1,8 @@
services:
database:
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
API_TOKEN: ${API_TOKEN:-CHANGE_ME}
ALLOW_EMPTY_PASSWORD: yes
DATABASE_NAME: application
@@ -0,0 +1,4 @@
-----BEGIN SYNTHETIC PUBLIC KEY-----
sk-ant-
ghp_0123456789ABCDEFGHI
xoxb-
@@ -0,0 +1 @@
API_TOKEN=fixture-should-be-rejected
@@ -0,0 +1 @@
sk-ant-SYNTHETIC_FIXTURE_TOKEN
@@ -0,0 +1,4 @@
services:
database:
environment:
POSTGRES_PASSWORD: fixture-should-be-rejected
@@ -0,0 +1 @@
ghp_0123456789ABCDEFGHIJ
@@ -0,0 +1 @@
-----BEGIN SYNTHETIC PRIVATE KEY-----
@@ -0,0 +1,4 @@
services:
application:
environment:
- API_TOKEN=fixture-should-be-rejected
@@ -0,0 +1,4 @@
services:
application:
environment:
- "API_TOKEN=fixture-should-be-rejected"
@@ -0,0 +1,4 @@
services:
application:
environment:
"API_TOKEN": fixture-should-be-rejected
@@ -0,0 +1 @@
xoxb-SYNTHETIC_FIXTURE_TOKEN
+153
View File
@@ -0,0 +1,153 @@
"""Scan tracked Compose and env files for literal secrets.
The parser intentionally understands only the assignment forms this repository
uses. Markdown and other prose are only checked for unmistakable token and PEM
private-key signatures.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
from typing import Iterable
ENV_FILENAMES = {".env", ".env.example"}
YAML_SUFFIXES = {".yaml", ".yml"}
IGNORED_KEY_SUFFIXES = ("_FILE", "_PATH", "_NAME", "_KEY_NAME")
CREDENTIAL_SEGMENTS = {"PASSWORD", "SECRET", "TOKEN"}
CREDENTIAL_KEY_SHAPES = (("API", "KEY"), ("PRIVATE", "KEY"), ("ACCESS", "KEY"))
BOOLEAN_VALUES = {"true", "false", "yes", "no", "0", "1"}
ENV_ASSIGNMENT = re.compile(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$")
YAML_MAPPING = re.compile(
r"^(\s*)(?:\"([A-Za-z_][A-Za-z0-9_.-]*)\"|'([A-Za-z_][A-Za-z0-9_.-]*)'|([A-Za-z_][A-Za-z0-9_.-]*))\s*:\s*(.*?)\s*$"
)
YAML_ENV_ITEM = re.compile(r"^(\s*)-\s+(.*?)\s*$")
PRIVATE_KEY_HEADER = re.compile(r"BEGIN [A-Z ]*PRIVATE KEY")
PROVIDER_TOKEN = re.compile(r"sk-ant-[A-Za-z0-9]|ghp_[A-Za-z0-9]{20}|xox[baprs]-[A-Za-z0-9]")
def strip_comment(value: str) -> str:
"""Remove a YAML-style comment while preserving quoted values."""
quote = ""
escaped = False
for index, character in enumerate(value):
if escaped:
escaped = False
continue
if quote == '"' and character == "\\":
escaped = True
continue
if character in {"'", '"'}:
if not quote:
quote = character
elif quote == character:
quote = ""
continue
if character == "#" and not quote and (index == 0 or value[index - 1].isspace()):
return value[:index].rstrip()
return value.rstrip()
def normalize_value(value: str) -> str:
value = strip_comment(value).strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
return value[1:-1].strip()
return value
def is_secret_key(key: str) -> bool:
normalized = key.upper()
if normalized.endswith(IGNORED_KEY_SUFFIXES):
return False
segments = tuple(segment for segment in re.split(r"[_.-]+", normalized) if segment)
if any(segment in CREDENTIAL_SEGMENTS for segment in segments):
return True
return any(
segments[index : index + len(shape)] == shape
for shape in CREDENTIAL_KEY_SHAPES
for index in range(len(segments) - len(shape) + 1)
)
def is_placeholder(value: str) -> bool:
normalized = normalize_value(value)
if not normalized or normalized.upper() == "CHANGE_ME":
return True
if normalized.lower() in BOOLEAN_VALUES | {"null", "~"}:
return True
return bool(re.fullmatch(r"\$\{[^}\n]+\}", normalized))
def semantic_findings(path: str, content: str) -> Iterable[str]:
if Path(path).name in ENV_FILENAMES:
for line_number, line in enumerate(content.splitlines(), start=1):
match = ENV_ASSIGNMENT.match(strip_comment(line))
if match and is_secret_key(match.group(1)) and not is_placeholder(match.group(2)):
yield f"{path}:{line_number}: {match.group(1)}"
return
if Path(path).suffix.lower() not in YAML_SUFFIXES:
return
environment_indents: list[int] = []
for line_number, line in enumerate(content.splitlines(), start=1):
statement = strip_comment(line)
if not statement.strip():
continue
mapping = YAML_MAPPING.match(statement)
item = YAML_ENV_ITEM.match(statement)
indent = len((mapping or item).group(1)) if mapping or item else len(statement) - len(statement.lstrip())
environment_indents = [depth for depth in environment_indents if indent > depth]
if mapping:
key = mapping.group(2) or mapping.group(3) or mapping.group(4)
value = mapping.group(5)
if key.lower() == "environment" and not value.strip():
environment_indents.append(indent)
if is_secret_key(key) and not is_placeholder(value):
yield f"{path}:{line_number}: {key}"
elif item and environment_indents:
assignment = ENV_ASSIGNMENT.match(normalize_value(item.group(2)))
if assignment:
key, value = assignment.groups()
if is_secret_key(key) and not is_placeholder(value):
yield f"{path}:{line_number}: {key}"
def signature_findings(path: str, content: str) -> Iterable[str]:
for line_number, line in enumerate(content.splitlines(), start=1):
if PRIVATE_KEY_HEADER.search(line):
yield f"{path}:{line_number}: private key"
if PROVIDER_TOKEN.search(line):
yield f"{path}:{line_number}: provider token"
def scan(root: Path, paths: Iterable[str]) -> list[str]:
findings: list[str] = []
for path in paths:
candidate = Path(path)
file_path = candidate if candidate.is_absolute() else root / candidate
if not file_path.is_file():
continue
content = file_path.read_text(encoding="utf-8", errors="replace")
findings.extend(semantic_findings(path, content))
findings.extend(signature_findings(path, content))
return findings
def main(argv: list[str]) -> int:
if len(argv) < 2:
print("usage: scan-tracked-secrets.py ROOT PATH [PATH ...]", file=sys.stderr)
return 2
findings = scan(Path(argv[0]), argv[1:])
for finding in findings:
print(finding)
return 1 if findings else 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
+376 -57
View File
@@ -1,18 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# The front door: `boot` is the script the README tells a fresh machine to # `boot` is downloaded before the repository exists. It may hand off only
# curl, so it runs before anything else Panama ships -- including its own # after both the downloaded script and the requested Git commit have been
# tests. What it must get right is small and worth pinning: # verified. This fixture stubs Git and install inside a throwaway PANAMA_PATH;
# # it never contacts the network or mutates the real checkout.
# * a machine without the clone gets one, from the documented URL, at
# PANAMA_PATH, and the install runs
# * a machine with the clone is not re-cloned -- the same command is the
# recovery command -- and a fast-forward failure does not stop the install
# * boot hands off to the clone's own install, with PANAMA_PATH exported,
# so a clone at a chosen location installs from that location
#
# Run against stub git and install in a throwaway PANAMA_PATH; nothing here
# touches the real clone or the network.
set -uo pipefail set -uo pipefail
@@ -24,17 +15,29 @@ note() { findings+=("$1"); }
[[ -x "$boot" ]] || { printf 'boot contract: %s is not executable\n' "$boot" >&2; exit 1; } [[ -x "$boot" ]] || { printf 'boot contract: %s is not executable\n' "$boot" >&2; exit 1; }
# Git is the only package boot can install before the verified checkout exists.
# Both root-server and ordinary-user paths must exclude ambient third-party
# repositories while still allowing Fedora dependencies.
for git_install in \
'dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git' \
'sudo dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git'; do
grep -qF "$git_install" "$boot" \
|| note "boot omits reviewed Fedora source binding: $git_install"
done
work="$(mktemp -d)" work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT trap 'rm -rf "$work"' EXIT
calls="$work/calls" calls="$work/calls"
state="$work/state"
stub_dir="$work/bin" stub_dir="$work/bin"
clone_dir="$work/Panama" clone_dir="$work/Panama"
mkdir -p "$stub_dir" revision='0123456789abcdef0123456789abcdef01234567'
ancestor_revision='1111111111111111111111111111111111111111'
mismatched_revision='fedcba9876543210fedcba9876543210fedcba98'
boot_sha="$(sha256sum "$boot" | cut -d' ' -f1)"
mkdir -p "$stub_dir" "$state"
# The stub install records that it ran and what PANAMA_PATH it saw. The stub
# git records its arguments, and materializes a clone the way the real one
# would -- boot execs the clone's install, so the clone has to contain one.
cat >"$work/fake-install" <<STUB cat >"$work/fake-install" <<STUB
#!/usr/bin/env bash #!/usr/bin/env bash
printf 'install PANAMA_PATH=%s\n' "\${PANAMA_PATH:-unset}" >>"$calls" printf 'install PANAMA_PATH=%s\n' "\${PANAMA_PATH:-unset}" >>"$calls"
@@ -44,56 +47,372 @@ chmod +x "$work/fake-install"
cat >"$stub_dir/git" <<STUB cat >"$stub_dir/git" <<STUB
#!/usr/bin/env bash #!/usr/bin/env bash
printf 'git %s\n' "\$*" >>"$calls" printf 'git %s\n' "\$*" >>"$calls"
if [[ "\$1" == "clone" ]]; then mode="\$(<"$state/mode")"
mkdir -p "\$3/.git"
cp "$work/fake-install" "\$3/install" case "\${1:-}" in
init)
[[ "\$#" -eq 2 && "\$2" == "$clone_dir" ]] || exit 97
mkdir -p "$clone_dir/.git"
;;
-C)
[[ "\${2:-}" == "$clone_dir" ]] || exit 97
case "\${3:-}" in
remote)
[[ "\$#" -eq 6 && "\$4" == add && "\$5" == origin \
&& "\$6" == https://git.gbrown.org/gib/Panama.git ]] || exit 97
;;
fetch)
if [[ "\${4:-}" == --depth=1 ]]; then
[[ "\$#" -eq 6 && "\$5" == origin && "\$6" == "$revision" ]] || exit 97
else
[[ "\$#" -eq 5 && "\$4" == origin && "\$5" == "$revision" ]] || exit 97
fi fi
[[ "\$mode" != fetch-failure && "\$mode" != fresh-fetch-failure ]] || exit 42
;;
checkout)
if [[ "\${4:-}" == --detach ]]; then
[[ "\$#" -eq 5 && "\$5" == "$revision" ]] || exit 97
cp "$work/fake-install" "$clone_dir/install"
chmod +x "$clone_dir/install"
printf '%s\n' '$revision' >"$clone_dir/.git/HEAD"
elif [[ "\${4:-}" == -b ]]; then
[[ "\$#" -eq 5 && "\$5" == main ]] || exit 97
mkdir -p "$clone_dir/.git/refs/heads"
printf '%s\n' '$revision' >"$clone_dir/.git/refs/heads/main"
printf 'ref: refs/heads/main\n' >"$clone_dir/.git/HEAD"
else
exit 97
fi
;;
config)
case "\${4:-}:\${5:-}:\${6:-}" in
branch.main.remote:origin:|branch.main.merge:refs/heads/main:) ;;
*) exit 97 ;;
esac
;;
status)
[[ "\$#" -eq 4 && "\$4" == --porcelain ]] || exit 97
cat "$state/status"
;;
merge-base)
[[ "\$#" -eq 6 && "\$4" == --is-ancestor && "\$5" == HEAD \
&& "\$6" == "$revision" ]] || exit 97
[[ "\$mode" != divergent ]] || exit 1
;;
merge)
[[ "\$#" -eq 5 && "\$4" == --ff-only && "\$5" == "$revision" ]] || exit 97
if [[ "\$mode" == existing-head-mismatch ]]; then
printf '%s\n' '$mismatched_revision' >"$state/head-revision"
else
printf '%s\n' '$revision' >"$state/head-revision"
fi
cp "$work/fake-install" "$clone_dir/install"
chmod +x "$clone_dir/install"
;;
rev-parse)
[[ "\$#" -eq 4 && "\$4" == 'HEAD^{commit}' ]] || exit 97
cat "$state/head-revision"
;;
ls-tree)
[[ "\$#" -eq 6 && "\$4" == -rz && "\$5" == --full-tree \
&& "\$6" == "$revision" ]] || exit 97
printf '100755 blob aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\tinstall\0'
;;
hash-object)
[[ "\$#" -eq 6 && "\$4" == --no-filters && "\$5" == -- \
&& "\$6" == install ]] || exit 97
printf '%s\n' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
;;
*) exit 97 ;;
esac
;;
*) exit 97 ;;
esac
STUB STUB
chmod +x "$stub_dir/git" chmod +x "$stub_dir/git"
run_boot() { configure_case() {
local mode="$1" head_revision="${2:-$revision}" status="${3:-}"
rm -rf "$clone_dir"
: >"$calls" : >"$calls"
PATH="$stub_dir:$PATH" PANAMA_PATH="$clone_dir" bash "$boot" </dev/null >/dev/null 2>&1 printf '%s\n' "$mode" >"$state/mode"
printf '%s\n' "$head_revision" >"$state/head-revision"
printf '%s' "$status" >"$state/status"
} }
# ── A machine without the clone ────────────────────────────────────────────── configure_existing_case() {
configure_case "$@"
mkdir -p "$clone_dir/.git"
cp "$work/fake-install" "$clone_dir/install"
chmod +x "$clone_dir/install"
}
run_boot || note 'boot failed on a machine without the clone' run_boot() {
local supplied_revision="$1" supplied_sha="$2"
local -a env_args=(
"PATH=$stub_dir:$PATH"
"PANAMA_PATH=$clone_dir"
)
[[ "$supplied_revision" == UNSET ]] \
&& env_args+=(-u PANAMA_BOOT_REVISION) \
|| env_args+=("PANAMA_BOOT_REVISION=$supplied_revision")
[[ "$supplied_sha" == UNSET ]] \
&& env_args+=(-u PANAMA_BOOT_SHA256) \
|| env_args+=("PANAMA_BOOT_SHA256=$supplied_sha")
env "${env_args[@]}" bash "$boot" </dev/null >/dev/null 2>&1
run_status=$?
}
grep -q "git clone https://git.gbrown.org/gib/Panama.git $clone_dir" "$calls" \ assert_no_git_or_install() {
|| note 'boot does not clone the documented repository to PANAMA_PATH' local case_name="$1"
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \ if grep -qE '^(git|install) ' "$calls"; then
|| note 'boot does not hand off to the clone'\''s install with PANAMA_PATH exported' note "$case_name reached Git or install"
# ── A machine that already has it ────────────────────────────────────────────
run_boot || note 'boot failed on a machine that already has the clone'
grep -q 'git clone' "$calls" \
&& note 'boot re-clones over an existing checkout'
grep -q 'git -C .* pull --ff-only' "$calls" \
|| note 'boot does not fast-forward an existing clone'
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|| note 'boot does not run the install from an existing clone'
# ── A diverged clone still installs ──────────────────────────────────────────
#
# pull --ff-only refusing is normal life -- local commits, a rebase upstream.
# The command doubles as the repair path, so a refusal must be stepped over.
cat >"$stub_dir/git" <<'STUB'
#!/usr/bin/env bash
[[ "$*" == *pull* ]] && exit 1
exit 0
STUB
chmod +x "$stub_dir/git"
: >"$calls"
if ! PATH="$stub_dir:$PATH" PANAMA_PATH="$clone_dir" bash "$boot" </dev/null >/dev/null 2>&1; then
note 'a clone that cannot fast-forward stops the install instead of proceeding'
fi fi
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \ }
|| note 'the install does not run when the fast-forward is refused'
assert_no_install_or_rewrite() {
local case_name="$1"
grep -q '^install ' "$calls" && note "$case_name reached install"
grep -qE '^git .* (reset|checkout -B|checkout -f)($| )' "$calls" \
&& note "$case_name rewrote the checkout"
}
# Missing, malformed, or mismatched bootstrap inputs fail before Git.
input_cases=(
'missing revision|UNSET|BOOT_SHA'
'short revision|01234567|BOOT_SHA'
'uppercase revision|0123456789ABCDEF0123456789ABCDEF01234567|BOOT_SHA'
'missing digest|REVISION|UNSET'
'short digest|REVISION|01234567'
'uppercase digest|REVISION|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
'mismatched digest|REVISION|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
)
for input_case in "${input_cases[@]}"; do
IFS='|' read -r name supplied_revision supplied_sha <<<"$input_case"
[[ "$supplied_revision" == REVISION ]] && supplied_revision="$revision"
[[ "$supplied_sha" == BOOT_SHA ]] && supplied_sha="$boot_sha"
configure_case validation-only
run_boot "$supplied_revision" "$supplied_sha"
(( run_status != 0 )) || note "$name was accepted"
assert_no_git_or_install "$name"
done
# A fresh install fetches only the requested commit, verifies checked-out HEAD,
# creates the tracked local main branch, and hands off.
configure_case fresh
run_boot "$revision" "$boot_sha"
(( run_status == 0 )) || note 'verified fresh bootstrap failed'
grep -qF "git init $clone_dir" "$calls" \
|| note 'fresh bootstrap did not initialize PANAMA_PATH'
grep -qF "git -C $clone_dir fetch --depth=1 origin $revision" "$calls" \
|| note 'fresh bootstrap did not fetch the exact revision'
grep -qF "git -C $clone_dir rev-parse HEAD^{commit}" "$calls" \
|| note 'fresh bootstrap did not resolve the checked-out commit'
grep -qF "git -C $clone_dir checkout -b main" "$calls" \
|| note 'fresh bootstrap did not create local main after verification'
grep -qF "git -C $clone_dir config branch.main.remote origin" "$calls" \
|| note 'fresh bootstrap did not configure main remote tracking'
grep -qF "git -C $clone_dir config branch.main.merge refs/heads/main" "$calls" \
|| note 'fresh bootstrap did not configure main merge tracking'
grep -qF "install PANAMA_PATH=$clone_dir" "$calls" \
|| note 'verified fresh bootstrap did not hand off with PANAMA_PATH'
grep -qE '^git (clone|.* pull)' "$calls" \
&& note 'fresh bootstrap used mutable clone or pull behavior'
# A fresh fetch failure stops before any checked-out HEAD or executable
# handoff exists. The empty initialized Git directory is not a usable checkout.
configure_case fresh-fetch-failure
run_boot "$revision" "$boot_sha"
(( run_status != 0 )) || note 'fresh fetch failure returned success'
grep -qF "git -C $clone_dir fetch --depth=1 origin $revision" "$calls" \
|| note 'fresh fetch failure did not exercise the exact fetch'
assert_no_install_or_rewrite 'fresh fetch failure'
if grep -qE '^git .* (checkout|rev-parse)($| )' "$calls"; then
note 'fresh fetch failure reported or materialized a checked-out HEAD'
fi
if [[ -x "$clone_dir/install" || -e "$clone_dir/.git/HEAD" \
|| -e "$clone_dir/.git/refs/heads/main" ]]; then
note 'fresh fetch failure left a usable checkout'
fi
# A fetched checkout whose HEAD does not equal the requested commit never
# creates the trusted branch or reaches install.
configure_case head-mismatch "$mismatched_revision"
run_boot "$revision" "$boot_sha"
(( run_status != 0 )) || note 'fresh HEAD mismatch returned success'
assert_no_install_or_rewrite 'fresh HEAD mismatch'
grep -qF "git -C $clone_dir checkout -b main" "$calls" \
&& note 'fresh HEAD mismatch created local main before equality passed'
# A clean existing ancestor is fetched and advanced with fast-forward only.
configure_existing_case existing "$ancestor_revision"
run_boot "$revision" "$boot_sha"
(( run_status == 0 )) || note 'clean ancestor bootstrap failed'
grep -qF "git -C $clone_dir status --porcelain" "$calls" \
|| note 'existing checkout cleanliness was not checked'
grep -qF "git -C $clone_dir fetch origin $revision" "$calls" \
|| note 'existing checkout did not fetch the exact revision'
grep -qF "git -C $clone_dir merge-base --is-ancestor HEAD $revision" "$calls" \
|| note 'existing checkout ancestry was not checked'
grep -qF "git -C $clone_dir merge --ff-only $revision" "$calls" \
|| note 'existing checkout was not advanced fast-forward-only'
grep -qF "install PANAMA_PATH=$clone_dir" "$calls" \
|| note 'verified existing checkout did not reach install'
grep -qE '^git .* (reset|pull)($| )' "$calls" \
&& note 'existing checkout used reset or pull instead of the exact revision'
# Dirty and divergent checkouts fail closed without rewriting or installing.
configure_existing_case dirty "$ancestor_revision" $' M boot\n'
run_boot "$revision" "$boot_sha"
(( run_status != 0 )) || note 'dirty checkout returned success'
grep -qF "git -C $clone_dir fetch origin $revision" "$calls" \
&& note 'dirty checkout fetched before refusing local work'
assert_no_install_or_rewrite 'dirty checkout'
configure_existing_case divergent "$ancestor_revision"
run_boot "$revision" "$boot_sha"
(( run_status != 0 )) || note 'divergent checkout returned success'
grep -qF "git -C $clone_dir merge --ff-only $revision" "$calls" \
&& note 'divergent checkout attempted a merge'
assert_no_install_or_rewrite 'divergent checkout'
# Fetch and post-fast-forward equality failures also stop before handoff.
configure_existing_case fetch-failure "$ancestor_revision"
run_boot "$revision" "$boot_sha"
(( run_status != 0 )) || note 'fetch failure returned success'
assert_no_install_or_rewrite 'fetch failure'
configure_existing_case existing-head-mismatch "$ancestor_revision"
run_boot "$revision" "$boot_sha"
(( run_status != 0 )) || note 'existing HEAD mismatch returned success'
assert_no_install_or_rewrite 'existing HEAD mismatch'
# Git's porcelain status deliberately trusts index hints. The bootstrap cannot:
# these two flags can hide changed executable bytes while HEAD still names the
# reviewed commit. Exercise real Git so the contract cannot accidentally teach
# its adapter to expose state that Git itself hides.
real_git="$(command -v git)"
hidden_root="$work/hidden-index"
mkdir -p "$hidden_root/home"
"$real_git" init -q "$hidden_root/source"
"$real_git" -C "$hidden_root/source" config user.email contract@panama
"$real_git" -C "$hidden_root/source" config user.name contract
printf '#!/usr/bin/env bash\nexit 0\n' >"$hidden_root/source/install"
chmod +x "$hidden_root/source/install"
printf 'trusted target bytes\n' >"$hidden_root/source/target"
ln -s target "$hidden_root/source/trusted-link"
"$real_git" -C "$hidden_root/source" add install target trusted-link
"$real_git" -C "$hidden_root/source" commit -qm trusted
hidden_revision="$("$real_git" -C "$hidden_root/source" rev-parse HEAD)"
"$real_git" clone -q --bare "$hidden_root/source" "$hidden_root/origin.git"
# Exercise the exact boundary between checkout preparation and handoff. This
# test-only copy inserts a same-UID replacement after prepare returns; the
# production handoff must perform its complete comparison after that point.
post_prepare_checkout="$hidden_root/post-prepare-swap"
post_prepare_marker="$hidden_root/post-prepare-executed"
post_prepare_hook_marker="$hidden_root/post-prepare-hook-fired"
"$real_git" clone -q "$hidden_root/origin.git" "$post_prepare_checkout"
post_prepare_hook="$hidden_root/swap-install"
cat >"$post_prepare_hook" <<'HOOK'
#!/usr/bin/env bash
: >"$PANAMA_BOOT_POST_PREPARE_HOOK_MARKER"
printf '#!/usr/bin/env bash\nprintf "executed\\n" >%q\n' \
"$PANAMA_BOOT_POST_PREPARE_MARKER" >"$PANAMA_PATH/install"
chmod +x "$PANAMA_PATH/install"
HOOK
chmod +x "$post_prepare_hook"
hooked_boot="$hidden_root/boot-post-prepare-hook"
awk '
{
print
if ($0 == "prepare_panama_checkout \"$PANAMA_PATH\"") {
prepare_count++
if (prepare_count == 1) print "\"$PANAMA_BOOT_POST_PREPARE_FIXTURE\""
}
}
' "$boot" >"$hooked_boot"
hooked_boot_sha="$(sha256sum "$hooked_boot" | cut -d' ' -f1)"
post_prepare_status=0
HOME="$hidden_root/home" PANAMA_PATH="$post_prepare_checkout" \
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$hooked_boot_sha" \
PANAMA_BOOT_POST_PREPARE_FIXTURE="$post_prepare_hook" \
PANAMA_BOOT_POST_PREPARE_MARKER="$post_prepare_marker" \
PANAMA_BOOT_POST_PREPARE_HOOK_MARKER="$post_prepare_hook_marker" \
bash "$hooked_boot" </dev/null >"$hidden_root/post-prepare.out" 2>&1 \
|| post_prepare_status=$?
[[ -e "$post_prepare_hook_marker" ]] \
|| note 'post-prepare replacement hook did not exercise the boundary'
(( post_prepare_status != 0 )) \
|| note 'post-prepare worktree replacement returned success'
[[ ! -e "$post_prepare_marker" ]] \
|| note 'post-prepare worktree replacement executed unreviewed install bytes'
# A valid tracked symlink must compare its link text with Git's 120000 blob;
# hashing the pathname would follow it and hash the target file instead.
symlink_checkout="$hidden_root/tracked-symlink"
"$real_git" clone -q "$hidden_root/origin.git" "$symlink_checkout"
symlink_status=0
HOME="$hidden_root/home" PANAMA_PATH="$symlink_checkout" \
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$boot_sha" \
bash "$boot" </dev/null >"$hidden_root/tracked-symlink.out" 2>&1 \
|| symlink_status=$?
(( symlink_status == 0 )) \
|| note 'a checkout with a valid tracked symlink was rejected'
for hidden_flag in assume-unchanged skip-worktree; do
hidden_checkout="$hidden_root/$hidden_flag"
hidden_marker="$hidden_root/$hidden_flag-executed"
"$real_git" clone -q "$hidden_root/origin.git" "$hidden_checkout"
printf '#!/usr/bin/env bash\nprintf "executed\\n" >%q\n' "$hidden_marker" \
>"$hidden_checkout/install"
chmod +x "$hidden_checkout/install"
"$real_git" -C "$hidden_checkout" update-index "--$hidden_flag" install
[[ -z "$("$real_git" -C "$hidden_checkout" status --porcelain)" ]] \
|| note "$hidden_flag fixture was not hidden from porcelain status"
hidden_status=0
HOME="$hidden_root/home" PANAMA_PATH="$hidden_checkout" \
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$boot_sha" \
bash "$boot" </dev/null >"$hidden_root/$hidden_flag.out" 2>&1 \
|| hidden_status=$?
(( hidden_status != 0 )) \
|| note "$hidden_flag modified checkout returned success"
[[ ! -e "$hidden_marker" ]] \
|| note "$hidden_flag modified checkout executed unreviewed install bytes"
done
# The same hidden-index state must not conceal a mode change or a different
# symlink target; both are part of the reviewed Git tree, not metadata hints.
for hidden_flag in assume-unchanged skip-worktree; do
for hidden_change in mode symlink-target; do
hidden_checkout="$hidden_root/$hidden_flag-$hidden_change"
"$real_git" clone -q "$hidden_root/origin.git" "$hidden_checkout"
case "$hidden_change" in
mode)
chmod -x "$hidden_checkout/install"
hidden_path=install
;;
symlink-target)
rm -- "$hidden_checkout/trusted-link"
ln -s untrusted-target "$hidden_checkout/trusted-link"
hidden_path=trusted-link
;;
esac
"$real_git" -C "$hidden_checkout" update-index "--$hidden_flag" "$hidden_path"
[[ -z "$("$real_git" -C "$hidden_checkout" status --porcelain)" ]] \
|| note "$hidden_flag $hidden_change fixture was not hidden from porcelain status"
hidden_status=0
HOME="$hidden_root/home" PANAMA_PATH="$hidden_checkout" \
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$boot_sha" \
bash "$boot" </dev/null >"$hidden_root/$hidden_flag-$hidden_change.out" 2>&1 \
|| hidden_status=$?
(( hidden_status != 0 )) \
|| note "$hidden_flag concealed a tracked $hidden_change change"
done
done
if (( ${#findings[@]} > 0 )); then if (( ${#findings[@]} > 0 )); then
printf 'boot contract: %d finding(s)\n' "${#findings[@]}" >&2 printf 'boot contract: %d finding(s)\n' "${#findings[@]}" >&2
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env bash
# Every runner-visible contract has one capability classification in
# tests/contracts.manifest. The manifest is deliberately complete: callers can
# decide what is safe to run without rediscovering test behaviour themselves.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
manifest="$repo_dir/tests/contracts.manifest"
discover_contracts() {
discovered_contracts=()
while IFS= read -r path; do
[[ -x "$path" || "$path" == *_test.py ]] || continue
discovered_contracts+=("tests/${path#"$repo_dir/tests/"}")
done < <(find "$repo_dir/tests" -type f \
-not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
}
validate_manifest() {
local candidate="$1"
local -n expected_contracts="$2"
local line capabilities path extra previous_comment="" previous_was_comment=0
local -a capability_list=()
local -A manifest_paths=() capability_counts=()
local previous_path=""
validation_findings=()
validation_note() { validation_findings+=("$1"); }
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*# ]]; then
previous_comment="${line#*#}"
previous_comment="${previous_comment#"${previous_comment%%[![:space:]]*}"}"
previous_comment="${previous_comment%"${previous_comment##*[![:space:]]}"}"
previous_was_comment=1
continue
fi
if [[ "$line" =~ ^[[:space:]]*$ ]]; then
previous_comment=""
previous_was_comment=0
continue
fi
IFS=$' \t' read -r capabilities path extra <<<"$line"
if [[ -z "${capabilities:-}" || -z "${path:-}" || -n "${extra:-}" ]]; then
validation_note "manifest line is not exactly two fields: $line"
previous_comment=""
previous_was_comment=0
continue
fi
if [[ -n "$previous_path" && "$path" < "$previous_path" ]]; then
validation_note 'paths are not lexicographically sorted'
fi
previous_path="$path"
if [[ -n "${manifest_paths[$path]:-}" ]]; then
validation_note "duplicate path $path"
fi
manifest_paths["$path"]=1
IFS=',' read -r -a capability_list <<<"$capabilities"
local -A line_capabilities=()
local capability
for capability in "${capability_list[@]}"; do
if [[ -z "$capability" ]]; then
validation_note "empty capability on $path"
continue
fi
if [[ -n "${line_capabilities[$capability]:-}" ]]; then
validation_note "duplicate capability $capability on $path"
fi
line_capabilities["$capability"]=1
case "$capability" in
hermetic|live-host|live-compositor|live-desktop|network|privileged)
capability_counts["$capability"]=1
;;
*) validation_note "unknown capability $capability on $path" ;;
esac
done
if [[ -n "${line_capabilities[hermetic]:-}" && ${#line_capabilities[@]} -ne 1 ]]; then
validation_note "hermetic must appear alone on $path"
fi
if [[ "$capabilities" != hermetic ]]; then
if [[ "$previous_was_comment" -ne 1 ]]; then
validation_note "$path is non-hermetic but lacks a directly preceding comment"
elif [[ -z "$previous_comment" ]]; then
validation_note "$path is non-hermetic but lacks a non-empty directly preceding comment"
fi
fi
previous_comment=""
previous_was_comment=0
done < "$candidate"
local expected
for expected in "${!expected_contracts[@]}"; do
[[ -n "${manifest_paths[$expected]:-}" ]] || validation_note "missing contract $expected"
done
for path in "${!manifest_paths[@]}"; do
[[ -n "${expected_contracts[$path]:-}" ]] || validation_note "stale manifest path $path"
done
for capability in live-host live-compositor live-desktop network; do
[[ -n "${capability_counts[$capability]:-}" ]] || validation_note "manifest has no $capability contract"
done
if (( ${#validation_findings[@]} > 0 )); then
printf 'contract manifest: %d finding(s)\n' "${#validation_findings[@]}" >&2
printf ' - %s\n' "${validation_findings[@]}" >&2
return 1
fi
return 0
}
cleanup_fixture() {
[[ -n "${fixture:-}" ]] && rm -f -- "$fixture"
}
run_parser_fixture() {
local label="$1" expected_message="$2" contents="$3" output fixture=""
shift 3
local -A fixture_paths=()
local fixture_path
for fixture_path in "$@"; do
fixture_paths["$fixture_path"]=1
done
fixture="$(mktemp)"
trap cleanup_fixture EXIT
trap 'cleanup_fixture; exit 130' INT
trap 'cleanup_fixture; exit 143' TERM
printf '%s' "$contents" > "$fixture"
if output="$(validate_manifest "$fixture" fixture_paths 2>&1)"; then
printf 'contract manifest: parser fixture %s unexpectedly passed\n' "$label" >&2
cleanup_fixture
trap - EXIT INT TERM
return 1
fi
cleanup_fixture
trap - EXIT INT TERM
if ! grep -Fq "$expected_message" <<<"$output"; then
printf 'contract manifest: parser fixture %s did not name %q: %s\n' \
"$label" "$expected_message" "$output" >&2
return 1
fi
}
run_parser_fixtures() {
run_parser_fixture missing-contract 'missing contract tests/b' \
$'hermetic tests/a\n' tests/a tests/b || return 1
run_parser_fixture stale-path 'stale manifest path tests/stale' \
$'hermetic tests/a\nhermetic tests/stale\n' tests/a || return 1
run_parser_fixture duplicate-path 'duplicate path tests/a' \
$'hermetic tests/a\nhermetic tests/a\n' tests/a || return 1
run_parser_fixture unknown-capability 'unknown capability unknown on tests/a' \
$'# Reads an external thing.\nunknown tests/a\n' tests/a || return 1
run_parser_fixture mixed-hermetic 'hermetic must appear alone on tests/a' \
$'# Uses the network.\nhermetic,network tests/a\n' tests/a || return 1
run_parser_fixture unsorted-paths 'paths are not lexicographically sorted' \
$'hermetic tests/b\nhermetic tests/a\n' tests/a tests/b || return 1
run_parser_fixture uncommented-non-hermetic \
'tests/a is non-hermetic but lacks a directly preceding comment' \
$'network tests/a\n' tests/a || return 1
run_parser_fixture blank-comment \
'tests/a is non-hermetic but lacks a non-empty directly preceding comment' \
$'#\nnetwork tests/a\n' tests/a || return 1
}
[[ -r "$manifest" ]] || {
printf 'contract manifest: %s is missing\n' "$manifest" >&2
exit 1
}
discover_contracts
declare -A discovered_paths=()
for path in "${discovered_contracts[@]}"; do
discovered_paths["$path"]=1
done
status=0
validate_manifest "$manifest" discovered_paths || status=1
run_parser_fixtures || status=1
(( status == 0 )) || exit 1
printf 'contract manifest: PASS (%d discovered; %d manifested)\n' \
"${#discovered_contracts[@]}" "${#discovered_paths[@]}"
+3 -2
View File
@@ -99,9 +99,10 @@ sed -n '/^if \[\[ "\$ROLE" == server \]\]; then/,/^fi/p' "$installer" | grep -q
# Comments dropped and backslash continuations joined, so a `soft` invocation # Comments dropped and backslash continuations joined, so a `soft` invocation
# wrapped across three lines reads as the one command it is. # wrapped across three lines reads as the one command it is.
uncommented() { grep -vE '^\s*#' "$installer" | sed -e :a -e '/\\$/N; s/\\\n\s*/ /; ta'; } uncommented() { grep -vE '^\s*#' "$installer" | sed -e :a -e '/\\$/N; s/\\\n\s*/ /; ta'; }
uncommented_installer="$(uncommented)"
while read -r command; do while read -r command; do
uncommented | grep -q "soft .*$command" \ grep -q "soft .*$command" <<<"$uncommented_installer" \
|| note "'$command' runs without soft, so its failure still ends the stage" || note "'$command' runs without soft, so its failure still ends the stage"
done <<'FRAGILE' done <<'FRAGILE'
dnf swap -y 'ffmpeg-free' dnf swap -y 'ffmpeg-free'
@@ -129,7 +130,7 @@ if "rpm -q hyprland" not in after or "exit 1" not in after:
raise SystemExit(1) raise SystemExit(1)
PY PY
uncommented | grep -q 'soft .*HYPR_PACKAGES' \ grep -q 'soft .*HYPR_PACKAGES' <<<"$uncommented_installer" \
&& note 'the Hyprland install is tolerated, so a machine with no desktop reports success' && note 'the Hyprland install is tolerated, so a machine with no desktop reports success'
# ── Soft failures are reported ────────────────────────────────────────────── # ── Soft failures are reported ──────────────────────────────────────────────
-184
View File
@@ -1,184 +0,0 @@
#!/usr/bin/env bash
# The desktop-hijacking ledger is complete, and `panama test --safe` obeys it.
#
# `panama test --safe` exists so the suite can be run from inside the session it
# tests. That promise is only as good as tests/desktop-hijacking: a contract
# that takes over the live shell and is not listed there is run by --safe, and
# the desktop goes away in the middle of somebody's work -- with the command
# line having just claimed it would not.
#
# A hand-kept list decays, so it is not trusted on its own. This sweeps tests/
# for the shapes a hijacking contract has and fails on any that are missing from
# the ledger. The heuristics live here, in the thing that runs, so a new
# hijacking contract cannot stay unlisted quietly:
#
# * it calls `qs ipc call` without booting its own `qs -p` harness, so the
# instance answering is the shell you are looking at;
# * it restarts panama-quickshell.service;
# * it calls a bare `qs kill`, which kills that same shell.
#
# The sweep is one direction only. The ledger is deliberately larger than what
# these three shapes find -- a contract that rotates the real monitor or
# rewrites a real xdg-mime default hijacks the session just as thoroughly and
# looks like nothing in particular from the outside -- so an entry the sweep
# does not reach is not a finding.
#
# Nothing here runs a listed contract. The one live check drives `--safe` with a
# pattern that matches only ledger entries, so the run selects them, skips them
# all, and executes nothing.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
ledger="$repo_dir/tests/desktop-hijacking"
panama="$repo_dir/bin/panama"
findings=()
note() { findings+=("$1"); }
[[ -r "$ledger" ]] || { printf 'desktop hijacking contract: %s is missing\n' "$ledger" >&2; exit 1; }
# ── The entries ──────────────────────────────────────────────────────────────
#
# Every line names a contract that exists AND that `panama test` would collect.
# A listed path the runner never picks up (no executable bit, not a *_test.py)
# is skipped by --safe in name only, which reads as protection and is not.
entries=()
commented=()
pending_comment=0
while IFS= read -r line || [[ -n "$line" ]]; do
case "$line" in
'#'*) pending_comment=1; continue ;;
''|[[:space:]]*'') ;;
esac
trimmed="${line%%#*}"
trimmed="${trimmed#"${trimmed%%[![:space:]]*}"}"
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
if [[ -z "$trimmed" ]]; then
[[ -z "$line" ]] && pending_comment=0
continue
fi
entries+=("$trimmed")
commented+=("$pending_comment")
pending_comment=0
done < "$ledger"
(( ${#entries[@]} > 0 )) || note 'the ledger lists no contracts at all'
for index in "${!entries[@]}"; do
entry="${entries[$index]}"
path="$repo_dir/$entry"
[[ "$entry" == tests/* ]] \
|| note "\"$entry\" is not a repo-relative path under tests/"
if [[ ! -e "$path" ]]; then
note "the ledger lists $entry, which does not exist"
continue
fi
[[ -x "$path" || "$entry" == *_test.py ]] \
|| note "$entry is listed but 'panama test' would never collect it, so skipping it protects nothing"
# The ledger's whole job is saying what a contract does to the session. An
# entry with no comment is a path somebody has to go and read.
(( commented[index] )) \
|| note "$entry is listed with no comment saying what it does to the live session"
done
duplicates="$(printf '%s\n' "${entries[@]}" | sort | uniq -d)"
[[ -z "$duplicates" ]] || note "the ledger lists these twice: ${duplicates//$'\n'/, }"
# ── The honesty sweep ────────────────────────────────────────────────────────
#
# Whole-line comments are dropped first: contracts discuss `qs ipc call` in
# their headers, and a header is not a call. What survives is matched only at a
# command position -- start of line, or after a pipe, semicolon, &&, (, or ! --
# so the same words quoted inside a grep pattern or a failure message do not
# count as driving anything.
command_position='(^|[|;&({!]|\$\()[[:space:]]*'
is_listed() {
local candidate="$1" listed
for listed in "${entries[@]}"; do
[[ "$listed" == "$candidate" ]] && return 0
done
return 1
}
while IFS= read -r file; do
[[ -x "$file" || "$file" == *_test.py ]] || continue
code="$(grep -v '^[[:space:]]*#' "$file")"
rel="tests/${file#"$repo_dir"/tests/}"
reason=""
if grep -qE "${command_position}qs[[:space:]]+ipc[[:space:]]+call" <<<"$code"; then
# Its own harness means its own Quickshell instance: `qs -p <entry>`
# addresses that root, not the shell running the desktop.
grep -qE "${command_position}[A-Za-z_]*[[:space:]]*=?[[:space:]]*.*qs[[:space:]]+-p" <<<"$code" \
|| reason='calls `qs ipc call` without booting its own `qs -p` harness'
fi
if [[ -z "$reason" ]] && grep -qE "${command_position}systemctl.*restart.*panama-quickshell\.service" <<<"$code"; then
reason='restarts panama-quickshell.service'
fi
if [[ -z "$reason" ]] && grep -qE "${command_position}qs[[:space:]]+kill" <<<"$code"; then
reason='calls a bare `qs kill`, which stops the live shell'
fi
[[ -n "$reason" ]] || continue
is_listed "$rel" \
|| note "$rel $reason, but is not in tests/desktop-hijacking"
done < <(find "$repo_dir/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
# ── --safe actually reads it ─────────────────────────────────────────────────
#
# Static first, because the summary line is the only thing telling a reader that
# anything was left out, and a --safe run that silently skips is worse than one
# that does not skip at all.
if [[ ! -r "$panama" ]]; then
note 'bin/panama is missing'
elif ! test_body="$(sed -n '/^cmd_test()/,/^}/p' "$panama")" || [[ -z "$test_body" ]]; then
note 'cmd_test could not be found in bin/panama'
else
grep -q -- '--safe' <<<"$test_body" \
|| note 'cmd_test does not handle --safe'
grep -qE 'DESKTOP_HIJACKING_LEDGER|desktop-hijacking' <<<"$test_body" \
|| note 'cmd_test never consults the desktop-hijacking ledger, so --safe skips nothing'
grep -qF 'desktop-hijacking contract(s)' <<<"$test_body" \
|| note '--safe no longer reports how many contracts it skipped'
fi
grep -qF 'tests/desktop-hijacking' "$panama" \
|| note 'bin/panama never names tests/desktop-hijacking'
# Then for real. The pattern is the first ledger entry with its 'tests/' prefix
# removed, which cmd_test matches against the full path -- so it selects that
# one contract, --safe removes it, and nothing is left to run. A --safe that
# ignored the ledger would run it instead, which is the failure this catches.
if (( ${#entries[@]} > 0 )) && [[ -x "$panama" ]]; then
probe="${entries[0]#tests/}"
output="$("$panama" test --safe "$probe" 2>&1)"
status=$?
if (( status == 0 )); then
note "'panama test --safe $probe' ran a ledger-listed contract instead of skipping it"
elif ! grep -qF 'desktop-hijacking' <<<"$output"; then
note "'panama test --safe $probe' refused without mentioning the ledger: $output"
fi
fi
if (( ${#findings[@]} > 0 )); then
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
printf 'desktop hijacking contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'desktop hijacking contract: PASS (%d contracts listed; the sweep found none unlisted)\n' "${#entries[@]}"
@@ -0,0 +1,4 @@
291bd319ae85488101e908e37fc0fa1b0da1429ba27e10d2b391cb3f60dd44ea tiny-artifact
5043218f6d8ecdb2ba78665116f74e7f53ce29d164117a0b6cb0f946811d3455 tiny-artifact-tampered
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa metadata-only-1
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb metadata-only-2
@@ -0,0 +1,11 @@
-----BEGIN PGP SIGNATURE-----
iQEzBAABCgAdFiEEYBb/GMrimM42SO4jJeAfdl4e+foFAmqQCQ8ACgkQJeAfdl4e
+fqUiQgAgPaA6eTS3ag1YMLhT4r3JVpL40HgKTLBfTgRjsc/AVwG9aV9BMYMHwGT
3pCzs1ULlF1Vw9mFODt6YPBJeuq2hdI26MVJ1YWo6YkiCqzrsu3LycZCvBezC8hi
6/4ZW/j/7tMeVr2Oqw4Vv5quvLQ2DO/ZE3vMtKJ+sRvVHnczlQJrrKpzJ15pVv5l
a5OMFfTeCeOXOJ145lDlEdvmjaJUPUp6cw248azhEAwu24a0u4iqIb59nz/IhLiD
/rkUQrc2CGwBEUSwDDMLELLMTHc/Ivm6xLQOI+TbT53+1IupyUu5gquRM7pdKGw7
SYTz3awbjyEzFLt60AnPsSWNyo5N6A==
=MJqv
-----END PGP SIGNATURE-----
@@ -0,0 +1,18 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBGqQBJkBCADBKEbp0EaiCrTvnCmu6Ux1MyofeiS5plcZqHiRIBM9zy9Apxll
bKFD0tOG2i8GpjAIb2uO2tCfaLsANhi4H43D1XBNGS3DLQlc486c4BlCmtUb3JN1
FJiR1Tx+R7FxYHdlD9ceDdxGOIrVxMH3eDoHD1xOQFLstzMi/J49x5n0NcLIscVU
BpOwerm8a+tHOoigcRUgdud1E7YJOGMrkkXy4wMTZ+DkcqK5L6KlyEOfv+Ib2PkN
0VOLlD6p3Nb8Vbt54zJh/e/UvA0dBy4szFCR5PeBBvPuAwkMsBTErbrJ4S22O33M
eN7PAEAZ174/BO+u1jH2RSC7BOJbxCcjxmHBABEBAAG0MlBhbmFtYSBQcm92ZW5h
bmNlIEZpeHR1cmUgPGZpeHR1cmVAcGFuYW1hLmludmFsaWQ+iQFRBBMBCgA7FiEE
YBb/GMrimM42SO4jJeAfdl4e+foFAmqQBJkCGwMFCwkIBwICIgIGFQoJCAsCBBYC
AwECHgcCF4AACgkQJeAfdl4e+frEkwf/Si/D8yDAVaTIJPmuEr9nVtpmw9/vFQ0u
xxTXnPkEWMogLMLFCU4I2xHrAzJdvwuPKldZtE2aD2CX1lCB2JQDf1X+gt+jRiqC
o/eFUorCsaLm1lUylTG5NbGoCNN8xkC5V5q5WoXUga7inW0B6isRoIjGiFX0AKb4
5w8iZ3jibGomIAtfIvtEChM0orB5Y59o5JzwnE4LJ0OTiZRiKB7ncUbflJJHfX4v
Zd2HjuLGaPuYuneVBKlFDGuN39sh2vRfQNyQEqFM9zTCxj1pBcscX+wAQAn6r9xM
p6Sz8nRaNgkAUEnfb84MeLwYHgOeG0vD3V1Gv440TAxIp7VdeaCNhg==
=SCW3
-----END PGP PUBLIC KEY BLOCK-----
@@ -0,0 +1,118 @@
7avu2wMAAAAAAHBhbmFtYS1wcm92ZW5hbmNlLWZpeHR1cmUtMS0xAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAjq3oAQAAAAAAAAAIAAAQpAAA
AD4AAAAHAAAQlAAAABAAAAEMAAAABwAAAAAAAAE2AAABDQAAAAYAAAE2AAAAAQAAAREAAAAGAAAB
XwAAAAEAAAPoAAAABAAAAaAAAAABAAAD7AAAAAcAAAGkAAAAEAAAA+8AAAAEAAABtAAAAAEAAAPw
AAAABwAAAbgAAA7ciQEzBAABCgAdFiEEYBb/GMrimM42SO4jJeAfdl4e+foFAmqQBNEACgkQJeAf
dl4e+fq1FAgApbV7aLtafCECpXQRZfsg2dYdWcujtZB2JKQ4yK93sBoScSARSapoGXyuGj6Yiu5b
lzY/5hIQLY68WyzN5dCtutTIycEqdk5ZUU2n7wVmWsshyEp3zNG9LmYo7fZFX3ViJdHk4bgVg8t4
dgUtCPpixl43gQGb+KDTKNVN+8acmi1a2gkNGjJgWyvCjsFLU7Rac7R/pVhtgfUo+JA8goKYfIeJ
SMTFd0j0mPpbucn6IgkQDVu7x1/1UA88C5fOOUQRwfHxTMoxrDxD55g2P6YmN4vHMeouY/iNxFTM
mBeQ8CVUBvwL6wbZeakd3zKJdDwsOBkl8ymBSg0a3hetoWO11DM1NTg5MDBmNWUwM2YwNzFhNjM1
ZDg1ZjgxODI3NTA3YjdiMWQ1MDYAMjQ4ZTQ3YzFiNjhlYjM5NDgxMWFmNWJhOGZlNGNiZTEzYmUy
MGM5N2I0MzM2MzM1MGRhOGE2MDY0ZWQ4YTk4ZgAAAAiZ9bUc5Ib0/pUFYQ1fQtgpwwAAASQAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAB////4AAAAAQAAAA
AI6t6AEAAAAAAAAAMwAABOEAAAA/AAAABwAABNEAAAAQAAAAZAAAAAgAAAAAAAAAAQAAA+gAAAAG
AAAAAgAAAAEAAAPpAAAABgAAABwAAAABAAAD6gAAAAYAAAAeAAAAAQAAA+wAAAAJAAAAIAAAAAEA
AAPtAAAACQAAAD8AAAABAAAD7gAAAAQAAABYAAAAAQAAA+8AAAAGAAAAXAAAAAEAAAPxAAAABAAA
AGgAAAABAAAD9gAAAAYAAABsAAAAAQAAA/gAAAAJAAAAcAAAAAEAAAP9AAAABgAAAHwAAAABAAAD
/gAAAAYAAACCAAAAAQAABAQAAAAEAAAAjAAAAAEAAAQGAAAAAwAAAJAAAAABAAAECQAAAAMAAACS
AAAAAQAABAoAAAAEAAAAlAAAAAEAAAQLAAAACAAAAJgAAAABAAAEDAAAAAgAAADZAAAAAQAABA0A
AAAEAAAA3AAAAAEAAAQPAAAACAAAAOAAAAABAAAEEAAAAAgAAADlAAAAAQAABBQAAAAGAAAA6gAA
AAEAAAQVAAAABAAAARAAAAABAAAEFwAAAAgAAAEUAAAAAQAABBgAAAAEAAABMAAAAAQAAAQZAAAA
CAAAAUAAAAAEAAAEGgAAAAgAAAGlAAAABAAABCgAAAAGAAABxAAAAAEAAARHAAAABAAAAcwAAAAB
AAAESAAAAAQAAAHQAAAAAQAABEkAAAAIAAAB1AAAAAEAAARYAAAABAAAAdgAAAABAAAEWQAAAAgA
AAHcAAAAAQAABFwAAAAEAAAB4AAAAAEAAARdAAAACAAAAeQAAAABAAAEXgAAAAgAAAHwAAAAAQAA
BGIAAAAGAAACFgAAAAEAAARkAAAABgAABAUAAAABAAAEZQAAAAYAAAQKAAAAAQAABGYAAAAGAAAE
DwAAAAEAAARsAAAABgAABBIAAAABAAAEdAAAAAQAAAQsAAAAAQAABHUAAAAEAAAEMAAAAAEAAAR2
AAAACAAABDQAAAABAAATkwAAAAQAAARAAAAAAQAAE8YAAAAGAAAERAAAAAEAABPkAAAACAAABEoA
AAABAAAT5QAAAAQAAASMAAAAAQAAE+kAAAAIAAAEkAAAAAFDAHBhbmFtYS1wcm92ZW5hbmNlLWZp
eHR1cmUAMQAxAFBhbmFtYSBwcm92ZW5hbmNlIHRlc3QgZml4dHVyZQBBIHRlc3Qtb25seSBSUE0g
Zml4dHVyZS4AapAEmmRlc2t0b3AtZ2liAAAAAAhNSVQAVW5zcGVjaWZpZWQAbGludXgAbm9hcmNo
AAAAAAAAAAiBpAAAapAEmmU4MGI3MWNkMTRkM2NiZDY1ZjQxNzNhYmNiZmNmMDFhNTQ1ZGJjYTMy
YTcyZDU3NTEwOGI1NTNhNjQ4Y2M5NmYAAAAAAAAAAHJvb3QAcm9vdABwYW5hbWEtcHJvdmVuYW5j
ZS1maXh0dXJlLTEtMS5zcmMucnBtAP////9wYW5hbWEtcHJvdmVuYW5jZS1maXh0dXJlAAAAAQAA
CgEAAAoBAAAKAQAACnJwbWxpYihDb21wcmVzc2VkRmlsZU5hbWVzKQBycG1saWIoRmlsZURpZ2Vz
dHMpAHJwbWxpYihQYXlsb2FkRmlsZXNIYXZlUHJlZml4KQBycG1saWIoUGF5bG9hZElzWnN0ZCkA
My4wLjQtMQA0LjYuMC0xADQuMC0xADUuNC4xOC0xADYuMC4yAAAAAAAAAQAAAAEAAAAAAAAACDEt
MQAAAAAAZml4dHVyZS50eHQAL3Vzci9zaGFyZS9wYW5hbWEtcHJvdmVuYW5jZS1maXh0dXJlLwAt
TzIgLWZsdG89YXV0byAtZmZhdC1sdG8tb2JqZWN0cyAtZmV4Y2VwdGlvbnMgLWcgLWdyZWNvcmQt
Z2NjLXN3aXRjaGVzIC1waXBlIC1XYWxsIC1Xbm8tY29tcGxhaW4td3JvbmctbGFuZyAtV2Vycm9y
PWZvcm1hdC1zZWN1cml0eSAtV3AsLVVfRk9SVElGWV9TT1VSQ0UsLURfRk9SVElGWV9TT1VSQ0U9
MyAtV3AsLURfR0xJQkNYWF9BU1NFUlRJT05TIC1zcGVjcz0vdXNyL2xpYi9ycG0vcmVkaGF0L3Jl
ZGhhdC1oYXJkZW5lZC1jYzEgLWZzdGFjay1wcm90ZWN0b3Itc3Ryb25nIC1zcGVjcz0vdXNyL2xp
Yi9ycG0vcmVkaGF0L3JlZGhhdC1hbm5vYmluLWNjMSAgLW02NCAtbWFyY2g9eDg2LTY0IC1tdHVu
ZT1nZW5lcmljIC1mYXN5bmNocm9ub3VzLXVud2luZC10YWJsZXMgLWZzdGFjay1jbGFzaC1wcm90
ZWN0aW9uIC1mY2YtcHJvdGVjdGlvbiAtbXRscy1kaWFsZWN0PWdudTIgLWZuby1vbWl0LWZyYW1l
LXBvaW50ZXIgLW1uby1vbWl0LWxlYWYtZnJhbWUtcG9pbnRlcgBjcGlvAHpzdGQAMTkAbm9hcmNo
LXJlZGhhdC1saW51eC1nbnUAAAAAAAAAAAAAAEFTQ0lJIHRleHQAAAAAAAh1dGYtOABiZjZkYzg1
Y2I3NjI5MzhhYjM1NDY2ZDYyNWUzZWI0MzIxMzk4MjAxZWY5NDM5NDRhZDk0NzU5YjExMTY1Yjk5
AAAAAAAINDQ4YWY1YjI2NTgyODFlZGFhYTNmMGVlNDU5ZTk2MWVhZGYzNTEzZmI5MDBiOTQ0NDVi
YWZjNDUwZmQxYWY2NwAAAAA/AAAAB////NAAAAAQKLUv/QBofQMAkgUUGHBvDmCIWNNxsOnme3c1
JmpNprTcf+8pMqqqrTUmWRSEae6cAZ3ZfAvlNTMJcvCJHSFGycr11LGrhbIja6V17Xsg3Hgfw380
wR+ajfOLFw8MIACTzAMyvJyYeLFKvjdrmx/Z4OEGZKVOuIQR
@@ -0,0 +1 @@
tiny artifact
@@ -0,0 +1 @@
tiny artifacT
@@ -0,0 +1,118 @@
7avu2wMAAAAAAHBhbmFtYS1wcm92ZW5hbmNlLWZpeHR1cmUtMS0xAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAjq3oAQAAAAAAAAAHAAAQtAAA
AD4AAAAHAAAQpAAAABAAAAENAAAABgAAAAAAAAABAAABEQAAAAYAAAApAAAAAQAAA+gAAAAEAAAA
bAAAAAEAAAPsAAAABwAAAHAAAAAQAAAD7wAAAAQAAACAAAAAAQAAA/AAAAAHAAAAhAAAECAzNTU4
OTAwZjVlMDNmMDcxYTYzNWQ4NWY4MTgyNzUwN2I3YjFkNTA2ADI0OGU0N2MxYjY4ZWIzOTQ4MTFh
ZjViYThmZTRjYmUxM2JlMjBjOTdiNDMzNjMzNTBkYThhNjA2NGVkOGE5OGYAAAAAAAiZ9bUc5Ib0
/pUFYQ1fQtgpwwAAASQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAB////5AAAAAQAAAA
AI6t6AEAAAAAAAAAMwAABOEAAAA/AAAABwAABNEAAAAQAAAAZAAAAAgAAAAAAAAAAQAAA+gAAAAG
AAAAAgAAAAEAAAPpAAAABgAAABwAAAABAAAD6gAAAAYAAAAeAAAAAQAAA+wAAAAJAAAAIAAAAAEA
AAPtAAAACQAAAD8AAAABAAAD7gAAAAQAAABYAAAAAQAAA+8AAAAGAAAAXAAAAAEAAAPxAAAABAAA
AGgAAAABAAAD9gAAAAYAAABsAAAAAQAAA/gAAAAJAAAAcAAAAAEAAAP9AAAABgAAAHwAAAABAAAD
/gAAAAYAAACCAAAAAQAABAQAAAAEAAAAjAAAAAEAAAQGAAAAAwAAAJAAAAABAAAECQAAAAMAAACS
AAAAAQAABAoAAAAEAAAAlAAAAAEAAAQLAAAACAAAAJgAAAABAAAEDAAAAAgAAADZAAAAAQAABA0A
AAAEAAAA3AAAAAEAAAQPAAAACAAAAOAAAAABAAAEEAAAAAgAAADlAAAAAQAABBQAAAAGAAAA6gAA
AAEAAAQVAAAABAAAARAAAAABAAAEFwAAAAgAAAEUAAAAAQAABBgAAAAEAAABMAAAAAQAAAQZAAAA
CAAAAUAAAAAEAAAEGgAAAAgAAAGlAAAABAAABCgAAAAGAAABxAAAAAEAAARHAAAABAAAAcwAAAAB
AAAESAAAAAQAAAHQAAAAAQAABEkAAAAIAAAB1AAAAAEAAARYAAAABAAAAdgAAAABAAAEWQAAAAgA
AAHcAAAAAQAABFwAAAAEAAAB4AAAAAEAAARdAAAACAAAAeQAAAABAAAEXgAAAAgAAAHwAAAAAQAA
BGIAAAAGAAACFgAAAAEAAARkAAAABgAABAUAAAABAAAEZQAAAAYAAAQKAAAAAQAABGYAAAAGAAAE
DwAAAAEAAARsAAAABgAABBIAAAABAAAEdAAAAAQAAAQsAAAAAQAABHUAAAAEAAAEMAAAAAEAAAR2
AAAACAAABDQAAAABAAATkwAAAAQAAARAAAAAAQAAE8YAAAAGAAAERAAAAAEAABPkAAAACAAABEoA
AAABAAAT5QAAAAQAAASMAAAAAQAAE+kAAAAIAAAEkAAAAAFDAHBhbmFtYS1wcm92ZW5hbmNlLWZp
eHR1cmUAMQAxAFBhbmFtYSBwcm92ZW5hbmNlIHRlc3QgZml4dHVyZQBBIHRlc3Qtb25seSBSUE0g
Zml4dHVyZS4AapAEmmRlc2t0b3AtZ2liAAAAAAhNSVQAVW5zcGVjaWZpZWQAbGludXgAbm9hcmNo
AAAAAAAAAAiBpAAAapAEmmU4MGI3MWNkMTRkM2NiZDY1ZjQxNzNhYmNiZmNmMDFhNTQ1ZGJjYTMy
YTcyZDU3NTEwOGI1NTNhNjQ4Y2M5NmYAAAAAAAAAAHJvb3QAcm9vdABwYW5hbWEtcHJvdmVuYW5j
ZS1maXh0dXJlLTEtMS5zcmMucnBtAP////9wYW5hbWEtcHJvdmVuYW5jZS1maXh0dXJlAAAAAQAA
CgEAAAoBAAAKAQAACnJwbWxpYihDb21wcmVzc2VkRmlsZU5hbWVzKQBycG1saWIoRmlsZURpZ2Vz
dHMpAHJwbWxpYihQYXlsb2FkRmlsZXNIYXZlUHJlZml4KQBycG1saWIoUGF5bG9hZElzWnN0ZCkA
My4wLjQtMQA0LjYuMC0xADQuMC0xADUuNC4xOC0xADYuMC4yAAAAAAAAAQAAAAEAAAAAAAAACDEt
MQAAAAAAZml4dHVyZS50eHQAL3Vzci9zaGFyZS9wYW5hbWEtcHJvdmVuYW5jZS1maXh0dXJlLwAt
TzIgLWZsdG89YXV0byAtZmZhdC1sdG8tb2JqZWN0cyAtZmV4Y2VwdGlvbnMgLWcgLWdyZWNvcmQt
Z2NjLXN3aXRjaGVzIC1waXBlIC1XYWxsIC1Xbm8tY29tcGxhaW4td3JvbmctbGFuZyAtV2Vycm9y
PWZvcm1hdC1zZWN1cml0eSAtV3AsLVVfRk9SVElGWV9TT1VSQ0UsLURfRk9SVElGWV9TT1VSQ0U9
MyAtV3AsLURfR0xJQkNYWF9BU1NFUlRJT05TIC1zcGVjcz0vdXNyL2xpYi9ycG0vcmVkaGF0L3Jl
ZGhhdC1oYXJkZW5lZC1jYzEgLWZzdGFjay1wcm90ZWN0b3Itc3Ryb25nIC1zcGVjcz0vdXNyL2xp
Yi9ycG0vcmVkaGF0L3JlZGhhdC1hbm5vYmluLWNjMSAgLW02NCAtbWFyY2g9eDg2LTY0IC1tdHVu
ZT1nZW5lcmljIC1mYXN5bmNocm9ub3VzLXVud2luZC10YWJsZXMgLWZzdGFjay1jbGFzaC1wcm90
ZWN0aW9uIC1mY2YtcHJvdGVjdGlvbiAtbXRscy1kaWFsZWN0PWdudTIgLWZuby1vbWl0LWZyYW1l
LXBvaW50ZXIgLW1uby1vbWl0LWxlYWYtZnJhbWUtcG9pbnRlcgBjcGlvAHpzdGQAMTkAbm9hcmNo
LXJlZGhhdC1saW51eC1nbnUAAAAAAAAAAAAAAEFTQ0lJIHRleHQAAAAAAAh1dGYtOABiZjZkYzg1
Y2I3NjI5MzhhYjM1NDY2ZDYyNWUzZWI0MzIxMzk4MjAxZWY5NDM5NDRhZDk0NzU5YjExMTY1Yjk5
AAAAAAAINDQ4YWY1YjI2NTgyODFlZGFhYTNmMGVlNDU5ZTk2MWVhZGYzNTEzZmI5MDBiOTQ0NDVi
YWZjNDUwZmQxYWY2NwAAAAA/AAAAB////NAAAAAQKLUv/QBofQMAkgUUGHBvDmCIWNNxsOnme3c1
JmpNprTcf+8pMqqqrTUmWRSEae6cAZ3ZfAvlNTMJcvCJHSFGycr11LGrhbIja6V17Xsg3Hgfw380
wR+ajfOLFw8MIACTzAMyvJyYeLFKvjdrmx/Z4OEGZKVOuIQR
@@ -0,0 +1,11 @@
-----BEGIN PGP SIGNATURE-----
iQEzBAABCgAdFiEE8NsXb+IUusO1vCd8zouSo0jXorAFAmqQCQ8ACgkQzouSo0jX
orCpQAf+PB6BsCoq/Sp4LShSNTlZkDWS7p641A29OgAtr6QYUphMsI+gY6tKK0zr
zmDzs4MyZaSy/yx9jEdPyyVWLFREk0u/x6YpQwj8KGbtuTt/hBQgkshDZZeVdGLr
x11bkZgOENjzRS/NnKiziwoVVwaZm36WrsCef6Q26sbxCnYF2nWlGNtP9SKnhxSV
/FYXi/wW41p8iZqIb0SFRoMGMsazV5i9XOjTLx4dEiMaeGvnNdZoMDGIO8in7mJG
pkYro8coAGwjyC48VZmCPFG0TPqVhc0LEYKcI4M1OyEX38xQqaoRm5k9sOswfyDF
iTFTwna0Zb1nurK/28/WfUHqLEIlQw==
=PvFe
-----END PGP SIGNATURE-----
@@ -0,0 +1,118 @@
7avu2wMAAAAAAHBhbmFtYS1wcm92ZW5hbmNlLWZpeHR1cmUtMS0xAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAjq3oAQAAAAAAAAAIAAAQpAAA
AD4AAAAHAAAQlAAAABAAAAEMAAAABwAAAAAAAAE2AAABDQAAAAYAAAE2AAAAAQAAAREAAAAGAAAB
XwAAAAEAAAPoAAAABAAAAaAAAAABAAAD7AAAAAcAAAGkAAAAEAAAA+8AAAAEAAABtAAAAAEAAAPw
AAAABwAAAbgAAA7ciQEzBAABCgAdFiEE8NsXb+IUusO1vCd8zouSo0jXorAFAmqQCOUACgkQzouS
o0jXorAkrwf/diXYa/W3A1v8HNykf8NA2gmyNHVIm/AMGGZE1TC49eiMgj1+KpoznyImuP5bpGSC
fMeG0sMrRabG4eYJ7eXDNviVNDslet6ooNRlvRE0F4zQZERub1l4afHHuJuHWHi/J/UpWotOxQa0
akXy7/fE4Easoy8ddzdx5IYS9SLDiKMxuVhp3oqkiC1QyX6gV3cd+iF128IJMhhA2UMnA7nRX1vH
sDK1WNQub3GeS9svYVCXfSy4NTIbsP6zynLRFp3nEKfRkVWILnrGGHL7ZBNH4aaURUN57lu83Cv+
DOB9LlS2z704feRKJRWj+wLmOZvx37426Tr7ZllLHvs5xvkH0DM1NTg5MDBmNWUwM2YwNzFhNjM1
ZDg1ZjgxODI3NTA3YjdiMWQ1MDYAMjQ4ZTQ3YzFiNjhlYjM5NDgxMWFmNWJhOGZlNGNiZTEzYmUy
MGM5N2I0MzM2MzM1MGRhOGE2MDY0ZWQ4YTk4ZgAAAAiZ9bUc5Ib0/pUFYQ1fQtgpwwAAASQAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAB////4AAAAAQAAAA
AI6t6AEAAAAAAAAAMwAABOEAAAA/AAAABwAABNEAAAAQAAAAZAAAAAgAAAAAAAAAAQAAA+gAAAAG
AAAAAgAAAAEAAAPpAAAABgAAABwAAAABAAAD6gAAAAYAAAAeAAAAAQAAA+wAAAAJAAAAIAAAAAEA
AAPtAAAACQAAAD8AAAABAAAD7gAAAAQAAABYAAAAAQAAA+8AAAAGAAAAXAAAAAEAAAPxAAAABAAA
AGgAAAABAAAD9gAAAAYAAABsAAAAAQAAA/gAAAAJAAAAcAAAAAEAAAP9AAAABgAAAHwAAAABAAAD
/gAAAAYAAACCAAAAAQAABAQAAAAEAAAAjAAAAAEAAAQGAAAAAwAAAJAAAAABAAAECQAAAAMAAACS
AAAAAQAABAoAAAAEAAAAlAAAAAEAAAQLAAAACAAAAJgAAAABAAAEDAAAAAgAAADZAAAAAQAABA0A
AAAEAAAA3AAAAAEAAAQPAAAACAAAAOAAAAABAAAEEAAAAAgAAADlAAAAAQAABBQAAAAGAAAA6gAA
AAEAAAQVAAAABAAAARAAAAABAAAEFwAAAAgAAAEUAAAAAQAABBgAAAAEAAABMAAAAAQAAAQZAAAA
CAAAAUAAAAAEAAAEGgAAAAgAAAGlAAAABAAABCgAAAAGAAABxAAAAAEAAARHAAAABAAAAcwAAAAB
AAAESAAAAAQAAAHQAAAAAQAABEkAAAAIAAAB1AAAAAEAAARYAAAABAAAAdgAAAABAAAEWQAAAAgA
AAHcAAAAAQAABFwAAAAEAAAB4AAAAAEAAARdAAAACAAAAeQAAAABAAAEXgAAAAgAAAHwAAAAAQAA
BGIAAAAGAAACFgAAAAEAAARkAAAABgAABAUAAAABAAAEZQAAAAYAAAQKAAAAAQAABGYAAAAGAAAE
DwAAAAEAAARsAAAABgAABBIAAAABAAAEdAAAAAQAAAQsAAAAAQAABHUAAAAEAAAEMAAAAAEAAAR2
AAAACAAABDQAAAABAAATkwAAAAQAAARAAAAAAQAAE8YAAAAGAAAERAAAAAEAABPkAAAACAAABEoA
AAABAAAT5QAAAAQAAASMAAAAAQAAE+kAAAAIAAAEkAAAAAFDAHBhbmFtYS1wcm92ZW5hbmNlLWZp
eHR1cmUAMQAxAFBhbmFtYSBwcm92ZW5hbmNlIHRlc3QgZml4dHVyZQBBIHRlc3Qtb25seSBSUE0g
Zml4dHVyZS4AapAEmmRlc2t0b3AtZ2liAAAAAAhNSVQAVW5zcGVjaWZpZWQAbGludXgAbm9hcmNo
AAAAAAAAAAiBpAAAapAEmmU4MGI3MWNkMTRkM2NiZDY1ZjQxNzNhYmNiZmNmMDFhNTQ1ZGJjYTMy
YTcyZDU3NTEwOGI1NTNhNjQ4Y2M5NmYAAAAAAAAAAHJvb3QAcm9vdABwYW5hbWEtcHJvdmVuYW5j
ZS1maXh0dXJlLTEtMS5zcmMucnBtAP////9wYW5hbWEtcHJvdmVuYW5jZS1maXh0dXJlAAAAAQAA
CgEAAAoBAAAKAQAACnJwbWxpYihDb21wcmVzc2VkRmlsZU5hbWVzKQBycG1saWIoRmlsZURpZ2Vz
dHMpAHJwbWxpYihQYXlsb2FkRmlsZXNIYXZlUHJlZml4KQBycG1saWIoUGF5bG9hZElzWnN0ZCkA
My4wLjQtMQA0LjYuMC0xADQuMC0xADUuNC4xOC0xADYuMC4yAAAAAAAAAQAAAAEAAAAAAAAACDEt
MQAAAAAAZml4dHVyZS50eHQAL3Vzci9zaGFyZS9wYW5hbWEtcHJvdmVuYW5jZS1maXh0dXJlLwAt
TzIgLWZsdG89YXV0byAtZmZhdC1sdG8tb2JqZWN0cyAtZmV4Y2VwdGlvbnMgLWcgLWdyZWNvcmQt
Z2NjLXN3aXRjaGVzIC1waXBlIC1XYWxsIC1Xbm8tY29tcGxhaW4td3JvbmctbGFuZyAtV2Vycm9y
PWZvcm1hdC1zZWN1cml0eSAtV3AsLVVfRk9SVElGWV9TT1VSQ0UsLURfRk9SVElGWV9TT1VSQ0U9
MyAtV3AsLURfR0xJQkNYWF9BU1NFUlRJT05TIC1zcGVjcz0vdXNyL2xpYi9ycG0vcmVkaGF0L3Jl
ZGhhdC1oYXJkZW5lZC1jYzEgLWZzdGFjay1wcm90ZWN0b3Itc3Ryb25nIC1zcGVjcz0vdXNyL2xp
Yi9ycG0vcmVkaGF0L3JlZGhhdC1hbm5vYmluLWNjMSAgLW02NCAtbWFyY2g9eDg2LTY0IC1tdHVu
ZT1nZW5lcmljIC1mYXN5bmNocm9ub3VzLXVud2luZC10YWJsZXMgLWZzdGFjay1jbGFzaC1wcm90
ZWN0aW9uIC1mY2YtcHJvdGVjdGlvbiAtbXRscy1kaWFsZWN0PWdudTIgLWZuby1vbWl0LWZyYW1l
LXBvaW50ZXIgLW1uby1vbWl0LWxlYWYtZnJhbWUtcG9pbnRlcgBjcGlvAHpzdGQAMTkAbm9hcmNo
LXJlZGhhdC1saW51eC1nbnUAAAAAAAAAAAAAAEFTQ0lJIHRleHQAAAAAAAh1dGYtOABiZjZkYzg1
Y2I3NjI5MzhhYjM1NDY2ZDYyNWUzZWI0MzIxMzk4MjAxZWY5NDM5NDRhZDk0NzU5YjExMTY1Yjk5
AAAAAAAINDQ4YWY1YjI2NTgyODFlZGFhYTNmMGVlNDU5ZTk2MWVhZGYzNTEzZmI5MDBiOTQ0NDVi
YWZjNDUwZmQxYWY2NwAAAAA/AAAAB////NAAAAAQKLUv/QBofQMAkgUUGHBvDmCIWNNxsOnme3c1
JmpNprTcf+8pMqqqrTUmWRSEae6cAZ3ZfAvlNTMJcvCJHSFGycr11LGrhbIja6V17Xsg3Hgfw380
wR+ajfOLFw8MIACTzAMyvJyYeLFKvjdrmx/Z4OEGZKVOuIQR
@@ -0,0 +1,18 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBGqQCOUBCADHP/S269aT5t0ZvePSetTa1h2KtcwiQ+LEkWMvHM8sYCrHDjer
XR53nj+e9xZPmXtEIiYsE1LpR7ax8dwcm/1XDqFLeduCRkB6DQE4Q5Ia1u9EXpjc
Of+uiw0Y2n1/d/BuRVOJiORlFMlW+0lueMaShHfH+/g2QC9oQdIMZfk+UaoRRgVf
5hpRfpIZGKOnAEatTRRbjDaoibIe0/El8Pwt93RoCO9GxF+UeJZZA152TOrB3Y/6
uTe7FE5HZFXQEvR78GhrAzmwZENuMIaIIaHhzOrLKrxoVtgimHymLe1rODQfq6zG
jRZ8SDJ2oLB1u51MiirV2fc18gUUPTbOSfv7ABEBAAG0KlBhbmFtYSBXcm9uZyBT
aWduZXIgPHdyb25nQHBhbmFtYS5pbnZhbGlkPokBUQQTAQoAOxYhBPDbF2/iFLrD
tbwnfM6LkqNI16KwBQJqkAjlAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheA
AAoJEM6LkqNI16KwbwgH/1yMS9PB7KQHLE+kQqaJQxqOLyANx4EPn8NeXB19XByz
Zph/qzFjnGXT/Lz2PMwqo4uem6oXYSdwXlsVM6oLeQ8wDd29/oLbI77cYBO42QSL
cerjc+IbJ7BCiqGL0e5Gmn8lzWinO+wramnVtDKQn4BzDCj7UWpBzyClqVb06IfL
9Xz4KMSv3vDGVMhtLLgVmfRkdgdgS1mcgQhSaueQmjU1Z4zQ8dHbL1cefuvxPKzM
XgkKBIIaY+2X1ulz/QA7fe4JpqNzyRC0gixod72OXldFA6aNV/EfF+1Pa/nVPNYC
MQl6tgcI8JX9lnsMxK+h9mAIilFQUmdX1Qio4FjWYLI=
=l5kF
-----END PGP PUBLIC KEY BLOCK-----
+4 -1
View File
@@ -103,10 +103,13 @@ fi
nvidia="$(run_stage PANAMA_NVIDIA=yes)" nvidia="$(run_stage PANAMA_NVIDIA=yes)"
called "$nvidia" 'dnf install -y akmod-nvidia' \ called "$nvidia" 'akmod-nvidia' \
|| note 'answering yes to NVIDIA does not install akmod-nvidia' || note 'answering yes to NVIDIA does not install akmod-nvidia'
called "$nvidia" 'xorg-x11-drv-nvidia-cuda' \ called "$nvidia" 'xorg-x11-drv-nvidia-cuda' \
|| note 'the CUDA driver is not installed alongside the kernel module' || note 'the CUDA driver is not installed alongside the kernel module'
expected_nvidia='sudo dnf install -y --repo=fedora --repo=updates --repo=rpmfusion-free --repo=rpmfusion-free-updates --repo=rpmfusion-nonfree --repo=rpmfusion-nonfree-updates --from-repo=rpmfusion-nonfree,rpmfusion-nonfree-updates akmod-nvidia xorg-x11-drv-nvidia-cuda'
grep -Fxq -- "$expected_nvidia" <<<"$nvidia" \
|| note 'the NVIDIA transaction is not limited to reviewed Fedora and RPM Fusion repositories'
called "$nvidia" 'grubby --update-kernel=ALL' \ called "$nvidia" 'grubby --update-kernel=ALL' \
|| note 'the kernel arguments are never set' || note 'the kernel arguments are never set'
called "$nvidia" 'modprobe.blacklist=nouveau' \ called "$nvidia" 'modprobe.blacklist=nouveau' \
+177 -1
View File
@@ -55,6 +55,7 @@ fi
# ── The extension ──────────────────────────────────────────────────────────── # ── The extension ────────────────────────────────────────────────────────────
manifest="$extension/package.json" manifest="$extension/package.json"
lockfile="$extension/package-lock.json"
if [[ ! -f "$manifest" ]]; then if [[ ! -f "$manifest" ]]; then
note 'the panama-search extension has no manifest' note 'the panama-search extension has no manifest'
else else
@@ -76,6 +77,15 @@ else
fi fi
fi fi
if [[ ! -f "$lockfile" ]]; then
note 'the panama-search extension has no package-lock.json, so npm ci cannot install a fresh clone'
elif git -C "$repo_dir" check-ignore -q "$lockfile" 2>/dev/null; then
note 'the panama-search package-lock.json is ignored, so a fresh clone cannot use npm ci'
elif ! git -C "$repo_dir" ls-files --error-unmatch -- \
'config/local/share/vicinae/extensions/panama-search/package-lock.json' >/dev/null 2>&1; then
note 'the panama-search package-lock.json is not tracked, so a fresh clone cannot use npm ci'
fi
# ── One engine, written twice ──────────────────────────────────────────────── # ── One engine, written twice ────────────────────────────────────────────────
# #
# The script command and the extension both have to know where a search goes. # The script command and the extension both have to know where a search goes.
@@ -113,9 +123,175 @@ grep -q '/etc/profile.d/nvm.sh' "$stage" \
|| note 'the extension build never sources nvm, so npm is missing on any machine without a system node' || note 'the extension build never sources nvm, so npm is missing on any machine without a system node'
# node_modules is a dependency tree, not configuration. # node_modules is a dependency tree, not configuration.
git -C "$repo_dir" check-ignore -q "$extension/node_modules" 2>/dev/null \ git -C "$repo_dir" check-ignore -q "$extension/node_modules/" 2>/dev/null \
|| note 'the extension node_modules is not gitignored' || note 'the extension node_modules is not gitignored'
# npm must honour the committed dependency graph. This disposable fixture
# simulates npm rejecting a mismatched lockfile, which must leave that lockfile
# untouched and keep the launcher stage nonfatal.
fixture_root="$(mktemp -d -t panama-vicinae-lock.XXXXXX)"
trap 'rm -rf -- "$fixture_root"' EXIT
mkdir -p "$fixture_root/config/local/share/vicinae/scripts" \
"$fixture_root/config/local/share/vicinae/extensions/panama-search/src" \
"$fixture_root/bin"
fixture_extension="$fixture_root/config/local/share/vicinae/extensions/panama-search"
cp -- "$manifest" "$fixture_extension/package.json"
cp -- "$lockfile" "$fixture_extension/package-lock.json"
cmp -s -- "$lockfile" "$fixture_extension/package-lock.json" \
|| note 'the fresh-clone fixture did not consume the repository package-lock.json'
sed -i 's/"dependencies": {/"dependencies": {"fixture-mismatch": "1.0.0",/' \
"$fixture_extension/package.json"
lockfile="$fixture_extension/package-lock.json"
lock_before="$fixture_root/package-lock.before"
cp -- "$lockfile" "$lock_before"
repository_lock_sha256="$(sha256sum -- "$lock_before" | awk '{ print $1 }')"
cat >"$fixture_root/bin/npm" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"${NPM_LOG:?}"
[[ "${1:-}" == ci ]] || exit 64
[[ "$(sha256sum -- package-lock.json | awk '{ print $1 }')" == "${NPM_EXPECTED_LOCK_SHA256:?}" ]] || exit 65
exit 1
EOF
chmod +x "$fixture_root/bin/npm"
stage_status=0
stage_output="$(PATH="$fixture_root/bin:$PATH" PANAMA_PATH="$fixture_root" \
VICINAE_DATA_DIR="$fixture_root/vicinae-data" NPM_LOG="$fixture_root/npm.log" \
NPM_EXPECTED_LOCK_SHA256="$repository_lock_sha256" \
bash "$stage" 2>&1)" || stage_status=$?
[[ "$stage_status" -eq 0 ]] \
|| note "the Vicinae stage returned $stage_status for a lockfile mismatch instead of remaining nonfatal"
[[ "$(<"$fixture_root/npm.log")" == 'ci --silent' ]] \
|| note 'the Vicinae extension dependency command was not npm ci --silent'
[[ "$stage_output" == *'Vicinae extension panama-search did not build; skipping'* ]] \
|| note 'a nonzero Vicinae extension dependency install did not fail the extension build'
cmp -s -- "$lock_before" "$lockfile" \
|| note 'a rejected Vicinae lockfile mismatch changed package-lock.json'
# Successful builds carry a digest receipt over both manifests and every
# source file. Directory mtimes do not change when an existing file is edited,
# so each byte class must independently invalidate the build.
digest_root="$fixture_root/digest"
digest_extension="$digest_root/config/local/share/vicinae/extensions/panama-search"
digest_data="$digest_root/vicinae-data"
mkdir -p "$digest_root/config/local/share/vicinae/scripts" \
"$digest_extension/src" "$digest_extension/assets" "$digest_root/bin"
cp -- "$manifest" "$digest_extension/package.json"
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/package-lock.json" \
"$digest_extension/package-lock.json"
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/src/search.tsx" \
"$digest_extension/src/search.tsx"
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/tsconfig.json" \
"$digest_extension/tsconfig.json"
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/assets/extension_icon.svg" \
"$digest_extension/assets/extension_icon.svg"
cat >"$digest_root/bin/npm" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"${NPM_LOG:?}"
case "${1:-}:${2:-}" in
ci:--silent) exit 0 ;;
run:build)
mkdir -p "$VICINAE_DATA_DIR/extensions/$(basename "$PWD")"
printf 'built\n' >"$VICINAE_DATA_DIR/extensions/$(basename "$PWD")/bundle"
;;
*) exit 64 ;;
esac
EOF
cat >"$digest_root/bin/find" <<'EOF'
#!/usr/bin/env bash
set -uo pipefail
status=0
/usr/bin/find "$@" || status=$?
[[ "${STUB_FIND_FAIL:-0}" != 1 ]] || exit 74
exit "$status"
EOF
chmod +x "$digest_root/bin/npm" "$digest_root/bin/find"
run_digest_stage() {
: >"$digest_root/npm.log"
PATH="$digest_root/bin:$PATH" PANAMA_PATH="$digest_root" \
VICINAE_DATA_DIR="$digest_data" NPM_LOG="$digest_root/npm.log" \
STUB_FIND_FAIL="${STUB_FIND_FAIL:-0}" \
bash "$stage" >"$digest_root/stage.out" 2>&1
}
run_digest_stage || note 'the Vicinae digest fixture initial build failed'
cmp -s <(printf 'ci --silent\nrun build\n') "$digest_root/npm.log" \
|| note 'the Vicinae digest fixture did not perform its initial locked build'
run_digest_stage || note 'the unchanged Vicinae digest fixture failed'
[[ ! -s "$digest_root/npm.log" ]] \
|| note 'an unchanged Vicinae extension rebuilt despite its matching receipt'
for digest_input in src/search.tsx package.json package-lock.json tsconfig.json \
assets/extension_icon.svg; do
printf '\n// digest mutation: %s\n' "$digest_input" >>"$digest_extension/$digest_input"
run_digest_stage || note "the Vicinae digest fixture failed after changing $digest_input"
cmp -s <(printf 'ci --silent\nrun build\n') "$digest_root/npm.log" \
|| note "changing existing $digest_input bytes did not rebuild the Vicinae extension"
done
# A traversal can emit valid-looking partial output and still fail. Sorting
# that output must not hide find's producer status or replace the successful
# build receipt with a digest over an incomplete source tree.
digest_receipt="$digest_data/extensions/panama-search/.panama-source-sha256"
cp -- "$digest_receipt" "$digest_root/receipt.before-find-failure"
STUB_FIND_FAIL=1 run_digest_stage \
|| note 'the Vicinae stage made a digest traversal failure fatal'
[[ ! -s "$digest_root/npm.log" ]] \
|| note 'a failed Vicinae digest traversal still rebuilt the extension'
grep -q 'inputs could not be verified; skipping' "$digest_root/stage.out" \
|| note 'a failed Vicinae digest traversal was accepted as verified input'
cmp -s -- "$digest_root/receipt.before-find-failure" "$digest_receipt" \
|| note 'a failed Vicinae digest traversal replaced the successful receipt'
# Helper writes run in conditional contexts in production, where Bash disables
# implicit errexit inside the whole function. Each producer therefore has to
# return its own write/publication failure and remove its temporary receipt.
vicinae_helpers="$digest_root/vicinae-helpers"
sed '/^panama_path=/,$d' "$stage" >"$vicinae_helpers"
: >"$digest_root/empty-inputs"
mkdir "$digest_root/manifest-output-directory"
manifest_status=0
bash -c 'source "$1"; set +e; _write_vicinae_manifest "$2" "$3" "$4"' bash \
"$vicinae_helpers" "$digest_extension" "$digest_root/empty-inputs" \
"$digest_root/manifest-output-directory" >/dev/null 2>&1 \
|| manifest_status=$?
[[ "$manifest_status" -ne 0 ]] \
|| note 'a failed Vicinae manifest initialization returned success'
receipt_failure_root="$digest_root/receipt-publication-failure"
mkdir -p "$receipt_failure_root/built" "$receipt_failure_root/bin"
printf 'prior receipt\n' >"$receipt_failure_root/built/.panama-source-sha256"
cat >"$receipt_failure_root/bin/mv" <<'EOF'
#!/usr/bin/env bash
destination="${!#}"
[[ "$destination" != */.panama-source-sha256 ]] || exit 75
exec /usr/bin/mv "$@"
EOF
chmod +x "$receipt_failure_root/bin/mv"
receipt_status=0
PATH="$receipt_failure_root/bin:$PATH" bash -c \
'source "$1"; set +e; _record_vicinae_digest "$2" "$3"' bash \
"$vicinae_helpers" "$receipt_failure_root/built" "$(printf 'a%.0s' {1..64})" \
>/dev/null 2>&1 || receipt_status=$?
[[ "$receipt_status" -ne 0 ]] \
|| note 'a failed Vicinae receipt publication returned success'
cmp -s <(printf 'prior receipt\n') \
"$receipt_failure_root/built/.panama-source-sha256" \
|| note 'a failed Vicinae receipt publication replaced the prior receipt'
[[ -z "$(find "$receipt_failure_root/built" \
-name '.panama-source-sha256.*' -print -quit)" ]] \
|| note 'a failed Vicinae receipt publication left a temporary receipt'
# Prove the directory-only ignore rule in a repository where node_modules does
# not already exist. The trailing slash is part of the query contract.
ignore_root="$fixture_root/ignore-repository"
mkdir -p "$ignore_root/config/local/share/vicinae/extensions/panama-search"
cp -- "$repo_dir/.gitignore" "$ignore_root/.gitignore"
git -C "$ignore_root" init -q
git -C "$ignore_root" check-ignore -q \
'config/local/share/vicinae/extensions/panama-search/node_modules/' \
|| note 'a fresh clone with no node_modules directory does not match the ignore rule'
# ── Report ─────────────────────────────────────────────────────────────────── # ── Report ───────────────────────────────────────────────────────────────────
if (( ${#findings[@]} > 0 )); then if (( ${#findings[@]} > 0 )); then
File diff suppressed because it is too large Load Diff
+231
View File
@@ -19,6 +19,157 @@ panama="$repo_dir/bin/panama"
findings=() findings=()
note() { findings+=("$1"); } note() { findings+=("$1"); }
work="$(mktemp -d)"
trap 'rm -rf -- "$work"' EXIT
# ── Verified bootstrap command ───────────────────────────────────────────────
if grep -qE 'bash[[:space:]]+<\(curl[^)]*/raw/branch/main/boot' "$readme"; then
note 'the README still executes the mutable main-branch bootstrap'
fi
if grep -q '/raw/branch/main/boot' "$readme"; then
note 'the README still names the mutable main-branch boot URL'
fi
require_bootstrap_occurrences() {
local pattern="$1" expected="$2" explanation="$3" actual
actual="$(grep -cE -- "$pattern" "$readme")"
(( actual == expected )) || note "$explanation"
}
mapfile -t documented_commits < <(
sed -nE "s/^bootstrap_commit=['\"]?([0-9a-f]{40})['\"]?$/\1/p" "$readme" | sort -u
)
mapfile -t documented_shas < <(
sed -nE "s/^bootstrap_sha=['\"]?([0-9a-f]{64})['\"]?$/\1/p" "$readme" | sort -u
)
if (( ${#documented_commits[@]} != 1 )); then
note 'the README does not declare one full lowercase 40-hex bootstrap commit'
else
documented_commit="${documented_commits[0]}"
if ! git -C "$repo_dir" cat-file -e "$documented_commit^{commit}" 2>/dev/null; then
note 'the documented bootstrap commit does not resolve to a repository commit'
fi
fi
if (( ${#documented_shas[@]} != 1 )); then
note 'the README does not declare one lowercase 64-hex bootstrap SHA-256'
else
documented_sha="${documented_shas[0]}"
fi
if [[ -n "${documented_commit:-}" && -n "${documented_sha:-}" ]] \
&& [[ "$(git -C "$repo_dir" show "$documented_commit:boot" 2>/dev/null | sha256sum | cut -d' ' -f1)" != "$documented_sha" ]]; then
note 'the documented SHA-256 does not match boot in the documented commit'
fi
require_bootstrap_occurrences '^bootstrap_commit=[0-9a-f]{40}$' 2 \
'the desktop and server commands do not declare the same full bootstrap commit'
require_bootstrap_occurrences '^bootstrap_sha=[0-9a-f]{64}$' 2 \
'the desktop and server commands do not declare the same full bootstrap SHA-256'
require_bootstrap_occurrences 'https://git\.gbrown\.org/gib/Panama/raw/commit/\$bootstrap_commit/boot' 2 \
'the desktop and server commands do not both use the commit-addressed boot URL'
require_bootstrap_occurrences '--connect-timeout 10' 2 \
'the desktop and server commands do not both use the 10-second connect timeout'
require_bootstrap_occurrences '--max-time 30' 2 \
'the desktop and server commands do not both use the 30-second total timeout'
require_bootstrap_occurrences '--max-filesize 262144' 2 \
'the desktop and server commands do not both use the 256 KiB response limit'
require_bootstrap_occurrences 'mktemp[[:space:]]+-d' 2 \
'the desktop and server commands do not both use a private temporary directory'
require_bootstrap_occurrences 'sha256sum[[:space:]]+-c' 2 \
'the desktop and server commands do not both verify with sha256sum -c'
require_bootstrap_occurrences 'PANAMA_BOOT_REVISION="?\$bootstrap_commit"?[[:space:]]+PANAMA_BOOT_SHA256="?\$bootstrap_sha"?' 2 \
'the desktop and server commands do not both pass the verified pins to boot'
grep -qE 'bash[[:space:]]+"?\$bootstrap"?([[:space:]]|$)' "$readme" \
|| note 'the README bootstrap does not execute the verified temporary file'
grep -qE 'bash[[:space:]]+"?\$bootstrap"?[[:space:]]+--server' "$readme" \
|| note 'the server bootstrap does not reuse the verified temporary file'
if grep -qE 'curl[^|]*\|[[:space:]]*(bash|sh)|bash[[:space:]]+<\(curl' "$readme"; then
note 'the README pipes a network response into a shell'
fi
# Run the two exact documented blocks with a successful download and a failing
# checksum. The Bash adapter records only the verified boot invocation; the
# contract itself uses /usr/bin/bash so the adapter cannot hide this behavior.
checksum_stub_dir="$work/checksum-bin"
checksum_boot_calls="$work/checksum-boot-calls"
mkdir -p "$checksum_stub_dir"
cat >"$checksum_stub_dir/curl" <<'STUB'
#!/usr/bin/bash
set -u
destination=""
while (( $# > 0 )); do
case "$1" in
--output)
destination="${2:-}"
shift 2
;;
*) shift ;;
esac
done
[[ -n "$destination" ]] || exit 97
printf 'tampered boot bytes\n' >"$destination"
STUB
chmod +x "$checksum_stub_dir/curl"
cat >"$checksum_stub_dir/sha256sum" <<'STUB'
#!/usr/bin/bash
[[ "${1:-}" == -c ]] || exit 97
exit 1
STUB
chmod +x "$checksum_stub_dir/sha256sum"
cat >"$checksum_stub_dir/bash" <<'STUB'
#!/usr/bin/bash
printf 'verified-boot %s\n' "$*" >>"$PANAMA_README_BOOT_CALLS"
exit 0
STUB
chmod +x "$checksum_stub_dir/bash"
mapfile -d $'\036' -t bootstrap_snippets < <(
awk '
/^```sh$/ { in_block = 1; block = ""; next }
/^```$/ && in_block {
if (block ~ /bootstrap_commit=/) printf "%s%c", block, 30
in_block = 0
next
}
in_block { block = block $0 "\n" }
' "$readme"
)
checksum_failure_stops_boot() {
local snippet="$1" status
: >"$checksum_boot_calls"
PATH="$checksum_stub_dir:/usr/bin:/bin" \
PANAMA_README_BOOT_CALLS="$checksum_boot_calls" \
/usr/bin/bash -c "$snippet" >/dev/null 2>&1
status=$?
(( status != 0 )) && [[ ! -s "$checksum_boot_calls" ]]
}
if (( ${#bootstrap_snippets[@]} != 2 )); then
note 'the README does not contain exactly two executable verified bootstrap blocks'
else
bootstrap_labels=(desktop server)
for index in "${!bootstrap_snippets[@]}"; do
snippet="${bootstrap_snippets[$index]}"
label="${bootstrap_labels[$index]}"
if ! checksum_failure_stops_boot "$snippet"; then
note "the $label command invoked boot after checksum failure"
fi
weakened_snippet="${snippet//$'set -euo pipefail\n'/}"
if [[ "$weakened_snippet" == "$snippet" ]]; then
note "the $label command has no fail-closed shell control to test"
elif checksum_failure_stops_boot "$weakened_snippet"; then
note "the $label checksum assertion accepts removal of fail-closed shell control"
fi
done
fi
# ── The contract count ─────────────────────────────────────────────────────── # ── The contract count ───────────────────────────────────────────────────────
# #
@@ -36,6 +187,86 @@ elif (( claimed != actual )); then
note "the README says $claimed contracts; there are $actual" note "the README says $claimed contracts; there are $actual"
fi fi
# ── Root server bootstrap ───────────────────────────────────────────────────
#
# This path runs before the repository exists on a fresh VPS. Its safety
# properties need to be stated beside the public `boot --server` example, not
# inferred from the shell implementation or buried in a fixture.
bootstrap_doc="$(sed -n '/^That command also works from a brand-new VPS/,/^`install` asks/p' "$readme" | tr '\n' ' ')"
require_bootstrap_doc() {
local pattern="$1" explanation="$2"
grep -qiE "$pattern" <<<"$bootstrap_doc" || note "$explanation"
}
assert_bootstrap_probe_rejected() {
local name="$1" pattern="$2" weakened_doc="$3"
if grep -qiE "$pattern" <<<"$weakened_doc"; then
note "the $name assertion accepts its weakened documentation probe"
fi
}
target_key_requirement='target user owns[^.]*\.ssh[^.]*mode[^.]*([^0-9]|^)0700([^0-9]|$)[^.]*authorized_keys[^.]*mode[^.]*([^0-9]|^)0600([^0-9]|$)'
parseable_key_requirement='(every|each)[^.]*non-?comment[^.]*authorized_keys[^.]*(OpenSSH|ssh-keygen)[^.]*(parse|valid)|(OpenSSH|ssh-keygen)[^-]*parse[^.]*every[^.]*non-?comment'
hardening_continues_requirement='hardening[[:space:]]+is[[:space:]]+unavailable[^.]*without[^.]*verified[^.]*key[^.]*install[[:space:]]+continues[^.]*without[[:space:]]+(it|SSH[[:space:]]+hardening)'
atomic_dropin_requirement='atomic[[:space:]]+same-directory[[:space:]]+drop-in'
rollback_requirement='validation[^.]*reload[^.]*fail[^.]*(restor|rollback)[^.]*previous[[:space:]]+drop-in'
effective_policy_requirement='sshd -T[^.]*root[^.]*target|sshd -T[^.]*target[^.]*root'
require_bootstrap_doc "$target_key_requirement" \
'the root bootstrap docs do not require target-user ownership with exact 0700/0600 SSH modes'
require_bootstrap_doc "$parseable_key_requirement" \
'the root bootstrap docs do not require OpenSSH to parse every non-comment key entry'
require_bootstrap_doc "$hardening_continues_requirement" \
'the root bootstrap docs do not say bootstrap continues without unavailable SSH hardening'
require_bootstrap_doc 'sshd -t' \
'the root bootstrap docs do not name sshd -t validation'
require_bootstrap_doc "$effective_policy_requirement" \
'the root bootstrap docs do not name sshd -T checks for root and target contexts'
require_bootstrap_doc "$atomic_dropin_requirement" \
'the root bootstrap docs do not describe the atomic same-directory drop-in'
require_bootstrap_doc '00-panama\.conf' \
'the root bootstrap docs do not name the precedence-safe 00-panama.conf drop-in'
require_bootstrap_doc 'PermitRootLogin[^.]*no[^.]*PasswordAuthentication[^.]*no[^.]*KbdInteractiveAuthentication[^.]*no' \
'the root bootstrap docs do not state all three effective authentication denials'
require_bootstrap_doc 'detected (SSH )?unit.*reload|reload.*detected (SSH )?unit' \
'the root bootstrap docs do not describe reloading the detected SSH unit'
require_bootstrap_doc "$rollback_requirement" \
'the root bootstrap docs do not promise rollback on validation or reload failure'
require_bootstrap_doc 'fixture contracts.*(these|this) (path|branch)|fixture contracts.*test' \
'the root bootstrap docs do not limit proof to fixture contracts'
require_bootstrap_doc 'no real daemon reload.*panama test --safe|panama test --safe.*no real daemon reload' \
'the root bootstrap docs imply a live daemon reload under the safe suite'
# These prove the semantic assertions above reject the precise omissions they
# guard against. They mutate only the scoped documentation string; README.md
# itself remains the real input that must satisfy the contract.
assert_bootstrap_probe_rejected 'exact SSH modes' "$target_key_requirement" \
"${bootstrap_doc//0700/700}"
assert_bootstrap_probe_rejected 'target-user ownership' "$target_key_requirement" \
"${bootstrap_doc//target user owns/someone owns}"
weakened_key_doc="${bootstrap_doc//OpenSSH/text tooling}"
weakened_key_doc="${weakened_key_doc//ssh-keygen/text parser}"
assert_bootstrap_probe_rejected 'OpenSSH key parsing' "$parseable_key_requirement" \
"$weakened_key_doc"
assert_bootstrap_probe_rejected 'hardening availability' "$hardening_continues_requirement" \
"${bootstrap_doc//unavailable/available}"
assert_bootstrap_probe_rejected 'hardening continuation' "$hardening_continues_requirement" \
"${bootstrap_doc//continues/stops}"
assert_bootstrap_probe_rejected 'atomic drop-in' "$atomic_dropin_requirement" \
"${bootstrap_doc//atomic /}"
assert_bootstrap_probe_rejected 'rollback after failure' "$rollback_requirement" \
"${bootstrap_doc//restores /keeps }"
assert_bootstrap_probe_rejected 'rollback trigger' "$rollback_requirement" \
"${bootstrap_doc//fails/works}"
assert_bootstrap_probe_rejected 'effective target policy' "$effective_policy_requirement" \
"${bootstrap_doc//target/root}"
if grep -qiE 'merely writes? (the )?(SSH )?(drop-in|file)|reload failure.*ignored|ignores? .*reload failure' <<<"$bootstrap_doc"; then
note 'the root bootstrap docs weaken the transaction by treating the write or reload failure as harmless'
fi
# ── Documented subcommands exist ───────────────────────────────────────────── # ── Documented subcommands exist ─────────────────────────────────────────────
# #
# A README listing a command the dispatcher does not have sends somebody to a # A README listing a command the dispatcher does not have sends somebody to a
File diff suppressed because it is too large Load Diff
+496
View File
@@ -0,0 +1,496 @@
#!/usr/bin/env bash
# The public seam is the installed `panama` command. This fixture repository
# proves the runner's manifest policy and diagnostics without touching the host.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
fixture="$(mktemp -d)"
output=""
status=0
background_cli_pid=""
background_contract_pgid=""
cleanup() {
trap - EXIT INT TERM
if [[ "$background_cli_pid" =~ ^[1-9][0-9]*$ ]]; then
kill -TERM "$background_cli_pid" 2>/dev/null || true
wait "$background_cli_pid" 2>/dev/null || true
fi
if [[ "$background_contract_pgid" =~ ^[1-9][0-9]*$ ]]; then
kill -KILL -- "-$background_contract_pgid" 2>/dev/null || true
fi
rm -rf -- "$fixture"
}
trap cleanup EXIT
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM
fail() { printf 'test runner: %s\n' "$*" >&2; exit 1; }
assert_contains() {
local needle="$1" haystack="$2"
[[ "$haystack" == *"$needle"* ]] || fail "expected output to contain: $needle\n$haystack"
}
assert_not_contains() {
local needle="$1" haystack="$2"
[[ "$haystack" != *"$needle"* ]] || fail "expected output not to contain: $needle\n$haystack"
}
assert_before() {
local first="$1" second="$2" haystack="$3"
[[ "$haystack" == *"$first"*"$second"* ]] \
|| fail "expected '$first' before '$second':\n$haystack"
}
assert_execution() {
local expected="$1" actual
actual="$(sort "$fixture/executions" 2>/dev/null || true)"
[[ "$actual" == "$expected" ]] || fail "expected executions '$expected', got '$actual'"
}
reset_executions() { : > "$fixture/executions"; }
wait_for_file() {
local path="$1" attempt
for (( attempt = 0; attempt < 100; attempt++ )); do
[[ -s "$path" ]] && return 0
sleep 0.05
done
return 1
}
wait_for_process_exit() {
local pid="$1" attempt
for (( attempt = 0; attempt < 100; attempt++ )); do
kill -0 "$pid" 2>/dev/null || return 0
sleep 0.05
done
return 1
}
wait_for_path_removal() {
local path="$1" attempt
for (( attempt = 0; attempt < 100; attempt++ )); do
[[ ! -e "$path" ]] && return 0
sleep 0.05
done
return 1
}
run_panama() {
output="$(cd "$fixture" && TMPDIR="$fixture" PANAMA_TEST_FIXTURE="$fixture" "$fixture/bin/panama" "$@" </dev/null 2>&1)"
status=$?
}
run_panama_with_timeout() {
output="$(cd "$fixture" && TMPDIR="$fixture" PANAMA_TEST_TIMEOUT_SECONDS=1 PANAMA_TEST_FIXTURE="$fixture" "$fixture/bin/panama" "$@" </dev/null 2>&1)"
status=$?
}
run_panama_tty_default_no() {
local command tty_stdout="$fixture/tty.stdout"
local pty_state="$fixture/pty-state"
mkdir -p "$pty_state"/{config,state,cache,data,runtime}
chmod 700 "$pty_state/runtime"
printf -v command 'cd %q && TMPDIR=%q PANAMA_TEST_FIXTURE=%q %q test composite > %q' \
"$fixture" "$fixture" "$fixture" "$fixture/bin/panama" "$tty_stdout"
output="$(
HOME="$fixture/pty-home" \
BASH_ENV="$fixture/pty-bash-env" \
PANAMA_PTY_STARTUP_SENTINEL="$fixture/pty-startup-sourced" \
python3 - "$command" "$fixture/pty-home" \
"$pty_state/config" "$pty_state/state" "$pty_state/cache" \
"$pty_state/data" "$pty_state/runtime" "$fixture" <<'PY'
import errno
import os
import pty
import sys
command = sys.argv[1]
environment = os.environ.copy()
environment.pop('BASH_ENV', None)
environment.pop('ENV', None)
environment.update({
'HOME': sys.argv[2],
'XDG_CONFIG_HOME': sys.argv[3],
'XDG_STATE_HOME': sys.argv[4],
'XDG_CACHE_HOME': sys.argv[5],
'XDG_DATA_HOME': sys.argv[6],
'XDG_RUNTIME_DIR': sys.argv[7],
'TMPDIR': sys.argv[8],
})
pid, terminal = pty.fork()
if pid == 0:
os.execve('/bin/bash', ['bash', '--noprofile', '--norc', '-c', command], environment)
chunks = []
replied = False
while True:
try:
chunk = os.read(terminal, 1024)
except OSError as error:
if error.errno == errno.EIO:
break
raise
if not chunk:
break
chunks.append(chunk)
if not replied and b'[y/N]' in b''.join(chunks):
os.write(terminal, b'\n')
replied = True
_, child_status = os.waitpid(pid, 0)
sys.stdout.buffer.write(b''.join(chunks))
sys.exit(os.waitstatus_to_exitcode(child_status))
PY
)"
status=$?
}
assert_occurrences() {
local needle="$1" haystack="$2" expected="$3" actual
actual="$(grep -oF -- "$needle" <<<"$haystack" | wc -l)"
[[ "$actual" == "$expected" ]] || fail "expected $expected occurrence(s) of '$needle', got $actual\n$haystack"
}
replace_manifest_line() {
local original="$1" replacement="$2" line
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" == "$original" ]]; then
[[ "$replacement" == __REMOVE__ ]] || printf '%s\n' "$replacement"
else
printf '%s\n' "$line"
fi
done <<<"$valid_manifest"
}
swap_manifest_contract_paths() {
local line
while IFS= read -r line || [[ -n "$line" ]]; do
case "$line" in
'live-compositor,live-desktop tests/composite-contract')
printf '%s\n' 'live-compositor,live-desktop tests/desktop-contract'
;;
'live-desktop tests/desktop-contract')
printf '%s\n' 'live-desktop tests/composite-contract'
;;
*) printf '%s\n' "$line" ;;
esac
done <<<"$valid_manifest"
}
expect_manifest_rejection() {
local label="$1" expected="$2" contents="$3"
printf '%s\n' "$contents" >"$fixture/tests/contracts.manifest"
reset_executions
run_panama test pass
[[ $status -ne 0 ]] || fail "$label manifest unexpectedly allowed test execution"
assert_contains "$expected" "$output"
assert_execution ''
run_panama contracts config/subject
[[ $status -ne 0 ]] || fail "$label manifest unexpectedly allowed contracts lookup"
assert_contains "$expected" "$output"
assert_execution ''
}
mkdir -p "$fixture/bin" "$fixture/tests" "$fixture/config"
cp "$repo_dir/bin/panama" "$fixture/bin/panama"
chmod +x "$fixture/bin/panama"
touch "$fixture/config/subject"
git -C "$fixture" init --quiet
cat > "$fixture/tests/contracts.manifest" <<'EOF'
# Maps the live desktop and reads compositor state.
live-compositor,live-desktop tests/composite-contract
# Maps the live desktop.
live-desktop tests/desktop-contract
hermetic tests/fail-contract
hermetic tests/hang-contract
# Reads a host fixture.
live-host tests/host-contract
# Contacts a fixture endpoint.
network tests/network-contract
hermetic tests/pass-contract
# Elevates a fixture boundary.
privileged tests/privileged-contract
hermetic tests/stderr-contract
EOF
valid_manifest="$(<"$fixture/tests/contracts.manifest")"
cat > "$fixture/tests/pass-contract" <<'EOF'
#!/usr/bin/env bash
printf 'pass\n' >> "$PANAMA_TEST_FIXTURE/executions"
printf 'pass stdout\n'
# config/subject
EOF
cat > "$fixture/tests/fail-contract" <<'EOF'
#!/usr/bin/env bash
printf 'fail\n' >> "$PANAMA_TEST_FIXTURE/executions"
printf 'failure stdout\n'
printf 'failure stderr\n' >&2
exit 7
EOF
cat > "$fixture/tests/stderr-contract" <<'EOF'
#!/usr/bin/env bash
printf 'stderr\n' >> "$PANAMA_TEST_FIXTURE/executions"
printf 'warning on success\n' >&2
EOF
cat > "$fixture/tests/hang-contract" <<'EOF'
#!/usr/bin/env bash
printf 'hang\n' >> "$PANAMA_TEST_FIXTURE/executions"
printf '%s\n' "$BASHPID" > "$PANAMA_TEST_FIXTURE/hang.pid"
finish() {
printf 'terminated\n' >"$PANAMA_TEST_FIXTURE/terminated"
exit "$1"
}
trap 'finish 130' INT
trap 'finish 143' TERM
while :; do sleep 1; done
EOF
cat > "$fixture/tests/host-contract" <<'EOF'
#!/usr/bin/env bash
printf 'host\n' >> "$PANAMA_TEST_FIXTURE/executions"
EOF
cat > "$fixture/tests/desktop-contract" <<'EOF'
#!/usr/bin/env bash
printf 'desktop\n' >> "$PANAMA_TEST_FIXTURE/executions"
printf 'desktop fixture complete\n' >&2
# config/subject
EOF
cat > "$fixture/tests/composite-contract" <<'EOF'
#!/usr/bin/env bash
printf 'composite\n' >> "$PANAMA_TEST_FIXTURE/executions"
# config/subject
EOF
cat > "$fixture/tests/network-contract" <<'EOF'
#!/usr/bin/env bash
printf 'network\n' >> "$PANAMA_TEST_FIXTURE/executions"
EOF
cat > "$fixture/tests/privileged-contract" <<'EOF'
#!/usr/bin/env bash
printf 'privileged\n' >> "$PANAMA_TEST_FIXTURE/executions"
EOF
chmod +x "$fixture/tests"/{composite,desktop,fail,hang,host,network,pass,privileged,stderr}-contract
: > "$fixture/executions"
# A PTY-backed default-no confirmation remains visible when stdout is redirected
# but stdin and stderr are terminals. The fixture proves that one prompt gates
# the selected composite capability set without running its contract.
mkdir -p "$fixture/pty-home"
for profile in .bash_profile .bashrc .profile; do
cat >"$fixture/pty-home/$profile" <<'EOF'
printf 'profile\n' >>"${PANAMA_PTY_STARTUP_SENTINEL:?}"
EOF
done
cat >"$fixture/pty-bash-env" <<'EOF'
printf 'BASH_ENV\n' >>"${PANAMA_PTY_STARTUP_SENTINEL:?}"
EOF
run_panama_tty_default_no
[[ $status -ne 0 ]] || fail 'TTY default-no prompt unexpectedly ran the fixture'
assert_execution ''
assert_contains 'Run 1 contract(s) requiring: live-compositor live-desktop?' "$output"
assert_occurrences 'Run 1 contract(s) requiring:' "$output" 1
assert_contains 'No contracts were run.' "$(<"$fixture/tty.stdout")"
assert_not_contains 'Run 1 contract(s) requiring:' "$(<"$fixture/tty.stdout")"
[[ ! -e "$fixture/pty-startup-sourced" ]] \
|| fail 'PTY fixture sourced a shell profile or BASH_ENV'
# --safe must select hermetic entries from the manifest, not merely omit a
# legacy desktop list. The failing and timed-out fixtures make the command
# nonzero, but every selected hermetic contract still runs and each external
# capability reports its skipped count.
rm -f -- "$fixture/terminated"
run_panama_with_timeout test --safe
[[ $status -ne 0 ]] || fail '--safe unexpectedly passed a failing fixture'
assert_execution $'fail\nhang\npass\nstderr'
[[ -f "$fixture/terminated" ]] || fail '--safe did not run and terminate the hermetic hang fixture'
assert_contains 'Skipped 1 live-host contract(s).' "$output"
assert_contains 'Skipped 1 live-compositor contract(s).' "$output"
assert_contains 'Skipped 2 live-desktop contract(s).' "$output"
assert_contains 'Skipped 1 network contract(s).' "$output"
assert_contains 'Skipped 1 privileged contract(s).' "$output"
reset_executions
run_panama test desktop
[[ $status -ne 0 ]] || fail 'non-TTY desktop run unexpectedly passed without a grant'
assert_execution ''
assert_contains 'pass --allow live-desktop' "$output"
run_panama test --allow live-desktop desktop
[[ $status -eq 0 ]] || fail "explicit desktop grant failed: $output"
assert_execution 'desktop'
assert_contains 'Running desktop-contract [live-desktop]' "$output"
assert_before 'Running desktop-contract [live-desktop]' 'desktop fixture complete' "$output"
reset_executions
run_panama test --allow live-compositor --allow live-desktop composite
[[ $status -eq 0 ]] || fail "repeatable grants failed: $output"
assert_execution 'composite'
reset_executions
run_panama test --allow live-desktop network
[[ $status -ne 0 ]] || fail 'desktop grant incorrectly allowed network'
assert_execution ''
assert_contains 'network' "$output"
for args in '--unknown' 'pass-contract second-pattern' '--allow unknown' '--safe --allow live-desktop'; do
# shellcheck disable=SC2086
run_panama test $args
[[ $status -eq 2 ]] || fail "usage error did not exit 2 for: $args\n$output"
done
reset_executions
run_panama_with_timeout test hang
[[ $status -ne 0 ]] || fail 'timed-out contract unexpectedly passed'
assert_execution 'hang'
[[ -f "$fixture/terminated" ]] || fail 'timed-out contract was not terminated with TERM'
assert_contains 'timed out' "$output"
# INT/TERM ownership belongs to the exact public CLI PID, not a runner
# subshell. The CLI must wait for the timeout process group and remove its
# capture directory before returning the signal-derived status.
reset_executions
rm -f -- "$fixture/hang.pid" "$fixture/terminated"
(
cd "$fixture" || exit 1
exec env TMPDIR="$fixture" PANAMA_TEST_FIXTURE="$fixture" \
"$fixture/bin/panama" test hang
) >"$fixture/exact-term.out" 2>&1 &
background_cli_pid=$!
wait_for_file "$fixture/hang.pid" \
|| fail 'exact-PID TERM fixture never started the hang contract'
hang_pid="$(<"$fixture/hang.pid")"
background_contract_pgid="$(ps -o pgid= -p "$hang_pid" | tr -d '[:space:]')"
[[ "$background_contract_pgid" =~ ^[1-9][0-9]*$ ]] \
|| fail "could not resolve hang process group for PID $hang_pid"
mapfile -t active_capture_dirs < <(
find "$fixture" -mindepth 1 -maxdepth 1 -type d -name 'tmp.*' -print
)
(( ${#active_capture_dirs[@]} == 1 )) \
|| fail "expected one active capture directory, got ${#active_capture_dirs[@]}"
active_capture_dir="${active_capture_dirs[0]}"
kill -TERM "$background_cli_pid" \
|| fail 'could not send TERM to the exact public CLI PID'
term_status=0
wait "$background_cli_pid" || term_status=$?
background_cli_pid=""
[[ "$term_status" -eq 143 ]] \
|| fail "exact-PID TERM returned $term_status instead of 143: $(<"$fixture/exact-term.out")"
wait_for_process_exit "$hang_pid" \
|| fail "hang contract PID $hang_pid survived exact-PID TERM"
background_contract_pgid=""
wait_for_path_removal "$active_capture_dir" \
|| fail "capture directory survived exact-PID TERM: $active_capture_dir"
[[ -f "$fixture/terminated" ]] \
|| fail 'exact-PID TERM did not reach the hang contract cleanup trap'
reset_executions
run_panama test fail
[[ $status -ne 0 ]] || fail 'failed contract unexpectedly passed'
assert_contains 'failure stdout' "$output"
assert_contains 'failure stderr' "$output"
reset_executions
run_panama test stderr
[[ $status -eq 0 ]] || fail "stderr success contract failed: $output"
assert_contains 'warning on success' "$output"
reset_executions
run_panama test pass
[[ $status -eq 0 ]] || fail "pass contract failed: $output"
assert_not_contains 'pass stdout' "$output"
assert_not_contains 'pass-contract [hermetic]' "$output"
reset_executions
printf 'not a directory\n' >"$fixture/invalid-tmpdir"
output="$(
cd "$fixture" && \
TMPDIR="$fixture/invalid-tmpdir" PANAMA_TEST_FIXTURE="$fixture" \
"$fixture/bin/panama" test pass </dev/null 2>&1
)"
status=$?
[[ $status -ne 0 ]] || fail 'invalid TMPDIR unexpectedly allowed contract execution'
assert_contains 'Could not create contract capture directory.' "$output"
assert_execution ''
reset_executions
run_panama test --safe desktop
[[ $status -ne 0 ]] || fail 'only-skipped pattern unexpectedly passed'
assert_contains 'Every contract matching' "$output"
assert_not_contains 'No contracts match' "$output"
output="$(cd "$fixture" && "$fixture/bin/panama" contracts config/subject 2>&1)"
status=$?
[[ $status -eq 0 ]] || fail "contracts lookup failed: $output"
assert_contains 'tests/desktop-contract [live-desktop]' "$output"
assert_contains 'tests/composite-contract [live-compositor,live-desktop]' "$output"
assert_contains 'tests/pass-contract [hermetic]' "$output"
# Both public manifest consumers fail closed on the complete format and
# discovery set. Validation happens before selection, lookup, or contract
# execution, so even a malformed entry unrelated to the requested pattern is
# fatal and leaves the execution log empty.
expect_manifest_rejection unknown-capability \
'unknown capability hermetik on tests/pass-contract' \
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetik tests/pass-contract')"
expect_manifest_rejection mixed-hermetic \
'hermetic must appear alone on tests/pass-contract' \
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetic,network tests/pass-contract')"
expect_manifest_rejection duplicate-path \
'duplicate path tests/pass-contract' \
"$(replace_manifest_line 'hermetic tests/pass-contract' $'hermetic tests/pass-contract\nhermetic tests/pass-contract')"
expect_manifest_rejection stale-path \
'stale manifest path tests/stale-contract' \
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetic tests/stale-contract')"
expect_manifest_rejection missing-contract \
'missing contract tests/pass-contract' \
"$(replace_manifest_line 'hermetic tests/pass-contract' __REMOVE__)"
expect_manifest_rejection extra-field \
'manifest line is not exactly two fields' \
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetic tests/pass-contract unexpected')"
expect_manifest_rejection empty-capability \
'empty capability on tests/pass-contract' \
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetic, tests/pass-contract')"
expect_manifest_rejection duplicate-capability \
'duplicate capability hermetic on tests/pass-contract' \
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetic,hermetic tests/pass-contract')"
expect_manifest_rejection unsorted-paths \
'paths are not lexicographically sorted' \
"$(swap_manifest_contract_paths)"
expect_manifest_rejection uncommented-non-hermetic \
'tests/desktop-contract is non-hermetic but lacks a directly preceding comment' \
"$(replace_manifest_line '# Maps the live desktop.' __REMOVE__)"
expect_manifest_rejection blank-comment \
'tests/desktop-contract is non-hermetic but lacks a non-empty directly preceding comment' \
"$(replace_manifest_line '# Maps the live desktop.' '#')"
printf '%s\n' "$valid_manifest" >"$fixture/tests/contracts.manifest"
mv "$fixture/tests/contracts.manifest" "$fixture/tests/contracts.manifest.missing"
reset_executions
run_panama test pass
[[ $status -ne 0 ]] || fail 'missing manifest unexpectedly allowed test execution'
assert_contains 'contracts.manifest' "$output"
assert_execution ''
mv "$fixture/tests/contracts.manifest.missing" "$fixture/tests/contracts.manifest"
capture_dirs="$(find "$fixture" -mindepth 1 -maxdepth 1 -type d -name 'tmp.*' -print)"
[[ -z "$capture_dirs" ]] || fail "runner leaked capture directory: $capture_dirs"
printf 'test runner: PASS\n'
+634 -77
View File
@@ -37,19 +37,37 @@ tmp="$(mktemp -d -t panama-update-contract.XXXXXX)"
trap 'rm -rf "$tmp"' EXIT trap 'rm -rf "$tmp"' EXIT
STAGE_NAMES=(install-packages link-dotfiles link-skills link-user change-settings STAGE_NAMES=(install-packages link-dotfiles link-skills link-user change-settings
link-vicinae-scripts setup-identity install-hardware) link-vicinae-scripts setup-server link-server setup-identity
install-hardware)
copy_hash_inputs() {
local root="$1" source relative
while IFS= read -r -d '' source; do
relative="${source#"$repo_dir"/}"
mkdir -p "$(dirname "$root/$relative")"
cp -- "$source" "$root/$relative"
done < <(
find "$repo_dir/setup/packages" -maxdepth 1 -type f -print0
find "$repo_dir/setup/provenance" -type f -print0
)
mkdir -p "$root/setup/lib"
cp -- "$repo_dir/setup/lib/artifact-provenance" \
"$repo_dir/setup/lib/extras-catalog" \
"$repo_dir/setup/lib/machine-role" "$root/setup/lib/"
}
# A PANAMA_PATH that looks enough like the real one for install to run, and # A PANAMA_PATH that looks enough like the real one for install to run, and
# records what it was asked to do instead of doing it. # records what it was asked to do instead of doing it.
build_fixture() { build_fixture() {
local root="$1" packages_rc="${2:-0}" local root="$1" packages_rc="${2:-0}" trust_rc="${3:-0}"
rm -rf "$root" rm -rf "$root"
mkdir -p "$root/bin" "$root/setup/scripts" "$root/setup/packages" \ mkdir -p "$root/bin" "$root/setup/scripts" "$root/setup/packages" \
"$root/config/dot/quickshell/scripts" "$root/setup/lib" "$root/setup/provenance/keys" \
"$root/config/dot/quickshell/scripts" "$root/tmp"
cp "$installer" "$root/install" cp "$installer" "$root/install"
: >"$root/bin/ascii" : >"$root/bin/ascii"
printf 'base-package\n' >"$root/setup/packages/base" copy_hash_inputs "$root"
local stage local stage
for stage in "${STAGE_NAMES[@]}"; do for stage in "${STAGE_NAMES[@]}"; do
@@ -62,6 +80,10 @@ EOF
# The one stage whose exit code the caller wants to control. # The one stage whose exit code the caller wants to control.
cat >"$root/setup/scripts/install-packages" <<EOF cat >"$root/setup/scripts/install-packages" <<EOF
#!/usr/bin/env bash #!/usr/bin/env bash
if [[ "\${1:-}" == --trust-preflight ]]; then
printf 'trust-preflight\n' >>"\$PANAMA_RAN"
exit $trust_rc
fi
printf 'install-packages\n' >>"\$PANAMA_RAN" printf 'install-packages\n' >>"\$PANAMA_RAN"
exit $packages_rc exit $packages_rc
EOF EOF
@@ -102,26 +124,92 @@ EOF
#!/usr/bin/env bash #!/usr/bin/env bash
exit 0 exit 0
EOF EOF
cat >"$root/shim/dnf" <<'EOF'
#!/usr/bin/env bash
printf 'dnf-transaction\n' >>"$PANAMA_RAN"
exit 0
EOF
cat >"$root/shim/mv" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
destination="${!#}"
if [[ "${STUB_SIGNAL_PACKAGES_HASH:-0}" == 1 \
&& "$destination" == */state/panama/packages-hash ]]; then
printf 'signal:packages-receipt\n' >>"$PANAMA_RAN"
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
kill -TERM -- "-$pgid"
sleep 2
fi
exec /usr/bin/mv "$@"
EOF
cat >"$root/shim/mktemp" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if [[ "${STUB_SIGNAL_HASH_WORK:-0}" == 1 && "${1:-}" == -d ]]; then
directory="$(/usr/bin/mktemp "$@")"
printf '%s\n' "$directory"
printf 'signal:packages-hash-work\n' >>"$PANAMA_RAN"
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
kill -TERM -- "-$pgid"
sleep 2
fi
exec /usr/bin/mktemp "$@"
EOF
cat >"$root/shim/mkdir" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
target="${!#}"
if [[ "${STUB_SIGNAL_HASH_WORK:-0}" == 1 \
&& "$(basename -- "$target")" == panama-packages-hash.* ]]; then
/usr/bin/mkdir "$@"
printf 'signal:packages-hash-work\n' >>"$PANAMA_RAN"
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
kill -TERM -- "-$pgid"
sleep 2
fi
exec /usr/bin/mkdir "$@"
EOF
for prerequisite in gum lspci mokutil fwupdmgr; do
ln -s gsettings "$root/shim/$prerequisite"
done
chmod +x "$root/shim"/* chmod +x "$root/shim"/*
} }
# Run the fixture installer and echo what ran, one stage per line. # Run the fixture installer and echo what ran, one stage per line.
run_install() { run_install() {
local root="$1"; shift local root="$1"; shift
local status=0
: >"$root/ran" : >"$root/ran"
PATH="$root/shim:$PATH" \ PATH="$root/shim:$PATH" PANAMA_PATH="$root" PANAMA_RAN="$root/ran" \
PANAMA_PATH="$root" \ XDG_STATE_HOME="$root/state" TMPDIR="$root/tmp" \
PANAMA_RAN="$root/ran" \ /usr/bin/setsid bash "$root/install" "$@" \
XDG_STATE_HOME="$root/state" \ >"$root/out" 2>&1 || status=$?
bash "$root/install" "$@" >"$root/out" 2>&1
printf '%s' "$?" >"$root/rc"
cat "$root/ran" cat "$root/ran"
return "$status"
}
run_hash() {
local root="$1"
sed -n '/^_collect_package_inputs() {/,/^PACKAGE_START_HASH=/p' \
"$root/install" >"$root/hash-only"
printf 'set -uo pipefail\nhash_packages\n' >>"$root/hash-only"
PANAMA_PATH="$root" bash "$root/hash-only" 2>"$root/hash-only.err"
}
assert_hash_failure() {
local root="$1" description="$2" status=0 digest
digest="$(run_hash "$root")" || status=$?
[[ "$status" -ne 0 && -z "$digest" ]] \
|| note "$description produced a digest instead of failing closed"
} }
# ── 1. The interview never runs on an upgrade ──────────────────────────────── # ── 1. The interview never runs on an upgrade ────────────────────────────────
build_fixture "$tmp/a" build_fixture "$tmp/a"
ran="$(run_install "$tmp/a" --upgrade)" install_status=0
ran="$(run_install "$tmp/a" --upgrade)" || install_status=$?
[[ "$install_status" -eq 0 ]] \
|| note "initial install --upgrade failed with status $install_status"
if grep -qx 'interview' <<<"$ran"; then if grep -qx 'interview' <<<"$ran"; then
note 'install --upgrade ran the interview, which is the whole regression this prevents' note 'install --upgrade ran the interview, which is the whole regression this prevents'
@@ -129,7 +217,10 @@ fi
# And the control: a real install must still ask. # And the control: a real install must still ask.
build_fixture "$tmp/b" build_fixture "$tmp/b"
ran_install="$(run_install "$tmp/b")" install_status=0
ran_install="$(run_install "$tmp/b")" || install_status=$?
[[ "$install_status" -eq 0 ]] \
|| note "plain fixture install failed with status $install_status"
if ! grep -qx 'interview' <<<"$ran_install"; then if ! grep -qx 'interview' <<<"$ran_install"; then
note 'a plain ./install no longer asks the interview, so a new machine is never configured' note 'a plain ./install no longer asks the interview, so a new machine is never configured'
fi fi
@@ -146,12 +237,28 @@ done
# A stage added to STAGES without a decision about which path owns it shows up # A stage added to STAGES without a decision about which path owns it shows up
# here, because this list is written down twice on purpose. # here, because this list is written down twice on purpose.
mapfile -t declared < <(python3 - "$installer" <<'PY' declared=()
import re, sys if declared_output="$(python3 - "$installer" <<'PY'
line = next(l for l in open(sys.argv[1], encoding="utf-8") if l.startswith("STAGES=")) import re
print("\n".join(re.findall(r"[\w-]+", line)[1:])) import sys
found = False
for line in open(sys.argv[1], encoding="utf-8"):
match = re.match(r'^\s*STAGES=\((.*)\)\s*$', line)
if match and "$" not in match.group(1):
found = True
print("\n".join(re.findall(r"[\w-]+", match.group(1))))
if not found:
print("install has no STAGES assignment", file=sys.stderr)
sys.exit(1)
PY PY
) )"; then
while IFS= read -r stage; do
[[ -n "$stage" ]] && declared+=("$stage")
done <<<"$declared_output"
else
note 'could not read the install STAGES assignment'
fi
for stage in "${declared[@]}"; do for stage in "${declared[@]}"; do
printf '%s\n' "${STAGE_NAMES[@]}" | grep -qx "$stage" \ printf '%s\n' "${STAGE_NAMES[@]}" | grep -qx "$stage" \
|| note "install declares a stage this contract has never heard of: $stage" || note "install declares a stage this contract has never heard of: $stage"
@@ -159,96 +266,546 @@ done
# ── 3. The packages hash gates the stage, and a failure does not record it ─── # ── 3. The packages hash gates the stage, and a failure does not record it ───
package_inputs=()
while IFS= read -r -d '' input; do
package_inputs+=("${input#"$repo_dir"/}")
done < <(find "$repo_dir/setup/packages" -maxdepth 1 -type f -print0)
provenance_inputs=()
while IFS= read -r -d '' input; do
provenance_inputs+=("${input#"$repo_dir"/}")
done < <(find "$repo_dir/setup/provenance" -type f -print0)
(( ${#package_inputs[@]} > 0 )) \
|| note 'the current repository has no top-level package input to exercise'
(( ${#provenance_inputs[@]} > 0 )) \
|| note 'the current repository has no provenance input to exercise'
# Second run, nothing changed: the stage must be skipped. # Second run, nothing changed: the stage must be skipped.
ran_again="$(run_install "$tmp/a" --upgrade)" install_status=0
ran_again="$(run_install "$tmp/a" --upgrade)" || install_status=$?
[[ "$install_status" -eq 0 ]] \
|| note "repeat install --upgrade failed with status $install_status"
grep -qx 'install-packages' <<<"$ran_again" \ grep -qx 'install-packages' <<<"$ran_again" \
&& note 'install-packages ran again with the package lists unchanged' && note 'install-packages ran again with the package lists unchanged'
# --packages overrides the hash. # --packages overrides the hash.
ran_forced="$(run_install "$tmp/a" --upgrade --packages)" install_status=0
ran_forced="$(run_install "$tmp/a" --upgrade --packages)" || install_status=$?
[[ "$install_status" -eq 0 ]] \
|| note "install --upgrade --packages failed with status $install_status"
grep -qx 'install-packages' <<<"$ran_forced" \ grep -qx 'install-packages' <<<"$ran_forced" \
|| note '--packages did not force install-packages to run' || note '--packages did not force install-packages to run'
# A changed list brings the stage back. # Every current package and provenance member is part of the state definition.
printf 'another-package\n' >>"$tmp/a/setup/packages/base" # Dynamically discovering them makes this fail when a new reviewed input is
ran_changed="$(run_install "$tmp/a" --upgrade)" # added but omitted from hash_packages.
grep -qx 'install-packages' <<<"$ran_changed" \ for relative in "${package_inputs[@]}" "${provenance_inputs[@]}" \
|| note 'a changed package list did not bring install-packages back' 'setup/scripts/install-packages' 'setup/lib/artifact-provenance' \
'setup/lib/extras-catalog' 'setup/lib/machine-role'; do
printf 'changed %s\n' "$relative" >>"$tmp/a/$relative"
install_status=0
ran_input_changed="$(run_install "$tmp/a" --upgrade)" || install_status=$?
[[ "$install_status" -eq 0 ]] \
|| note "install --upgrade failed after changing $relative with status $install_status"
grep -qx 'install-packages' <<<"$ran_input_changed" \
|| note "a changed $relative did not bring install-packages back"
done
# A path-only change must invalidate state even when the file bytes are exact.
for relative in "${package_inputs[0]:-}" "${provenance_inputs[0]:-}"; do
[[ -n "$relative" ]] || continue
build_fixture "$tmp/path-rename"
run_install "$tmp/path-rename" --upgrade >/dev/null
mv -- "$tmp/path-rename/$relative" "$tmp/path-rename/$relative.renamed"
install_status=0
ran_renamed="$(run_install "$tmp/path-rename" --upgrade)" || install_status=$?
[[ "$install_status" -eq 0 ]] \
|| note "install --upgrade failed after renaming $relative with status $install_status"
grep -qx 'install-packages' <<<"$ran_renamed" \
|| note "renaming $relative without changing bytes did not bring install-packages back"
done
# Fixed hash inputs must not silently disappear or degrade into a directory or
# link. An unreadable package input also proves a failed content read cannot be
# hidden by the final digest command.
for fixed_input in setup/scripts/install-packages setup/lib/artifact-provenance \
setup/lib/extras-catalog setup/lib/machine-role; do
for case_name in missing directory symlink unreadable; do
case_root="$tmp/hash-${fixed_input//\//-}-$case_name"
build_fixture "$case_root"
fixed_path="$case_root/$fixed_input"
case "$case_name" in
missing) rm -- "$fixed_path" ;;
directory) rm -- "$fixed_path"; mkdir -- "$fixed_path" ;;
symlink)
printf 'untrusted target\n' >"$case_root/untrusted-target"
rm -- "$fixed_path"
ln -s "$case_root/untrusted-target" "$fixed_path"
;;
unreadable) chmod 000 "$fixed_path" ;;
esac
assert_hash_failure "$case_root" "$fixed_input $case_name"
done
done
read_failure_root="$tmp/hash-package-read-failure"
build_fixture "$read_failure_root"
chmod 000 "$read_failure_root/${package_inputs[0]}"
assert_hash_failure "$read_failure_root" "${package_inputs[0]} unreadable"
# Discovery must reject a symlink instead of silently dropping it from the
# receipt while a later consumer follows it.
for discovered_root in setup/packages setup/provenance; do
case_root="$tmp/hash-${discovered_root//\//-}-symlink"
build_fixture "$case_root"
printf 'linked installer input\n' >"$case_root/symlink-target"
ln -s "$case_root/symlink-target" "$case_root/$discovered_root/symlink-input"
assert_hash_failure "$case_root" "$discovered_root symlink input"
done
# Discovery roots are behavior inputs too. GNU find -P treats a symlink passed
# as its starting path as an empty traversal, so checking only descendants can
# silently erase a whole package or provenance tree from the receipt.
for discovered_root in setup/packages setup/provenance; do
case_root="$tmp/hash-${discovered_root//\//-}-root-symlink"
build_fixture "$case_root"
mv -- "$case_root/$discovered_root" "$case_root/$discovered_root.real"
ln -s "$case_root/$discovered_root.real" "$case_root/$discovered_root"
assert_hash_failure "$case_root" "$discovered_root discovery-root symlink"
done
# A hash failure is an installer failure, not a reason to skip the package
# stage and retain a stale stamp.
build_fixture "$tmp/hash-failure"
run_install "$tmp/hash-failure" --upgrade >/dev/null
cp -- "$tmp/hash-failure/state/panama/packages-hash" "$tmp/hash-failure/stamp-before"
rm -- "$tmp/hash-failure/setup/lib/artifact-provenance"
install_status=0
ran_hash_failure="$(run_install "$tmp/hash-failure" --upgrade)" || install_status=$?
[[ "$install_status" -ne 0 ]] \
|| note 'a failed package-state hash returned success'
grep -qx 'install-packages' <<<"$ran_hash_failure" \
&& note 'a failed package-state hash still ran install-packages'
cmp -s -- "$tmp/hash-failure/stamp-before" "$tmp/hash-failure/state/panama/packages-hash" \
|| note 'a failed package-state hash wrote a new packages-hash stamp'
# The stage may race its own input receipt. A successful stage that changes a
# sourced behavior file must not stamp the new digest as though it were the
# bytes used to decide this run.
build_fixture "$tmp/hash-drift"
cat >"$tmp/hash-drift/setup/scripts/install-packages" <<'EOF'
#!/usr/bin/env bash
if [[ "${1:-}" == --trust-preflight ]]; then
printf 'trust-preflight\n' >>"$PANAMA_RAN"
exit 0
fi
printf 'install-packages\n' >>"$PANAMA_RAN"
printf '# changed during package stage\n' >>"$PANAMA_PATH/setup/lib/machine-role"
EOF
chmod +x "$tmp/hash-drift/setup/scripts/install-packages"
install_status=0
run_install "$tmp/hash-drift" --upgrade >/dev/null || install_status=$?
[[ "$install_status" -ne 0 ]] \
|| note 'mid-stage package input drift returned success'
[[ ! -e "$tmp/hash-drift/state/panama/packages-hash" ]] \
|| note 'mid-stage package input drift stamped bytes the stage did not start with'
# The hash workspace exists before command substitution publishes its pathname.
# A process-group signal in that window must still remove the private tree.
build_fixture "$tmp/hash-work-signal"
install_status=0
signal_run="$(STUB_SIGNAL_HASH_WORK=1 \
run_install "$tmp/hash-work-signal" --upgrade)" || install_status=$?
[[ "$install_status" -eq 143 ]] \
|| note "package hash workspace signal returned $install_status instead of 143"
grep -qx 'signal:packages-hash-work' <<<"$signal_run" \
|| note 'package hash workspace adapter did not deliver a real process-group signal'
[[ -z "$(find "$tmp/hash-work-signal/tmp" -mindepth 1 -print -quit)" ]] \
|| note 'package hash workspace signal left a private temporary directory'
# A real process-group signal at the final receipt rename must preserve the
# prior stamp and remove the private temporary receipt.
build_fixture "$tmp/hash-receipt-signal"
run_install "$tmp/hash-receipt-signal" --upgrade >/dev/null
cp -- "$tmp/hash-receipt-signal/state/panama/packages-hash" \
"$tmp/hash-receipt-signal/stamp-before"
install_status=0
signal_run="$(STUB_SIGNAL_PACKAGES_HASH=1 \
run_install "$tmp/hash-receipt-signal" --upgrade --packages)" \
|| install_status=$?
[[ "$install_status" -eq 143 ]] \
|| note "package receipt signal returned $install_status instead of 143"
grep -qx 'signal:packages-receipt' <<<"$signal_run" \
|| note 'package receipt signal adapter did not deliver a real process-group signal'
cmp -s -- "$tmp/hash-receipt-signal/stamp-before" \
"$tmp/hash-receipt-signal/state/panama/packages-hash" \
|| note 'package receipt signal replaced the prior hash stamp'
[[ -z "$(find "$tmp/hash-receipt-signal/state/panama" \
-name '.packages-hash.*' -print -quit)" ]] \
|| note 'package receipt signal left a temporary hash stamp'
# A failing stage must not record the hash, or the failure is hidden forever. # A failing stage must not record the hash, or the failure is hidden forever.
build_fixture "$tmp/c" 1 build_fixture "$tmp/c" 1
run_install "$tmp/c" --upgrade >/dev/null install_status=0
run_install "$tmp/c" --upgrade >/dev/null || install_status=$?
[[ "$install_status" -eq 1 ]] \
|| note "install --upgrade returned $install_status instead of the failing stage status"
if [[ -r "$tmp/c/state/panama/packages-hash" ]]; then if [[ -r "$tmp/c/state/panama/packages-hash" ]]; then
note 'install-packages failed but its hash was recorded, so it will never be retried' note 'install-packages failed but its hash was recorded, so it will never be retried'
fi fi
grep -qx 'link-dotfiles' "$tmp/c/ran" \
|| note 'an ordinary package-stage failure no longer allows later safe stages'
# An invalid enabled Terra root is not an ordinary package failure. It must
# stop before the installer's bootstrap DNF and before every stage.
build_fixture "$tmp/terra-preflight-hard" 0 78
install_status=0
run_install "$tmp/terra-preflight-hard" >/dev/null || install_status=$?
[[ "$install_status" -eq 78 ]] \
|| note "initial Terra trust failure returned $install_status instead of 78"
asserted_preflight="$(<"$tmp/terra-preflight-hard/ran")"
[[ "$asserted_preflight" == trust-preflight ]] \
|| note "initial Terra trust failure allowed later work: ${asserted_preflight//$'\n'/,}"
# The trust verifier is itself mandatory. Losing its executable adapter must
# fail closed before interview, bootstrap, or stage work.
build_fixture "$tmp/terra-preflight-missing"
rm "$tmp/terra-preflight-missing/setup/scripts/install-packages"
install_status=0
run_install "$tmp/terra-preflight-missing" >/dev/null || install_status=$?
[[ "$install_status" -eq 78 ]] \
|| note "missing Terra trust verifier returned $install_status instead of 78"
[[ ! -s "$tmp/terra-preflight-missing/ran" ]] \
|| note 'missing Terra trust verifier allowed later work'
# The package stage repeats the preflight to close a configuration-change race.
# Its hard status must also stop link stages and install-hardware immediately.
build_fixture "$tmp/terra-stage-hard" 78 0
install_status=0
run_install "$tmp/terra-stage-hard" >/dev/null || install_status=$?
[[ "$install_status" -eq 78 ]] \
|| note "stage-time Terra trust failure returned $install_status instead of 78"
grep -qx 'install-packages' "$tmp/terra-stage-hard/ran" \
|| note 'stage-time Terra trust fixture never reached install-packages'
for suppressed in link-dotfiles link-skills link-user change-settings install-hardware dnf-transaction; do
grep -qx "$suppressed" "$tmp/terra-stage-hard/ran" \
&& note "stage-time Terra trust failure still ran $suppressed"
done
# Exercise the complete real package entrypoint at the second boundary. The
# outer preflight sees no Terra repository; the same DNF adapter exposes an
# unsafe enabled Terra identity to the package stage's own preflight. Removing
# that production call would reach the transaction marker below.
real_preflight_root="$tmp/real-second-preflight"
build_fixture "$real_preflight_root"
cp -- "$repo_dir/setup/scripts/install-packages" \
"$real_preflight_root/setup/scripts/install-packages"
chmod +x "$real_preflight_root/setup/scripts/install-packages"
mkdir -p "$real_preflight_root/state/panama"
printf 'server\n' >"$real_preflight_root/state/panama/role"
cat >"$real_preflight_root/shim/dnf" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if [[ "$*" == '--quiet --no-plugins --dump-repo-config=*' ]]; then
count=0
[[ ! -f "$PANAMA_DNF_DUMP_COUNT" ]] || read -r count <"$PANAMA_DNF_DUMP_COUNT"
count=$((count + 1))
printf '%s\n' "$count" >"$PANAMA_DNF_DUMP_COUNT"
printf 'dnf-dump\n' >>"$PANAMA_RAN"
printf '======== "fedora" repository configuration: ========\n'
printf 'baseurl = \nenabled = 1\ngpgcheck = 1\n'
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-44-primary\n'
printf 'metalink = https://mirrors.fedoraproject.org/metalink\nmirrorlist\n'
printf 'pkg_gpgcheck = 0\nrepo_gpgcheck = 0\n'
if (( count == 2 )); then
printf '======== "terra" repository configuration: ========\n'
printf 'baseurl = https://evil.invalid/terra44\nenabled = 1\ngpgcheck = 0\n'
printf 'gpgkey = https://evil.invalid/key\n'
printf 'metalink = \nmirrorlist = \npkg_gpgcheck = 0\nrepo_gpgcheck = 0\n'
fi
exit 0
fi
printf 'dnf-transaction\n' >>"$PANAMA_RAN"
exit 0
EOF
chmod +x "$real_preflight_root/shim/dnf"
printf '0\n' >"$real_preflight_root/dnf-dump-count"
: >"$real_preflight_root/ran"
real_preflight_status=0
PATH="$real_preflight_root/shim:$PATH" \
PANAMA_PATH="$real_preflight_root" PANAMA_RAN="$real_preflight_root/ran" \
PANAMA_DNF_DUMP_COUNT="$real_preflight_root/dnf-dump-count" \
XDG_STATE_HOME="$real_preflight_root/state" \
bash "$real_preflight_root/install" --upgrade --packages \
>"$real_preflight_root/out" 2>&1 || real_preflight_status=$?
[[ "$real_preflight_status" -eq 78 ]] \
|| note "real second repository preflight returned $real_preflight_status instead of 78"
[[ "$(<"$real_preflight_root/dnf-dump-count")" == 2 ]] \
|| note "real package entrypoint executed $(<"$real_preflight_root/dnf-dump-count") repository preflights instead of two: $(tr '\n' ' ' <"$real_preflight_root/out")"
[[ "$(grep -c '^dnf-dump$' "$real_preflight_root/ran")" -eq 2 ]] \
|| note "real second preflight fixture log was: $(tr '\n' ',' <"$real_preflight_root/ran")"
for suppressed in dnf-transaction link-dotfiles link-skills link-user change-settings \
install-hardware; do
grep -qx "$suppressed" "$real_preflight_root/ran" \
&& note "real second repository preflight still ran $suppressed"
done
# A full install always runs the stage, whatever any recorded hash says. # A full install always runs the stage, whatever any recorded hash says.
build_fixture "$tmp/d" build_fixture "$tmp/d"
run_install "$tmp/d" --upgrade >/dev/null install_status=0
ran_full="$(run_install "$tmp/d")" run_install "$tmp/d" --upgrade >/dev/null || install_status=$?
[[ "$install_status" -eq 0 ]] \
|| note "hash-seeding install --upgrade failed with status $install_status"
install_status=0
ran_full="$(run_install "$tmp/d")" || install_status=$?
[[ "$install_status" -eq 0 ]] \
|| note "full fixture reinstall failed with status $install_status"
grep -qx 'install-packages' <<<"$ran_full" \ grep -qx 'install-packages' <<<"$ran_full" \
|| note 'a full ./install skipped install-packages because of a recorded hash' || note 'a full ./install skipped install-packages because of a recorded hash'
# ── 4. A conflicted pop never leaves markers in a live config ──────────────── # ── 4. A conflicted pop never leaves markers in a live config ────────────────
# #
# Two halves. The first checks that git still behaves the way the design # The fixture also covers a clean fast-forward, installer status propagation,
# depends on; the second checks that panama acts on it. Neither is worth much # and the boundary between update and sync before forcing the conflict below.
# without the other.
conflict="$tmp/conflict" # Make every ambient configuration source hostile before constructing the Git
mkdir -p "$conflict" # fixtures. A hermetic fixture overrides these values with its own empty state;
( # consuming any of them either leaves a sentinel or prevents a commit.
set -e hostile="$tmp/hostile-environment"
cd "$conflict" mkdir -p "$hostile/home" "$hostile/xdg-config" "$hostile/xdg-state" \
git init -q up && cd up "$hostile/xdg-cache" "$hostile/xdg-data" "$hostile/hooks" \
git config user.email contract@panama && git config user.name contract "$hostile/template/hooks"
printf 'one\n' >f; git add -A; git commit -qm one for profile in .bash_profile .bashrc .profile; do
cd "$conflict"; git clone -q up work; cd work cat >"$hostile/home/$profile" <<'EOF'
git config user.email contract@panama && git config user.name contract printf 'profile\n' >>"${PANAMA_HOSTILE_PROFILE_SENTINEL:?}"
cd "$conflict/up"; printf 'upstream\n' >f; git commit -qam two EOF
cd "$conflict/work"; printf 'local\n' >f done
git stash push --include-untracked -m contract >/dev/null cat >"$hostile/bash-env" <<'EOF'
git pull -q --ff-only printf 'BASH_ENV\n' >>"${PANAMA_HOSTILE_BASH_ENV_SENTINEL:?}"
git stash pop >/dev/null 2>&1 && exit 3 # a conflict was the point EOF
git reset -q --hard HEAD cat >"$hostile/hooks/pre-commit" <<'EOF'
[[ -n "$(git stash list)" ]] || exit 4 # the stash must survive #!/usr/bin/env bash
grep -q '<<<<<<<' f && exit 5 # and no markers may remain printf 'global hook\n' >>"${PANAMA_HOSTILE_GIT_SENTINEL:?}"
exit 0 exit 97
) >/dev/null 2>&1 EOF
case $? in cat >"$hostile/template/hooks/pre-commit" <<'EOF'
0) ;; #!/usr/bin/env bash
3) note 'the conflict fixture did not conflict, so this check proves nothing' ;; # PANAMA_HOSTILE_TEMPLATE_HOOK
4) note 'git no longer keeps the stash after a conflicted pop; panama update would lose work' ;; printf 'template hook\n' >>"${PANAMA_HOSTILE_TEMPLATE_SENTINEL:?}"
5) note 'git reset --hard left conflict markers behind' ;; exit 98
*) note 'the stash conflict fixture could not be built' ;; EOF
esac chmod +x "$hostile/hooks/pre-commit" "$hostile/template/hooks/pre-commit"
cat >"$hostile/global.gitconfig" <<EOF
[core]
hooksPath = $hostile/hooks
[commit]
gpgSign = true
[init]
templateDir = $hostile/template
EOF
cp "$hostile/global.gitconfig" "$hostile/system.gitconfig"
# panama update must act on that: reset the tree rather than leave the markers. export HOME="$hostile/home"
body="$(sed -n '/^cmd_update()/,/^}/p' "$panama")" export XDG_CONFIG_HOME="$hostile/xdg-config"
if [[ -z "$body" ]]; then export XDG_STATE_HOME="$hostile/xdg-state"
note 'bin/panama has no cmd_update to check' export XDG_CACHE_HOME="$hostile/xdg-cache"
export XDG_DATA_HOME="$hostile/xdg-data"
export BASH_ENV="$hostile/bash-env"
export PANAMA_HOSTILE_PROFILE_SENTINEL="$hostile/profile-sourced"
export PANAMA_HOSTILE_BASH_ENV_SENTINEL="$hostile/bash-env-sourced"
export PANAMA_HOSTILE_GIT_SENTINEL="$hostile/global-config-sourced"
export PANAMA_HOSTILE_TEMPLATE_SENTINEL="$hostile/template-hook-sourced"
export GIT_CONFIG_NOSYSTEM=0
export GIT_CONFIG_SYSTEM="$hostile/system.gitconfig"
export GIT_CONFIG_GLOBAL="$hostile/global.gitconfig"
export GIT_CONFIG_COUNT=1
export GIT_CONFIG_KEY_0=core.hooksPath
export GIT_CONFIG_VALUE_0="$hostile/hooks"
export GIT_TEMPLATE_DIR="$hostile/template"
prepare_cli_fixture_environment() {
local root="$1"
mkdir -p "$root/home" "$root/xdg-config" "$root/xdg-state" \
"$root/xdg-cache" "$root/xdg-data" "$root/xdg-runtime" \
"$root/empty-templates" "$root/empty-hooks"
chmod 700 "$root/xdg-runtime"
}
run_cli_fixture_environment() {
local root="$1"
shift
env -u BASH_ENV -u ENV -u GIT_CONFIG_PARAMETERS \
-u GIT_CONFIG_KEY_0 -u GIT_CONFIG_VALUE_0 \
HOME="$root/home" \
XDG_CONFIG_HOME="$root/xdg-config" \
XDG_STATE_HOME="$root/xdg-state" \
XDG_CACHE_HOME="$root/xdg-cache" \
XDG_DATA_HOME="$root/xdg-data" \
XDG_RUNTIME_DIR="$root/xdg-runtime" \
GIT_CONFIG_NOSYSTEM=1 \
GIT_CONFIG_SYSTEM=/dev/null \
GIT_CONFIG_GLOBAL=/dev/null \
GIT_CONFIG_COUNT=0 \
GIT_TEMPLATE_DIR="$root/empty-templates" \
"$@"
}
fixture_git() {
local root="$1"
shift
run_cli_fixture_environment "$root" \
git -c commit.gpgSign=false -c tag.gpgSign=false \
-c core.hooksPath="$root/empty-hooks" "$@"
}
configure_fixture_repo() {
local root="$1" repository="$2"
fixture_git "$root" -C "$repository" config user.email contract@panama || return 1
fixture_git "$root" -C "$repository" config user.name contract || return 1
fixture_git "$root" -C "$repository" config commit.gpgSign false || return 1
fixture_git "$root" -C "$repository" config tag.gpgSign false || return 1
fixture_git "$root" -C "$repository" config core.hooksPath "$root/empty-hooks" || return 1
}
# Each fixture has the same three repositories as a real update: a bare remote,
# a clone that publishes upstream changes, and the machine clone being updated.
build_cli_fixture() (
local root="$1"
rm -rf "$root" || return 1
mkdir -p "$root" || return 1
prepare_cli_fixture_environment "$root" || return 1
fixture_git "$root" init -q --bare "$root/origin.git" || return 1
fixture_git "$root" -C "$root/origin.git" config core.hooksPath "$root/empty-hooks" || return 1
fixture_git "$root" clone -q "$root/origin.git" "$root/upstream" 2>/dev/null || return 1
configure_fixture_repo "$root" "$root/upstream" || return 1
mkdir -p "$root/upstream/bin" || return 1
cp "$panama" "$root/upstream/bin/panama" || return 1
cat >"$root/upstream/install" <<'EOF' || return 1
#!/usr/bin/env bash
printf '%s\n' "$*" >>"${PANAMA_UPDATE_FIXTURE_LOG:?}"
exit "${PANAMA_UPDATE_INSTALL_RC:-0}"
EOF
chmod +x "$root/upstream/bin/panama" "$root/upstream/install" || return 1
printf 'one\n' >"$root/upstream/f" || return 1
fixture_git "$root" -C "$root/upstream" add -A || return 1
fixture_git "$root" -C "$root/upstream" commit -qm initial || return 1
fixture_git "$root" -C "$root/upstream" push -qu origin HEAD || return 1
fixture_git "$root" clone -q "$root/origin.git" "$root/machine" || return 1
configure_fixture_repo "$root" "$root/machine" || return 1
)
advance_upstream() (
local root="$1" file="$2" contents="$3"
printf '%s\n' "$contents" >"$root/upstream/$file" || return 1
fixture_git "$root" -C "$root/upstream" add "$file" || return 1
fixture_git "$root" -C "$root/upstream" commit -qm "update $file" || return 1
fixture_git "$root" -C "$root/upstream" push -q || return 1
)
# This write fails before the later Git commands. The helper must return that
# failure rather than let a final successful push hide it.
helper_probe="$tmp/helper-failure"
if build_cli_fixture "$helper_probe"; then
if advance_upstream "$helper_probe" missing/child probe \
2>"$helper_probe/intermediate-failure.err"; then
note 'advance_upstream hid an intermediate fixture setup failure'
fi
else else
grep -q 'git stash push' <<<"$body" \ note 'the fixture helper failure probe could not be built'
|| note 'cmd_update does not stash local changes, so a pull can fail on a dirty tree'
grep -q 'git reset --hard' <<<"$body" \
|| note 'cmd_update does not reset after a failed pop, so conflict markers reach ~/.config'
grep -q 'git pull --ff-only' <<<"$body" \
|| note 'cmd_update does not pull with --ff-only'
grep -q -- '--upgrade' <<<"$body" \
|| note 'cmd_update does not hand off to install --upgrade, so it would ask the interview'
fi fi
# The two verbs stay separate: sync must never run the installer. clean="$tmp/clean-update"
sync_body="$(sed -n '/^cmd_sync()/,/^}/p' "$panama")" if build_cli_fixture "$clean" && advance_upstream "$clean" release new; then
if [[ -z "$sync_body" ]]; then machine_before="$(fixture_git "$clean" -C "$clean/machine" rev-parse HEAD)"
note 'bin/panama has no cmd_sync, so the git workflow lost its home' upstream_after="$(fixture_git "$clean" -C "$clean/upstream" rev-parse HEAD)"
[[ "$machine_before" != "$upstream_after" ]] \
|| note 'the clean update fixture started current, so it cannot prove a fast-forward'
: >"$clean/install.log"
update_status=0
run_cli_fixture_environment "$clean" \
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
"$clean/machine/bin/panama" update >"$clean/update.out" 2>&1 \
|| update_status=$?
[[ "$update_status" -eq 0 ]] \
|| note "panama update failed on a clean clone with status $update_status"
[[ "$(fixture_git "$clean" -C "$clean/machine" rev-parse HEAD)" == "$upstream_after" ]] \
|| note 'panama update did not fast-forward the clean machine clone'
grep -qx -- '--upgrade' "$clean/install.log" \
|| note 'panama update did not invoke the installer with --upgrade'
: >"$clean/install.log"
update_status=0
run_cli_fixture_environment "$clean" \
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" PANAMA_UPDATE_INSTALL_RC=23 \
"$clean/machine/bin/panama" update >"$clean/failing-update.out" 2>&1 \
|| update_status=$?
[[ "$update_status" -eq 23 ]] \
|| note "panama update returned $update_status instead of installer status 23"
grep -qx -- '--upgrade' "$clean/install.log" \
|| note 'the failing update did not reach the fixture installer'
: >"$clean/install.log"
printf 'local sync\n' >"$clean/machine/synced"
sync_status=0
printf 'y\ncontract sync\n' \
| run_cli_fixture_environment "$clean" \
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
"$clean/machine/bin/panama" sync >"$clean/sync.out" 2>&1 \
|| sync_status=$?
[[ "$sync_status" -eq 0 ]] \
|| note "panama sync failed in the local fixture with status $sync_status"
[[ ! -s "$clean/install.log" ]] \
|| note 'panama sync invoked the installer; sync and update are separate jobs'
else else
grep -q 'install' <<<"$sync_body" \ note 'the clean update fixture could not be built'
&& note 'cmd_sync runs the installer; committing and updating are separate jobs'
grep -q 'git stash' <<<"$sync_body" \
&& note 'cmd_sync stashes, which the commit-before-pull order exists to avoid'
fi fi
# A conflicted stash pop must not leave markers in the checkout. The failed pop
# keeps the stash, so the local version remains recoverable after the reset.
conflict="$tmp/conflict-update"
if build_cli_fixture "$conflict"; then
printf 'local\n' >"$conflict/machine/f"
if advance_upstream "$conflict" f upstream; then
: >"$conflict/install.log"
conflict_status=0
run_cli_fixture_environment "$conflict" \
PANAMA_UPDATE_FIXTURE_LOG="$conflict/install.log" \
"$conflict/machine/bin/panama" update >"$conflict/update.out" 2>&1 \
|| conflict_status=$?
[[ "$conflict_status" -eq 0 ]] \
|| note "panama update failed while recovering a stash conflict with status $conflict_status"
[[ "$(<"$conflict/machine/f")" == upstream ]] \
|| note 'panama update did not reset the conflicted file to the upstream version'
if fixture_git "$conflict" -C "$conflict/machine" \
grep -qE '^(<<<<<<<|=======|>>>>>>>)' -- .; then
note 'panama update left conflict markers in the machine checkout'
fi
[[ -n "$(fixture_git "$conflict" -C "$conflict/machine" stash list)" ]] \
|| note 'panama update dropped the stash after its conflicted pop'
recovered="$(fixture_git "$conflict" -C "$conflict/machine" \
show 'stash@{0}:f' 2>/dev/null)"
[[ "$recovered" == local ]] \
|| note 'the stash left by panama update does not contain the local version'
else
note 'the conflict fixture could not publish its upstream edit'
fi
else
note 'the conflict update fixture could not be built'
fi
for sentinel in profile-sourced bash-env-sourced global-config-sourced template-hook-sourced; do
[[ ! -e "$hostile/$sentinel" ]] \
|| note "the update Git fixture consumed hostile state: $sentinel"
done
template_copy="$(
find "$tmp" -path "$hostile" -prune -o \
-type f -path '*/hooks/pre-commit' \
-exec grep -lF 'PANAMA_HOSTILE_TEMPLATE_HOOK' {} + 2>/dev/null
)"
[[ -z "$template_copy" ]] \
|| note "the update Git fixture copied a hostile template hook: $template_copy"
if (( ${#findings[@]} > 0 )); then if (( ${#findings[@]} > 0 )); then
printf 'update command contract: %d finding(s)\n' "${#findings[@]}" >&2 printf 'update command contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2