Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a69cacc006 | ||
|
|
28e387868f | ||
|
|
1f65e09865 | ||
|
|
dce00e45d1 | ||
|
|
0d1841cf86 | ||
|
|
ba4e5e6677 | ||
|
|
1ffd05f0e1 | ||
|
|
92c60c3ecd | ||
|
|
31fc1d3eaa | ||
|
|
8264ca6320 | ||
|
|
eab4280969 | ||
|
|
1c3cd7ac72 | ||
|
|
8652f92aae | ||
|
|
4abf3f8acd | ||
|
|
3f8539176a | ||
|
|
18ecc2244f | ||
|
|
94a4314730 | ||
|
|
27acbe7a95 | ||
|
|
fc8f226747 | ||
|
|
98e29eb8f3 | ||
|
|
00e11e0b8d | ||
|
|
ac25fd776c | ||
|
|
51e2c8418a | ||
|
|
d267da58ad | ||
|
|
20b41c3976 | ||
|
|
bfb37afd69 | ||
|
|
b361db8486 | ||
|
|
12b858371e | ||
|
|
77f625d7bb | ||
|
|
34e295d184 | ||
|
|
e19fb00276 | ||
|
|
37688f702a | ||
|
|
71f4f0d24b | ||
|
|
3839183401 | ||
|
|
68b6664111 | ||
|
|
803362ebad | ||
|
|
6a7bdcb835 | ||
|
|
37fd5e890e | ||
|
|
a19c6dc3ef | ||
|
|
6d71a77798 | ||
|
|
d8144a66c1 | ||
|
|
43d1e15858 | ||
|
|
1192ad64dc | ||
|
|
044139ed63 | ||
|
|
0d894afe3e |
@@ -138,10 +138,16 @@ steps, each checked and skipped when already true:
|
||||
exists, use it.
|
||||
2. Ensure the user has a password (needed for sudo) — `passwd` interactively
|
||||
if none is set.
|
||||
3. Copy root's `authorized_keys` to the user when the user has none.
|
||||
4. Offer sshd hardening (yes/no, default yes): drop-in under
|
||||
`/etc/ssh/sshd_config.d/` with `PermitRootLogin no`,
|
||||
`PasswordAuthentication no`, then reload sshd. Skipped when already set.
|
||||
3. Copy root's `authorized_keys` only after OpenSSH parses every key line. The
|
||||
destination directory and file are created as the target UID at `0700/0600`
|
||||
and revalidated before hardening is available.
|
||||
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
|
||||
`install --server` as that user.
|
||||
|
||||
|
||||
@@ -50,13 +50,17 @@ Editing `config/dot/quickshell/**` hot-reloads the live shell on every save.
|
||||
|
||||
## 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.
|
||||
- During a desktop session, run `panama test --safe [pattern]` — it skips everything listed in
|
||||
`tests/desktop-hijacking` (contracts that restart the shell, open real overlays, or write
|
||||
through the live compositor). Run the full `panama test` only with the user's go-ahead, and say
|
||||
so first: it visibly hijacks the desktop.
|
||||
- During a desktop session, run `panama test --safe [pattern]`. `--safe` selects only contracts
|
||||
classified as `hermetic`.
|
||||
- A plain full run prompts in a terminal before any selected non-hermetic contract starts. In
|
||||
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`.
|
||||
- After changing `PreferenceSchema.qml` or `services/SettingsRoutes.qml`, regenerate:
|
||||
`config/dot/quickshell/scripts/panama-settings-docs` (writes `docs/settings.md`) and
|
||||
@@ -88,4 +92,42 @@ it against the real system.
|
||||
|
||||
## 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
@@ -39,10 +39,10 @@ __pycache__/
|
||||
/config/firefox/chrome/panama-theme.css
|
||||
/config/firefox/chrome/panama-theme.css.tmp
|
||||
|
||||
# Build products of the Vicinae extension. The source is the repository's; the
|
||||
# dependency tree and the bundle it produces are machine state, rebuilt by
|
||||
# `panama apps`.
|
||||
# Build products of the Vicinae extension. The source and reviewed lockfile are
|
||||
# in the repository; the dependency tree and bundle are machine state.
|
||||
/config/local/share/vicinae/extensions/*/node_modules/
|
||||
/config/local/share/vicinae/extensions/*/dist/
|
||||
/config/local/share/vicinae/extensions/*/build/
|
||||
/config/local/share/vicinae/extensions/*/package-lock.json
|
||||
!/config/local/share/vicinae/extensions/panama-search/package-lock.json
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
# Panama
|
||||
|
||||
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
|
||||
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
|
||||
deliberately small enough to read first, and the same two steps by hand work
|
||||
identically:
|
||||
deliberately small enough to read first. The command downloads one immutable
|
||||
`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
|
||||
git clone https://git.gbrown.org/gib/Panama.git ~/.local/share/Panama
|
||||
~/.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:
|
||||
Once a machine exists, the command that keeps it current is `panama update` —
|
||||
one command, and it never asks you anything:
|
||||
|
||||
```sh
|
||||
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
|
||||
one thing Panama records durably (`~/.local/state/panama/role`), because
|
||||
`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
|
||||
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
|
||||
your user with sudo, moves the SSH key over, offers to harden sshd, and hands
|
||||
off to a normal install as that user.
|
||||
That command also works from a brand-new VPS's **root** login. It creates or
|
||||
reuses your sudo-enabled user, then copies a safe root key when it can or
|
||||
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/`
|
||||
in order, without stopping again:
|
||||
@@ -198,14 +247,26 @@ docs/ Settings reference, and the design specs behind the work
|
||||
|
||||
## 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
|
||||
panama test # everything
|
||||
panama test dock # just the ones matching "dock"
|
||||
panama test --safe
|
||||
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
|
||||
```
|
||||
|
||||
`--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:
|
||||
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
|
||||
@@ -243,8 +304,9 @@ panama sync # review, commit and push your changes to this repo
|
||||
panama edit # open it in Neovim
|
||||
panama doctor # what is actually running, not what was installed
|
||||
panama diagnose # hand the health summary and recent errors to your agent
|
||||
panama test # every contract, or a subset by pattern
|
||||
panama test --safe # the same, minus the ones that take over the desktop
|
||||
panama test # every contract, prompting before non-hermetic work
|
||||
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 migrate # apply repairs this machine has not had yet
|
||||
panama upgrade # re-run ./install from anywhere, interview and all
|
||||
|
||||
+372
-111
@@ -10,7 +10,7 @@
|
||||
# edit Open the Panama repo in Neovim
|
||||
# doctor Report what is actually running on this machine
|
||||
# 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
|
||||
# upgrade Re-run the installer from anywhere, interview included
|
||||
# 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.
|
||||
confirm() {
|
||||
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
|
||||
[[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]]
|
||||
}
|
||||
@@ -85,12 +85,18 @@ ${BOLD}Commands:${RESET}
|
||||
${GREEN}diagnose${RESET} Hand the health summary, the recent journal errors and
|
||||
whatever you say is wrong to your coding agent, in a terminal.
|
||||
Needs an agent chosen on Settings › System › Agents.
|
||||
${GREEN}test${RESET} Run every contract under tests/. Give it a pattern to run
|
||||
a subset: 'panama test dock' runs the ones matching 'dock'.
|
||||
--safe skips the ones that take over the live desktop; what
|
||||
they are and why is tests/desktop-hijacking.
|
||||
${GREEN}contracts${RESET} Name the contracts that mention a given file, each marked
|
||||
safe or desktop. A heuristic over the text of tests/, so it
|
||||
${GREEN}test${RESET} Run contracts classified by tests/contracts.manifest. Give it a
|
||||
pattern to run a subset. --safe selects hermetic contracts only.
|
||||
Plain terminal runs prompt before non-hermetic work. Automation
|
||||
must grant each required capability with a repeatable --allow.
|
||||
Each non-hermetic contract announces its exact capabilities
|
||||
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".
|
||||
${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
|
||||
@@ -121,6 +127,9 @@ ${BOLD}Examples:${RESET}
|
||||
$PROGRAM diagnose the bar disappears after unplugging the monitor
|
||||
$PROGRAM test dock
|
||||
$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 upgrade
|
||||
$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 '#'
|
||||
# comment saying what it does to the live session, the contracts that drive the
|
||||
# real shell, compositor or machine rather than a harness. Read by `test --safe`
|
||||
# to decide what to skip, and by `contracts` to mark each hit.
|
||||
#
|
||||
# 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"
|
||||
# The manifest is the runtime authority for every collected contract. An absent
|
||||
# manifest is unsafe: this command must never infer that unclassified tests are
|
||||
# hermetic.
|
||||
CONTRACT_MANIFEST="tests/contracts.manifest"
|
||||
CONTRACT_CAPABILITIES=(hermetic live-host live-compositor live-desktop network privileged)
|
||||
|
||||
hijacking_entries() {
|
||||
local ledger="$PANAMA_DIR/$DESKTOP_HIJACKING_LEDGER" line
|
||||
[[ -r "$ledger" ]] || return 0
|
||||
contract_paths() {
|
||||
local candidate
|
||||
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
|
||||
line="${line%%#*}"
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
[[ -n "$line" ]] && printf '%s\n' "$line"
|
||||
done < "$ledger"
|
||||
return 0
|
||||
[[ "$line" =~ ^[[:space:]]*(#|$) ]] && continue
|
||||
IFS=$' \t' read -r capabilities path <<<"$line"
|
||||
printf '%s\t%s\n' "$path" "$capabilities"
|
||||
done < "$PANAMA_DIR/$CONTRACT_MANIFEST"
|
||||
}
|
||||
|
||||
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
|
||||
# ----------------------------------------------------------------------------
|
||||
#
|
||||
# The contracts are the main safety net in this repository and had no entry
|
||||
# point: 121 executables with no runner and no mention in the README, which is
|
||||
# most of the way to not having them.
|
||||
# The contracts are the main safety net in this repository. The manifest is the
|
||||
# single list of what the runner executes and which external boundaries each
|
||||
# contract reaches.
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
# open overlays, restart the shell, move your windows. Running the suite while
|
||||
# sitting in front of the machine used to mean losing the session for a few
|
||||
# minutes, so the honest options were "run everything" or "run nothing". --safe
|
||||
# is the third: skip exactly what tests/desktop-hijacking names, and say how
|
||||
# many were skipped, so the gap is stated rather than implied.
|
||||
# --safe runs only contracts the manifest classifies as hermetic and reports
|
||||
# each external capability it skipped. A plain terminal run asks before any
|
||||
# selected non-hermetic work. Automation must grant every required capability
|
||||
# with repeatable --allow flags. Non-hermetic contracts announce their exact
|
||||
# capability list before execution. Each contract gets an outer timeout, 180
|
||||
# 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() {
|
||||
local pattern="" safe=0 arg
|
||||
# Position-independent, because 'panama test --safe dock' and
|
||||
# 'panama test dock --safe' are the same intent and nobody should have to
|
||||
# remember which one this accepts.
|
||||
for arg in "$@"; do
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
--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
|
||||
done
|
||||
|
||||
local -a suite=()
|
||||
local -A hijacking=()
|
||||
local skipped=0 entry rel
|
||||
(( safe == 0 || ${#grants[@]} == 0 )) || {
|
||||
err '--safe cannot be combined with --allow.'
|
||||
return 2
|
||||
}
|
||||
|
||||
if (( safe )); then
|
||||
while IFS= read -r entry; do
|
||||
hijacking["$entry"]=1
|
||||
done < <(hijacking_entries)
|
||||
fi
|
||||
|
||||
# Executables, plus the Python suites. Those are unittest files rather than
|
||||
# executables, and collecting only what has the executable bit would skip them
|
||||
# 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 ))
|
||||
while IFS=$'\t' read -r rel capabilities; do
|
||||
manifest_capabilities["$rel"]="$capabilities"
|
||||
[[ -z "$pattern" || "$rel" == *"$pattern"* ]] || continue
|
||||
if (( safe )) && [[ "$capabilities" != hermetic ]]; then
|
||||
IFS=',' read -r -a capability_list <<<"$capabilities"
|
||||
for capability in "${capability_list[@]}"; do
|
||||
(( ++skipped_counts["$capability"] ))
|
||||
done
|
||||
continue
|
||||
fi
|
||||
suite+=("$path")
|
||||
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
||||
suite+=("$rel")
|
||||
done < <(contract_manifest_entries)
|
||||
|
||||
if (( ${#suite[@]} == 0 )); then
|
||||
# "Nothing matched" and "everything that matched was skipped" are different
|
||||
# answers, and reporting the first for the second is how --safe would come
|
||||
# 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
|
||||
if (( safe )) && (( ${#skipped_counts[@]} > 0 )); then
|
||||
err "Every contract matching '${pattern}' needs an external capability; --safe skipped all of them."
|
||||
else
|
||||
err "No contracts match '${pattern}'"
|
||||
fi
|
||||
exit 1
|
||||
return 1
|
||||
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)"
|
||||
local -a failed=()
|
||||
local path name
|
||||
local -a runner
|
||||
for path in "${suite[@]}"; do
|
||||
name="${path#"$PANAMA_DIR"/tests/}"
|
||||
for index in "${!suite[@]}"; do
|
||||
rel="${suite[$index]}"
|
||||
path="$PANAMA_DIR/$rel"
|
||||
name="${rel#tests/}"
|
||||
stdout_file="$capture_dir/$index.stdout"
|
||||
stderr_file="$capture_dir/$index.stderr"
|
||||
if [[ "$path" == *_test.py ]]; then
|
||||
runner=(python3 "$path")
|
||||
else
|
||||
runner=("$path")
|
||||
fi
|
||||
if "${runner[@]}" >/dev/null 2>&1; then
|
||||
ok "$name"
|
||||
else
|
||||
err "$name"
|
||||
failed+=("$name")
|
||||
capabilities="${manifest_capabilities[$rel]}"
|
||||
if [[ "$capabilities" != hermetic ]]; then
|
||||
info "Running $name [$capabilities]"
|
||||
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
|
||||
|
||||
header "Result"
|
||||
if (( ${#failed[@]} == 0 )); then
|
||||
ok "${#suite[@]} contract(s) passed"
|
||||
if (( safe )); then
|
||||
printf 'Skipped %d desktop-hijacking contract(s) (%s).\n' "$skipped" "$DESKTOP_HIJACKING_LEDGER"
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
err "${#failed[@]} of ${#suite[@]} failed:"
|
||||
printf ' %s\n' "${failed[@]}" >&2
|
||||
final_status=1
|
||||
fi
|
||||
err "${#failed[@]} of ${#suite[@]} failed:"
|
||||
printf ' %s\n' "${failed[@]}" >&2
|
||||
warn "Run one on its own to see why: ${BOLD}${PANAMA_DIR}/tests/<name>${RESET}"
|
||||
if (( safe )); then
|
||||
printf 'Skipped %d desktop-hijacking contract(s) (%s).\n' "$skipped" "$DESKTOP_HIJACKING_LEDGER"
|
||||
fi
|
||||
return 1
|
||||
|
||||
cleanup_contract_capture
|
||||
trap - EXIT INT TERM
|
||||
return "$final_status"
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Command: contracts
|
||||
# ----------------------------------------------------------------------------
|
||||
#
|
||||
# "I changed this file -- what should I run?" There are 177 contracts and no
|
||||
# index, so the honest answers were "all of them" (minutes, and half of them
|
||||
# take the desktop away) or "the ones whose name sounds related" (which is how
|
||||
# a covering contract gets skipped).
|
||||
# "I changed this file -- what should I run?" The suite is large enough that
|
||||
# running all of it or guessing from contract names are both poor answers.
|
||||
#
|
||||
# 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
|
||||
@@ -577,8 +837,8 @@ cmd_test() {
|
||||
# a real answer -- exit 1 so a script can tell the difference -- but it is a
|
||||
# statement about this search, not about the file.
|
||||
#
|
||||
# Each hit is marked from tests/desktop-hijacking, so the output also answers
|
||||
# "and can I run them right now".
|
||||
# Each hit is labeled from tests/contracts.manifest, so the output also answers
|
||||
# which boundary the matching contract reaches.
|
||||
cmd_contracts() {
|
||||
local target="${1:-}"
|
||||
if [[ -z "$target" ]]; then
|
||||
@@ -621,21 +881,22 @@ cmd_contracts() {
|
||||
suffix="${suffix#*/}"
|
||||
done
|
||||
|
||||
local -A hijacking=()
|
||||
local entry
|
||||
while IFS= read -r entry; do
|
||||
hijacking["$entry"]=1
|
||||
done < <(hijacking_entries)
|
||||
validate_contract_manifest || return 1
|
||||
local -A manifest_capabilities=()
|
||||
local capabilities
|
||||
while IFS=$'\t' read -r rel capabilities; do
|
||||
manifest_capabilities["$rel"]="$capabilities"
|
||||
done < <(contract_manifest_entries)
|
||||
|
||||
# The same collection `test` runs, so anything named here is something the
|
||||
# runner would actually execute.
|
||||
local -a hits=()
|
||||
local candidate rel
|
||||
while IFS= read -r candidate; do
|
||||
[[ -x "$candidate" || "$candidate" == *_test.py ]] || continue
|
||||
while IFS= read -r rel; do
|
||||
candidate="$PANAMA_DIR/$rel"
|
||||
grep -qF "${patterns[@]}" "$candidate" 2>/dev/null || continue
|
||||
hits+=("tests/${candidate#"$PANAMA_DIR"/tests/}")
|
||||
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
||||
hits+=("$rel")
|
||||
done < <(contract_paths)
|
||||
|
||||
if (( ${#hits[@]} == 0 )); then
|
||||
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
|
||||
|
||||
for rel in "${hits[@]}"; do
|
||||
if [[ -n "${hijacking[$rel]:-}" ]]; then
|
||||
printf '%s [desktop]\n' "$rel"
|
||||
else
|
||||
printf '%s [safe]\n' "$rel"
|
||||
fi
|
||||
[[ -n "${manifest_capabilities[$rel]:-}" ]] || {
|
||||
err "Contract has no manifest capability label: $rel"
|
||||
return 1
|
||||
}
|
||||
printf '%s [%s]\n' "$rel" "${manifest_capabilities[$rel]}"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
# 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)
|
||||
# bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot) --server
|
||||
# Download this file from the documented commit URL, verify its documented
|
||||
# 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
|
||||
# moment somebody curls it -- nothing here can be fixed by re-running
|
||||
@@ -17,9 +18,144 @@
|
||||
|
||||
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"
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
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
|
||||
INSTALL_ARGS=()
|
||||
@@ -33,6 +169,290 @@ for arg in "$@"; do
|
||||
esac
|
||||
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.
|
||||
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
|
||||
@@ -84,83 +504,86 @@ if [[ "$(id -u)" -eq 0 ]]; then
|
||||
passwd "$username" </dev/tty
|
||||
fi
|
||||
|
||||
# The key that reached root is the key that should reach the user, or the
|
||||
# next SSH login has no way in once root logins are closed below.
|
||||
user_home="$(getent passwd "$username" | cut -d: -f6)"
|
||||
if [[ -s /root/.ssh/authorized_keys && ! -s "$user_home/.ssh/authorized_keys" ]]; then
|
||||
echo "Copying root's authorized_keys to $username"
|
||||
mkdir -p "$user_home/.ssh"
|
||||
cp /root/.ssh/authorized_keys "$user_home/.ssh/authorized_keys"
|
||||
chmod 700 "$user_home/.ssh"
|
||||
chmod 600 "$user_home/.ssh/authorized_keys"
|
||||
chown -R "$username:$username" "$user_home/.ssh"
|
||||
# Do not close root/password access until the account's key is an exact,
|
||||
# usable login path. The fixture adapter resolves these logical system paths
|
||||
# beneath a temporary root; ordinary execution receives the original paths.
|
||||
logical_user_home="$(getent passwd "$username" | cut -d: -f6)"
|
||||
user_home=""
|
||||
if [[ "$logical_user_home" == /* ]]; then
|
||||
user_home="$(system_path "$logical_user_home")" || true
|
||||
fi
|
||||
bootstrap_home="$user_home"
|
||||
if [[ -z "$bootstrap_home" ]]; then
|
||||
bootstrap_home="$(system_path "/home/$username")"
|
||||
fi
|
||||
|
||||
# Offered rather than imposed, defaulting to yes: a VPS keeps its provider's
|
||||
# web console, so locking password and root logins out of sshd is
|
||||
# recoverable even when it goes wrong. Written as a drop-in so it never
|
||||
# fights the distribution's own sshd_config.
|
||||
SSHD_DROPIN=/etc/ssh/sshd_config.d/90-panama.conf
|
||||
if [[ -f "$SSHD_DROPIN" ]]; then
|
||||
echo "sshd is already hardened ($SSHD_DROPIN)"
|
||||
else
|
||||
printf 'Harden sshd (disable root login and password auth)? [Y/n]: '
|
||||
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."
|
||||
user_ssh_dir="$user_home/.ssh"
|
||||
user_keys="$user_ssh_dir/authorized_keys"
|
||||
if [[ -n "$user_home" && ! -e "$user_keys" && ! -L "$user_keys" \
|
||||
&& ! -L "$user_ssh_dir" ]] && safe_root_authorized_keys; then
|
||||
copy_root_key=0
|
||||
if [[ ! -e "$user_ssh_dir" ]]; then
|
||||
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
|
||||
copy_root_key=1
|
||||
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
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "Installing git, which the clone needs"
|
||||
dnf install -y git
|
||||
dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git
|
||||
fi
|
||||
|
||||
# Cloned straight into the user's home and owned by them: this is the
|
||||
# checkout `panama update` will pull from for the life of the machine, and
|
||||
# a root-owned .git in a user's home is a wound that never heals.
|
||||
PANAMA_PATH="$user_home/.local/share/Panama"
|
||||
if [[ -d "$PANAMA_PATH/.git" ]]; then
|
||||
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
|
||||
# Create or advance the checkout as the target user. A root-owned .git in a
|
||||
# user's home would break every later update.
|
||||
PANAMA_PATH="$bootstrap_home/.local/share/Panama"
|
||||
BOOTSTRAP_USER="$username"
|
||||
prepare_panama_checkout "$PANAMA_PATH"
|
||||
|
||||
echo "Handing off to install as $username"
|
||||
exec runuser -u "$username" -- env PANAMA_PATH="$PANAMA_PATH" \
|
||||
"$PANAMA_PATH/install" --server </dev/tty
|
||||
verified_install_handoff 1
|
||||
fi
|
||||
|
||||
# git is the one dependency the clone itself needs. Everything else -- gum
|
||||
# included -- is bootstrapped by `install`.
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
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
|
||||
|
||||
if [[ -d "$PANAMA_PATH/.git" ]]; then
|
||||
# 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
|
||||
prepare_panama_checkout "$PANAMA_PATH"
|
||||
|
||||
# `curl | bash` and `bash <(curl ...)` can leave stdin as the pipe, and the
|
||||
# first thing install runs is the interview, which has to be able to ask.
|
||||
# A shell invoked from automation can have a pipe as stdin, while the first
|
||||
# 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
|
||||
# so itself.
|
||||
# The probe actually opens /dev/tty rather than testing -r: a process with no
|
||||
# controlling terminal passes -r and then fails the redirect.
|
||||
handoff_tty=0
|
||||
if [[ ! -t 0 ]] && (exec </dev/tty) 2>/dev/null; then
|
||||
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
|
||||
handoff_tty=1
|
||||
fi
|
||||
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
|
||||
verified_install_handoff "$handoff_tty"
|
||||
|
||||
@@ -14,7 +14,11 @@ takes to run the suite.
|
||||
|
||||
Usage:
|
||||
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 --output PATH
|
||||
exit 1 if PATH is stale
|
||||
panama-settings-docs --stdout print without writing
|
||||
|
||||
Where each group of settings lives comes from SettingsSearch.qml's routing and
|
||||
@@ -264,7 +268,14 @@ def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--check", action="store_true")
|
||||
parser.add_argument("--stdout", action="store_true")
|
||||
parser.add_argument("--output", type=pathlib.Path, default=OUTPUT)
|
||||
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:
|
||||
rendered = render(read_entries(), read_routes(), read_titles())
|
||||
@@ -279,19 +290,19 @@ def main():
|
||||
return 0
|
||||
|
||||
if args.check:
|
||||
if not OUTPUT.exists():
|
||||
print("panama-settings-docs: docs/settings.md has never been generated",
|
||||
if not output.exists():
|
||||
print(f"panama-settings-docs: {displayed_output} has never been generated",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
if OUTPUT.read_text() != rendered:
|
||||
print("panama-settings-docs: docs/settings.md is stale; re-run this "
|
||||
if output.read_text() != rendered:
|
||||
print(f"panama-settings-docs: {displayed_output} is stale; re-run this "
|
||||
"script and commit the result", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT.write_text(rendered)
|
||||
print(f"wrote {OUTPUT.relative_to(ROOT)}")
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(rendered)
|
||||
print(f"wrote {displayed_output}")
|
||||
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.
|
||||
@@ -22,7 +22,7 @@
|
||||
# * setup-identity and install-hardware are dropped. They exist only to
|
||||
# consume interview answers -- git identity, NVIDIA, Secure Boot, firmware
|
||||
# -- 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.
|
||||
#
|
||||
# Everything else is shared on purpose: the sudo keepalive, the per-stage
|
||||
@@ -64,12 +64,15 @@ done
|
||||
|
||||
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
|
||||
# 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
|
||||
# changed, so this hashes them and remembers the result.
|
||||
# time. On an upgrade it is worth running only when its package lists or
|
||||
# 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 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"
|
||||
PACKAGES_HASH="$STATE_DIR/packages-hash"
|
||||
|
||||
hash_packages() {
|
||||
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -exec sha256sum {} + \
|
||||
| sort | sha256sum | cut -d' ' -f1
|
||||
_collect_package_inputs() {
|
||||
local destination="$1" raw="${1}.raw"
|
||||
[[ -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() {
|
||||
local current_hash recorded_hash
|
||||
|
||||
current_hash="$(hash_packages)" || return 2
|
||||
PACKAGE_START_HASH="$current_hash"
|
||||
(( FORCE_PACKAGES )) && return 0
|
||||
(( UPGRADE )) || 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
|
||||
# documents for its markers: a step that did not complete has not happened, and
|
||||
# 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"
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
#
|
||||
@@ -137,7 +236,8 @@ if (( ! UPGRADE )); then
|
||||
fi
|
||||
if (( ${#bootstrap[@]} > 0 )); then
|
||||
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
|
||||
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 --
|
||||
# which is why this is a flag rather than a rewrite of seven stage scripts.
|
||||
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
|
||||
|
||||
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"
|
||||
[[ -x "$script" ]] || continue
|
||||
printf '\n=== %s ===\n' "$stage"
|
||||
if [[ "$stage" == install-packages ]] && ! packages_needed; then
|
||||
echo "The package lists have not changed since the last run; skipping."
|
||||
echo "Run with --packages to install them anyway."
|
||||
continue
|
||||
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 "Run with --packages to install them anyway."
|
||||
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 "$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
|
||||
failed+=("$stage")
|
||||
printf '!!! %s failed\n' "$stage" >&2
|
||||
elif [[ "$stage" == install-packages ]]; then
|
||||
record_packages_hash
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
Executable
+151
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -53,7 +53,12 @@ if [[ "${PANAMA_NVIDIA:-no}" == yes ]]; then
|
||||
warn "Secure Boot question, or disable Secure Boot first."
|
||||
else
|
||||
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
|
||||
# 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.
|
||||
|
||||
+1618
-175
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,75 @@
|
||||
|
||||
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}"
|
||||
vicinae_data_dir="${VICINAE_DATA_DIR:-$HOME/.local/share/vicinae}"
|
||||
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
|
||||
name="$(basename "$extension")"
|
||||
|
||||
# Skip a build that would produce what is already there. `npm install`
|
||||
# alone takes long enough to be worth not repeating on every re-run of
|
||||
# a stage that is otherwise nearly instant.
|
||||
# Skip only when a prior successful build records the digest of both
|
||||
# manifests and every source byte. Directory mtimes do not change when
|
||||
# an existing source file is edited.
|
||||
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"
|
||||
continue
|
||||
fi
|
||||
|
||||
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
|
||||
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
|
||||
done
|
||||
elif [[ -d "$extensions_source" ]]; then
|
||||
|
||||
@@ -40,7 +40,21 @@ Other tools that beat raw commands:
|
||||
- `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 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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -355,13 +355,17 @@ for line in sys.stdin:
|
||||
STUB
|
||||
chmod +x "$stub/codex"
|
||||
|
||||
mkdir -p "$work/codex/sessions/2026/08/25"
|
||||
cat >"$work/codex/sessions/2026/08/25/session.jsonl" <<'JSONL'
|
||||
{"type":"turn_context","payload":{"model":"gpt-5.6-sol"}}
|
||||
{"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}}}}
|
||||
{"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
|
||||
touch "$work/codex/sessions/2026/08/25/session.jsonl"
|
||||
session_day="$(date +%Y/%m/%d)"
|
||||
session_date="$(date +%Y-%m-%d)"
|
||||
session_offset="$(date +%:z)"
|
||||
mkdir -p "$work/codex/sessions/$session_day"
|
||||
{
|
||||
printf '%s\n' '{"type":"turn_context","payload":{"model":"gpt-5.6-sol"}}'
|
||||
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="$(
|
||||
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
|
||||
# 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.
|
||||
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
|
||||
# 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)$'
|
||||
|
||||
# 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
|
||||
# all (codex comes through npm), and node, npm and pnpm come from nvm on
|
||||
# purpose -- a dnf nodejs earlier on PATH would win every per-project
|
||||
# `nvm use`, which is the whole point of having nvm.
|
||||
# reasons, both deliberate: bun and codex use reviewed release archives,
|
||||
# Claude Code and pnpm use signed DNF repositories, and node/npm use a reviewed
|
||||
# Node archive inside nvm's version directory. A system nodejs earlier on PATH
|
||||
# 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.
|
||||
SELF_INSTALLED='^(bun|claude|codex|node|npm|pnpm)$'
|
||||
|
||||
|
||||
@@ -27,8 +27,7 @@
|
||||
# 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.
|
||||
#
|
||||
# The geometry checks launch isolated shells against a temporary config. The
|
||||
# real settings are read to build them and never written.
|
||||
# The geometry checks launch isolated shells against a temporary config.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
@@ -47,8 +46,138 @@ for path in "$dock" "$body" "$strip"; do
|
||||
[[ -r "$path" ]] || fail "missing $path"
|
||||
done
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work" "$shell_dir/dock-position-probe.qml"' EXIT
|
||||
# Keep this contract honest: it must never add a probe to the linked source
|
||||
# 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 ───────────────────────────────────────────────
|
||||
|
||||
@@ -111,7 +240,8 @@ PY
|
||||
|
||||
# ── 1, 2, 7. Geometry, measured ────────────────────────────────────────────────
|
||||
|
||||
cat >"$shell_dir/dock-position-probe.qml" <<'QML'
|
||||
probe="$fixture_shell/$probe_name"
|
||||
cat >"$probe" <<'QML'
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.modules.dock
|
||||
@@ -164,28 +294,50 @@ ShellRoot {
|
||||
}
|
||||
QML
|
||||
|
||||
settings_source="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json"
|
||||
[[ -r "$settings_source" ]] || fail 'no settings to build a probe configuration from'
|
||||
|
||||
measure() {
|
||||
local position="$1"
|
||||
python3 - "$settings_source" "$work/panama/settings.json" "$position" <<'PY'
|
||||
python3 - "$work/config/panama/settings.json" "$position" <<'PY'
|
||||
import json, pathlib, sys
|
||||
data = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
||||
data["dockPosition"] = sys.argv[3]
|
||||
out = pathlib.Path(sys.argv[2])
|
||||
out = pathlib.Path(sys.argv[1])
|
||||
data = {
|
||||
"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.write_text(json.dumps(data))
|
||||
out.write_text(json.dumps(data) + "\n")
|
||||
PY
|
||||
( cd "$shell_dir" && XDG_CONFIG_HOME="$work" timeout 40 qs -p ./dock-position-probe.qml 2>&1 ) \
|
||||
| grep -oE '(DOCKGEOM|MASKGEOM) .*'
|
||||
local output="$work/$position.output"
|
||||
(
|
||||
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'
|
||||
left="$(measure left)"
|
||||
measure left
|
||||
left="$geometry"
|
||||
[[ -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'
|
||||
|
||||
# The span checks read the window; the region checks read every line.
|
||||
|
||||
@@ -25,7 +25,34 @@ fail() {
|
||||
|
||||
[[ -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'
|
||||
assert_doc_unchanged || fail 'the default --check changed tracked docs/settings.md'
|
||||
|
||||
# A generator that silently emitted nothing would also pass --check against an
|
||||
# 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
|
||||
# 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"
|
||||
printf '\n<!-- drift -->\n' >>"$doc"
|
||||
if "$generator" --check >/dev/null 2>&1; then
|
||||
cp "$scratch/settings.md" "$doc"
|
||||
printf '\n<!-- drift -->\n' >>"$scratch/settings.md"
|
||||
if "$generator" --check --output "$scratch/settings.md" >/dev/null 2>&1; then
|
||||
fail '--check reported success on a modified file, so staleness would never be caught'
|
||||
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"
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
# instead of being handed whole to a Text element and to anybody reading
|
||||
# over a shoulder.
|
||||
#
|
||||
# Runs entirely against a temporary config home. The real settings store is read
|
||||
# for the export and never written.
|
||||
# Runs entirely against temporary config homes: one explicit source fixture and
|
||||
# one isolated import destination. The real settings store is never consulted.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
@@ -52,6 +52,29 @@ trap 'rm -rf "$work"' EXIT
|
||||
bundle="$work/export.json"
|
||||
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 ───────────────────────────────
|
||||
|
||||
"$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" ]] \
|
||||
|| 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
|
||||
bundle = json.load(open(sys.argv[1]))
|
||||
schema = open(sys.argv[2]).read()
|
||||
@@ -105,6 +128,16 @@ PY
|
||||
# ── 3, 4. Arrival is validated per key, and the types are all covered ───────
|
||||
|
||||
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'
|
||||
import json, sys
|
||||
@@ -208,18 +241,15 @@ PY
|
||||
|
||||
# ── 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'
|
||||
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
|
||||
store = json.load(open(sys.argv[1]))
|
||||
if store.get("aSettingTheBundleNeverMentions") != "kept":
|
||||
raise SystemExit('a setting the bundle did not mention was removed')
|
||||
bundle = json.load(open(sys.argv[1]))["settings"]
|
||||
store = json.load(open(sys.argv[2]))
|
||||
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
|
||||
|
||||
applied="$("$helper" import "$bundle" | field "['applied']")"
|
||||
|
||||
@@ -24,36 +24,47 @@ note() { findings+=("$1"); }
|
||||
|
||||
[[ -d "$server_dir" ]] || { printf 'compose secrets contract: no server/ directory\n' >&2; exit 1; }
|
||||
|
||||
# ── 1. Tracked content is clean ──────────────────────────────────────────────
|
||||
#
|
||||
# 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.
|
||||
# ── 1. Scanner fixtures and tracked content ──────────────────────────────────
|
||||
|
||||
while IFS= read -r file; do
|
||||
path="$repo_dir/$file"
|
||||
[[ -f "$path" ]] || continue
|
||||
scanner="$repo_dir/tests/server/scan-tracked-secrets.py"
|
||||
fixtures_dir="$repo_dir/tests/server/fixtures/secrets"
|
||||
|
||||
# A secret-bearing key with a literal value. ${VAR} interpolations, empty
|
||||
# values, the CHANGE_ME placeholder, and booleans (ALLOW_EMPTY_PASSWORD=yes
|
||||
# is a switch, not a credential) are the allowed shapes; anything else
|
||||
# after PASSWORD/SECRET/TOKEN/KEY is treated as a leak. Keys that merely
|
||||
# configure where a secret lives (a *_FILE path, a key NAME) are not
|
||||
# values.
|
||||
if ! python3 "$scanner" "$fixtures_dir/clean" \
|
||||
compose.yml .env.example README.md signature-near-misses.txt; then
|
||||
note 'the clean secret-scanning fixture was rejected'
|
||||
fi
|
||||
|
||||
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
|
||||
note "$file looks like it carries a secret: ${hit%%[=:]*}"
|
||||
done < <(grep -inE '(password|secret|token|api_key|private_key|access_key)[a-z0-9_]*[[:space:]]*[:=]' "$path" 2>/dev/null \
|
||||
| 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
|
||||
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/')
|
||||
[[ -n "$hit" ]] && note "$hit"
|
||||
done <<< "$output"
|
||||
fi
|
||||
|
||||
# ── 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
|
||||
@@ -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:]))
|
||||
+372
-53
@@ -1,18 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# The front door: `boot` is the script the README tells a fresh machine to
|
||||
# curl, so it runs before anything else Panama ships -- including its own
|
||||
# tests. What it must get right is small and worth pinning:
|
||||
#
|
||||
# * 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.
|
||||
# `boot` is downloaded before the repository exists. It may hand off only
|
||||
# after both the downloaded script and the requested Git commit have been
|
||||
# verified. This fixture stubs Git and install inside a throwaway PANAMA_PATH;
|
||||
# it never contacts the network or mutates the real checkout.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
@@ -24,17 +15,29 @@ note() { findings+=("$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)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
calls="$work/calls"
|
||||
state="$work/state"
|
||||
stub_dir="$work/bin"
|
||||
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
|
||||
#!/usr/bin/env bash
|
||||
printf 'install PANAMA_PATH=%s\n' "\${PANAMA_PATH:-unset}" >>"$calls"
|
||||
@@ -44,56 +47,372 @@ chmod +x "$work/fake-install"
|
||||
cat >"$stub_dir/git" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
printf 'git %s\n' "\$*" >>"$calls"
|
||||
if [[ "\$1" == "clone" ]]; then
|
||||
mkdir -p "\$3/.git"
|
||||
cp "$work/fake-install" "\$3/install"
|
||||
fi
|
||||
mode="\$(<"$state/mode")"
|
||||
|
||||
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
|
||||
[[ "\$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
|
||||
chmod +x "$stub_dir/git"
|
||||
|
||||
run_boot() {
|
||||
configure_case() {
|
||||
local mode="$1" head_revision="${2:-$revision}" status="${3:-}"
|
||||
rm -rf "$clone_dir"
|
||||
: >"$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" \
|
||||
|| note 'boot does not clone the documented repository to PANAMA_PATH'
|
||||
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|
||||
|| note 'boot does not hand off to the clone'\''s install with PANAMA_PATH exported'
|
||||
assert_no_git_or_install() {
|
||||
local case_name="$1"
|
||||
if grep -qE '^(git|install) ' "$calls"; then
|
||||
note "$case_name reached Git or install"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── A machine that already has it ────────────────────────────────────────────
|
||||
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"
|
||||
}
|
||||
|
||||
run_boot || note 'boot failed on a machine that already has the clone'
|
||||
# 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'
|
||||
)
|
||||
|
||||
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'
|
||||
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 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.
|
||||
# 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'
|
||||
|
||||
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'
|
||||
# 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
|
||||
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|
||||
|| note 'the install does not run when the fast-forward is refused'
|
||||
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
|
||||
printf 'boot contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
|
||||
Executable
+194
@@ -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[@]}"
|
||||
@@ -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
|
||||
# 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_installer="$(uncommented)"
|
||||
|
||||
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"
|
||||
done <<'FRAGILE'
|
||||
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)
|
||||
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'
|
||||
|
||||
# ── Soft failures are reported ──────────────────────────────────────────────
|
||||
|
||||
@@ -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-----
|
||||
@@ -103,10 +103,13 @@ fi
|
||||
|
||||
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'
|
||||
called "$nvidia" 'xorg-x11-drv-nvidia-cuda' \
|
||||
|| 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' \
|
||||
|| note 'the kernel arguments are never set'
|
||||
called "$nvidia" 'modprobe.blacklist=nouveau' \
|
||||
|
||||
@@ -55,6 +55,7 @@ fi
|
||||
# ── The extension ────────────────────────────────────────────────────────────
|
||||
|
||||
manifest="$extension/package.json"
|
||||
lockfile="$extension/package-lock.json"
|
||||
if [[ ! -f "$manifest" ]]; then
|
||||
note 'the panama-search extension has no manifest'
|
||||
else
|
||||
@@ -76,6 +77,15 @@ else
|
||||
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 ────────────────────────────────────────────────
|
||||
#
|
||||
# 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'
|
||||
|
||||
# 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'
|
||||
|
||||
# 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 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
|
||||
Executable
+2945
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,157 @@ panama="$repo_dir/bin/panama"
|
||||
|
||||
findings=()
|
||||
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 ───────────────────────────────────────────────────────
|
||||
#
|
||||
@@ -36,6 +187,86 @@ elif (( claimed != actual )); then
|
||||
note "the README says $claimed contracts; there are $actual"
|
||||
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 ─────────────────────────────────────────────
|
||||
#
|
||||
# A README listing a command the dispatcher does not have sends somebody to a
|
||||
|
||||
Executable
+1440
File diff suppressed because it is too large
Load Diff
Executable
+496
@@ -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'
|
||||
@@ -37,19 +37,37 @@ tmp="$(mktemp -d -t panama-update-contract.XXXXXX)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
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
|
||||
# records what it was asked to do instead of doing it.
|
||||
build_fixture() {
|
||||
local root="$1" packages_rc="${2:-0}"
|
||||
local root="$1" packages_rc="${2:-0}" trust_rc="${3:-0}"
|
||||
rm -rf "$root"
|
||||
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"
|
||||
: >"$root/bin/ascii"
|
||||
printf 'base-package\n' >"$root/setup/packages/base"
|
||||
copy_hash_inputs "$root"
|
||||
|
||||
local stage
|
||||
for stage in "${STAGE_NAMES[@]}"; do
|
||||
@@ -62,6 +80,10 @@ EOF
|
||||
# The one stage whose exit code the caller wants to control.
|
||||
cat >"$root/setup/scripts/install-packages" <<EOF
|
||||
#!/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"
|
||||
exit $packages_rc
|
||||
EOF
|
||||
@@ -102,26 +124,92 @@ EOF
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
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"/*
|
||||
}
|
||||
|
||||
# Run the fixture installer and echo what ran, one stage per line.
|
||||
run_install() {
|
||||
local root="$1"; shift
|
||||
local status=0
|
||||
: >"$root/ran"
|
||||
PATH="$root/shim:$PATH" \
|
||||
PANAMA_PATH="$root" \
|
||||
PANAMA_RAN="$root/ran" \
|
||||
XDG_STATE_HOME="$root/state" \
|
||||
bash "$root/install" "$@" >"$root/out" 2>&1
|
||||
printf '%s' "$?" >"$root/rc"
|
||||
PATH="$root/shim:$PATH" PANAMA_PATH="$root" PANAMA_RAN="$root/ran" \
|
||||
XDG_STATE_HOME="$root/state" TMPDIR="$root/tmp" \
|
||||
/usr/bin/setsid bash "$root/install" "$@" \
|
||||
>"$root/out" 2>&1 || status=$?
|
||||
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 ────────────────────────────────
|
||||
|
||||
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
|
||||
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.
|
||||
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
|
||||
note 'a plain ./install no longer asks the interview, so a new machine is never configured'
|
||||
fi
|
||||
@@ -146,12 +237,28 @@ done
|
||||
|
||||
# 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.
|
||||
mapfile -t declared < <(python3 - "$installer" <<'PY'
|
||||
import re, sys
|
||||
line = next(l for l in open(sys.argv[1], encoding="utf-8") if l.startswith("STAGES="))
|
||||
print("\n".join(re.findall(r"[\w-]+", line)[1:]))
|
||||
declared=()
|
||||
if declared_output="$(python3 - "$installer" <<'PY'
|
||||
import re
|
||||
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
|
||||
)
|
||||
)"; 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
|
||||
printf '%s\n' "${STAGE_NAMES[@]}" | grep -qx "$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 ───
|
||||
|
||||
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.
|
||||
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" \
|
||||
&& note 'install-packages ran again with the package lists unchanged'
|
||||
|
||||
# --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" \
|
||||
|| note '--packages did not force install-packages to run'
|
||||
|
||||
# A changed list brings the stage back.
|
||||
printf 'another-package\n' >>"$tmp/a/setup/packages/base"
|
||||
ran_changed="$(run_install "$tmp/a" --upgrade)"
|
||||
grep -qx 'install-packages' <<<"$ran_changed" \
|
||||
|| note 'a changed package list did not bring install-packages back'
|
||||
# Every current package and provenance member is part of the state definition.
|
||||
# Dynamically discovering them makes this fail when a new reviewed input is
|
||||
# added but omitted from hash_packages.
|
||||
for relative in "${package_inputs[@]}" "${provenance_inputs[@]}" \
|
||||
'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.
|
||||
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
|
||||
note 'install-packages failed but its hash was recorded, so it will never be retried'
|
||||
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.
|
||||
build_fixture "$tmp/d"
|
||||
run_install "$tmp/d" --upgrade >/dev/null
|
||||
ran_full="$(run_install "$tmp/d")"
|
||||
install_status=0
|
||||
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" \
|
||||
|| note 'a full ./install skipped install-packages because of a recorded hash'
|
||||
|
||||
# ── 4. A conflicted pop never leaves markers in a live config ────────────────
|
||||
#
|
||||
# Two halves. The first checks that git still behaves the way the design
|
||||
# depends on; the second checks that panama acts on it. Neither is worth much
|
||||
# without the other.
|
||||
# The fixture also covers a clean fast-forward, installer status propagation,
|
||||
# and the boundary between update and sync before forcing the conflict below.
|
||||
|
||||
conflict="$tmp/conflict"
|
||||
mkdir -p "$conflict"
|
||||
(
|
||||
set -e
|
||||
cd "$conflict"
|
||||
git init -q up && cd up
|
||||
git config user.email contract@panama && git config user.name contract
|
||||
printf 'one\n' >f; git add -A; git commit -qm one
|
||||
cd "$conflict"; git clone -q up work; cd work
|
||||
git config user.email contract@panama && git config user.name contract
|
||||
cd "$conflict/up"; printf 'upstream\n' >f; git commit -qam two
|
||||
cd "$conflict/work"; printf 'local\n' >f
|
||||
git stash push --include-untracked -m contract >/dev/null
|
||||
git pull -q --ff-only
|
||||
git stash pop >/dev/null 2>&1 && exit 3 # a conflict was the point
|
||||
git reset -q --hard HEAD
|
||||
[[ -n "$(git stash list)" ]] || exit 4 # the stash must survive
|
||||
grep -q '<<<<<<<' f && exit 5 # and no markers may remain
|
||||
exit 0
|
||||
) >/dev/null 2>&1
|
||||
case $? in
|
||||
0) ;;
|
||||
3) note 'the conflict fixture did not conflict, so this check proves nothing' ;;
|
||||
4) note 'git no longer keeps the stash after a conflicted pop; panama update would lose work' ;;
|
||||
5) note 'git reset --hard left conflict markers behind' ;;
|
||||
*) note 'the stash conflict fixture could not be built' ;;
|
||||
esac
|
||||
# Make every ambient configuration source hostile before constructing the Git
|
||||
# fixtures. A hermetic fixture overrides these values with its own empty state;
|
||||
# consuming any of them either leaves a sentinel or prevents a commit.
|
||||
hostile="$tmp/hostile-environment"
|
||||
mkdir -p "$hostile/home" "$hostile/xdg-config" "$hostile/xdg-state" \
|
||||
"$hostile/xdg-cache" "$hostile/xdg-data" "$hostile/hooks" \
|
||||
"$hostile/template/hooks"
|
||||
for profile in .bash_profile .bashrc .profile; do
|
||||
cat >"$hostile/home/$profile" <<'EOF'
|
||||
printf 'profile\n' >>"${PANAMA_HOSTILE_PROFILE_SENTINEL:?}"
|
||||
EOF
|
||||
done
|
||||
cat >"$hostile/bash-env" <<'EOF'
|
||||
printf 'BASH_ENV\n' >>"${PANAMA_HOSTILE_BASH_ENV_SENTINEL:?}"
|
||||
EOF
|
||||
cat >"$hostile/hooks/pre-commit" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf 'global hook\n' >>"${PANAMA_HOSTILE_GIT_SENTINEL:?}"
|
||||
exit 97
|
||||
EOF
|
||||
cat >"$hostile/template/hooks/pre-commit" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# PANAMA_HOSTILE_TEMPLATE_HOOK
|
||||
printf 'template hook\n' >>"${PANAMA_HOSTILE_TEMPLATE_SENTINEL:?}"
|
||||
exit 98
|
||||
EOF
|
||||
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.
|
||||
body="$(sed -n '/^cmd_update()/,/^}/p' "$panama")"
|
||||
if [[ -z "$body" ]]; then
|
||||
note 'bin/panama has no cmd_update to check'
|
||||
export HOME="$hostile/home"
|
||||
export XDG_CONFIG_HOME="$hostile/xdg-config"
|
||||
export XDG_STATE_HOME="$hostile/xdg-state"
|
||||
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
|
||||
grep -q 'git stash push' <<<"$body" \
|
||||
|| 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'
|
||||
note 'the fixture helper failure probe could not be built'
|
||||
fi
|
||||
|
||||
# The two verbs stay separate: sync must never run the installer.
|
||||
sync_body="$(sed -n '/^cmd_sync()/,/^}/p' "$panama")"
|
||||
if [[ -z "$sync_body" ]]; then
|
||||
note 'bin/panama has no cmd_sync, so the git workflow lost its home'
|
||||
clean="$tmp/clean-update"
|
||||
if build_cli_fixture "$clean" && advance_upstream "$clean" release new; then
|
||||
machine_before="$(fixture_git "$clean" -C "$clean/machine" rev-parse HEAD)"
|
||||
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
|
||||
grep -q 'install' <<<"$sync_body" \
|
||||
&& 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'
|
||||
note 'the clean update fixture could not be built'
|
||||
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
|
||||
printf 'update command contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
|
||||
Reference in New Issue
Block a user