Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99156442b5 | ||
|
|
4c27203214 | ||
|
|
c71d6c8799 | ||
|
|
cb305f6662 | ||
|
|
1ee42f2cb6 | ||
|
|
1ae3825fda | ||
|
|
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 |
@@ -9,7 +9,7 @@ You are editing a running desktop, not a codebase that gets deployed later. Ever
|
|||||||
repository is symlinked into `~/.config`, so a save is live the moment it lands. That single fact
|
repository is symlinked into `~/.config`, so a save is live the moment it lands. That single fact
|
||||||
drives every rule below.
|
drives every rule below.
|
||||||
|
|
||||||
Read before large work, in this order:
|
Before large work, read these in order:
|
||||||
|
|
||||||
- `README.md` — layout, the `panama` command, how installing and updating work
|
- `README.md` — layout, the `panama` command, how installing and updating work
|
||||||
- `config/dot/hypr/README.md` — the compositor config is **Lua, not hyprlang**; read "The one
|
- `config/dot/hypr/README.md` — the compositor config is **Lua, not hyprlang**; read "The one
|
||||||
@@ -50,15 +50,20 @@ Editing `config/dot/quickshell/**` hot-reloads the live shell on every save.
|
|||||||
|
|
||||||
## The contract discipline
|
## The contract discipline
|
||||||
|
|
||||||
~180 executable contracts under `tests/` pin behavior. The rules:
|
Every executable contract under `tests/` is classified in `tests/contracts.manifest`. The rules:
|
||||||
|
|
||||||
- `panama contracts <file>` names the contracts that mention a file; run those after touching it.
|
- `panama contracts <file>` names the contracts that mention a file; run those after touching it.
|
||||||
- During a desktop session, run `panama test --safe [pattern]` — it skips everything listed in
|
- During a desktop session, run `panama test --safe [pattern]`. `--safe` selects only contracts
|
||||||
`tests/desktop-hijacking` (contracts that restart the shell, open real overlays, or write
|
classified as `hermetic`.
|
||||||
through the live compositor). Run the full `panama test` only with the user's go-ahead, and say
|
- A plain full run prompts in a terminal before any selected non-hermetic contract starts. In
|
||||||
so first: it visibly hijacks the desktop.
|
automation, grant every required capability with repeatable flags, for example
|
||||||
|
`panama test --allow live-compositor --allow live-desktop keybinds`.
|
||||||
|
- Each contract has a 180-second outer timeout by default. Override it with a positive
|
||||||
|
`PANAMA_TEST_TIMEOUT_SECONDS` value. Failures print the contract's captured stdout and stderr.
|
||||||
|
Successful stdout stays quiet. Successful stderr is surfaced as a warning.
|
||||||
- Contracts run directly too: `tests/setup/interview-contract`.
|
- Contracts run directly too: `tests/setup/interview-contract`.
|
||||||
- After changing `PreferenceSchema.qml` or `services/SettingsRoutes.qml`, regenerate:
|
- After changing `PreferenceSchema.qml` or
|
||||||
|
`config/dot/quickshell/services/SettingsRoutes.qml`, regenerate:
|
||||||
`config/dot/quickshell/scripts/panama-settings-docs` (writes `docs/settings.md`) and
|
`config/dot/quickshell/scripts/panama-settings-docs` (writes `docs/settings.md`) and
|
||||||
`config/dot/quickshell/scripts/panama-settings-commands` (writes the launcher deep links).
|
`config/dot/quickshell/scripts/panama-settings-commands` (writes the launcher deep links).
|
||||||
Both take `--check`; `tests/quickshell/settings-docs-contract` fails when stale.
|
Both take `--check`; `tests/quickshell/settings-docs-contract` fails when stale.
|
||||||
@@ -88,4 +93,42 @@ it against the real system.
|
|||||||
|
|
||||||
## Root work
|
## Root work
|
||||||
|
|
||||||
Never bare `sudo` — load the `panama-sudo` skill. Migrations already follow the rule.
|
Never bare `sudo`. Load the `panama-sudo` skill first. Migrations already follow the rule.
|
||||||
|
|
||||||
|
## Initial bootstrap trust
|
||||||
|
|
||||||
|
Use the complete command in `README.md` for a new machine. It downloads `boot`
|
||||||
|
from the documented commit URL into a private temporary directory, enforces the
|
||||||
|
documented curl time and byte limits, verifies the committed blob with
|
||||||
|
`sha256sum -c`, and passes both pins to the verified script. Desktop and server
|
||||||
|
bootstrap use the same commit and digest. Never substitute a branch URL, pipe a
|
||||||
|
response into Bash, or invent a newer pin from an uncommitted file.
|
||||||
|
|
||||||
|
`boot` validates the full lowercase commit and SHA-256 before Git or install. A
|
||||||
|
fresh destination fetches only that revision and creates a tracked local `main`
|
||||||
|
after HEAD matches. An existing checkout must be clean and an ancestor of the
|
||||||
|
requested revision; it advances with fast-forward only. Dirty, divergent,
|
||||||
|
fetch-failed, or mismatched checkouts stop without reset or install. Once Panama
|
||||||
|
exists, use `panama update` for normal updates.
|
||||||
|
|
||||||
|
`boot --server` is the one exception where a new Fedora VPS may begin as root.
|
||||||
|
Before it offers SSH hardening, it copies a safe root key when possible or
|
||||||
|
verifies the target key. The target user's `.ssh` must be owned by that user at
|
||||||
|
`0700`, and `authorized_keys` must be owned by that user at `0600`. Without a
|
||||||
|
verified target key, SSH hardening is unavailable and the bootstrap continues.
|
||||||
|
Every non-comment key line must parse with `ssh-keygen`. Root-key destination
|
||||||
|
creation and writing run as the target UID, followed by the same owner, mode,
|
||||||
|
and key checks. Do not replace that with root writes or assume the user's
|
||||||
|
primary group matches the username.
|
||||||
|
|
||||||
|
Accepted hardening uses atomic same-directory `00-panama.conf` with exactly
|
||||||
|
`PermitRootLogin no`, `PasswordAuthentication no`, and
|
||||||
|
`KbdInteractiveAuthentication no`. A pre-existing symlink or non-regular
|
||||||
|
object makes hardening unavailable, as does a missing SSH unit. Panama runs
|
||||||
|
`sshd -t`, then checks effective root and target-user policy with `sshd -T -C`
|
||||||
|
before reloading the detected unit. Validation or reload failure restores a
|
||||||
|
prior regular file with its metadata before it retries validation and reload.
|
||||||
|
Failed recovery stops the handoff with instructions that distinguish a prior
|
||||||
|
file from no prior file. The fixture contracts also cover declined hardening
|
||||||
|
and interrupted preparation. `panama test --safe` never reloads a live daemon,
|
||||||
|
so it is not live-host proof.
|
||||||
@@ -138,10 +138,16 @@ steps, each checked and skipped when already true:
|
|||||||
exists, use it.
|
exists, use it.
|
||||||
2. Ensure the user has a password (needed for sudo) — `passwd` interactively
|
2. Ensure the user has a password (needed for sudo) — `passwd` interactively
|
||||||
if none is set.
|
if none is set.
|
||||||
3. Copy root's `authorized_keys` to the user when the user has none.
|
3. Copy root's `authorized_keys` only after OpenSSH parses every key line. The
|
||||||
4. Offer sshd hardening (yes/no, default yes): drop-in under
|
destination directory and file are created as the target UID at `0700/0600`
|
||||||
`/etc/ssh/sshd_config.d/` with `PermitRootLogin no`,
|
and revalidated before hardening is available.
|
||||||
`PasswordAuthentication no`, then reload sshd. Skipped when already set.
|
4. Offer sshd hardening (yes/no, default yes): atomically install
|
||||||
|
`/etc/ssh/sshd_config.d/00-panama.conf` with `PermitRootLogin no`,
|
||||||
|
`PasswordAuthentication no`, and `KbdInteractiveAuthentication no`. Run
|
||||||
|
`sshd -t` plus effective root/target `sshd -T -C` checks before reloading the
|
||||||
|
detected unit. Restore a prior regular file with metadata on failure. A
|
||||||
|
missing unit or unsupported existing drop-in leaves SSH unchanged and
|
||||||
|
bootstrap continues. The later binding transaction design owns the details.
|
||||||
5. Move/clone the checkout under the user's home, chown it, and re-exec
|
5. Move/clone the checkout under the user's home, chown it, and re-exec
|
||||||
`install --server` as that user.
|
`install --server` as that user.
|
||||||
|
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/panama
|
||||||
+3
-3
@@ -39,10 +39,10 @@ __pycache__/
|
|||||||
/config/firefox/chrome/panama-theme.css
|
/config/firefox/chrome/panama-theme.css
|
||||||
/config/firefox/chrome/panama-theme.css.tmp
|
/config/firefox/chrome/panama-theme.css.tmp
|
||||||
|
|
||||||
# Build products of the Vicinae extension. The source is the repository's; the
|
# Build products of the Vicinae extension. The source and reviewed lockfile are
|
||||||
# dependency tree and the bundle it produces are machine state, rebuilt by
|
# in the repository; the dependency tree and bundle are machine state.
|
||||||
# `panama apps`.
|
|
||||||
/config/local/share/vicinae/extensions/*/node_modules/
|
/config/local/share/vicinae/extensions/*/node_modules/
|
||||||
/config/local/share/vicinae/extensions/*/dist/
|
/config/local/share/vicinae/extensions/*/dist/
|
||||||
/config/local/share/vicinae/extensions/*/build/
|
/config/local/share/vicinae/extensions/*/build/
|
||||||
/config/local/share/vicinae/extensions/*/package-lock.json
|
/config/local/share/vicinae/extensions/*/package-lock.json
|
||||||
|
!/config/local/share/vicinae/extensions/panama-search/package-lock.json
|
||||||
|
|||||||
@@ -1,25 +1,37 @@
|
|||||||
# Panama
|
# Panama
|
||||||
|
|
||||||
Formerly Sunhat. A personal config for Fedora, with the intention of helping a
|
Formerly Sunhat. A personal config for Fedora, with the intention of helping a
|
||||||
user set up their Fedora system with one command.
|
user set up their Fedora system with one checked command.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot)
|
(
|
||||||
|
set -euo pipefail
|
||||||
|
bootstrap_commit=0d1841cf86975d52485c94c0caa5d1b67e5c54a1
|
||||||
|
bootstrap_sha=fd8dcd70d1266da109972df346e3d85d1037f6f6e1942b0157064412ab892372
|
||||||
|
bootstrap_dir="$(umask 077; mktemp -d)"
|
||||||
|
trap 'rm -rf -- "$bootstrap_dir"' EXIT
|
||||||
|
bootstrap="$bootstrap_dir/boot"
|
||||||
|
curl --fail --location --connect-timeout 10 --max-time 30 --max-filesize 262144 \
|
||||||
|
--output "$bootstrap" \
|
||||||
|
"https://git.gbrown.org/gib/Panama/raw/commit/$bootstrap_commit/boot"
|
||||||
|
printf '%s %s\n' "$bootstrap_sha" "$bootstrap" >"$bootstrap_dir/boot.sha256"
|
||||||
|
sha256sum -c "$bootstrap_dir/boot.sha256"
|
||||||
|
PANAMA_BOOT_REVISION="$bootstrap_commit" PANAMA_BOOT_SHA256="$bootstrap_sha" \
|
||||||
|
bash "$bootstrap"
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
`boot` installs git if the machine lacks it, clones this repository to
|
`boot` installs git if the machine lacks it, creates a verified checkout at
|
||||||
`~/.local/share/Panama` (or `$PANAMA_PATH`), and hands off to `install`. It is
|
`~/.local/share/Panama` (or `$PANAMA_PATH`), and hands off to `install`. It is
|
||||||
deliberately small enough to read first, and the same two steps by hand work
|
deliberately small enough to read first. The command downloads one immutable
|
||||||
identically:
|
`boot` blob into a private temporary directory, checks its SHA-256, then asks
|
||||||
|
that verified script to fetch the same commit. A clean existing checkout may
|
||||||
|
fast-forward to that exact revision. A dirty or divergent checkout, a failed
|
||||||
|
fetch, or a revision mismatch stops before `install`; Panama never resets local
|
||||||
|
work or falls back to whatever checkout happens to be present.
|
||||||
|
|
||||||
```sh
|
Once a machine exists, the command that keeps it current is `panama update` —
|
||||||
git clone https://git.gbrown.org/gib/Panama.git ~/.local/share/Panama
|
one command, and it never asks you anything:
|
||||||
~/.local/share/Panama/install
|
|
||||||
```
|
|
||||||
|
|
||||||
Both are safe to run again: an existing clone is fast-forwarded rather than
|
|
||||||
replaced. Once a machine exists, though, the command that keeps it current is
|
|
||||||
`panama update` — one command, and it never asks you anything:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
panama update
|
panama update
|
||||||
@@ -35,15 +47,52 @@ shell environment minus everything that needs a screen, plus rootless podman
|
|||||||
and the compose services in [`server/`](server/README.md). The answer is the
|
and the compose services in [`server/`](server/README.md). The answer is the
|
||||||
one thing Panama records durably (`~/.local/state/panama/role`), because
|
one thing Panama records durably (`~/.local/state/panama/role`), because
|
||||||
`panama update` asks nothing and still has to know. A fresh server skips the
|
`panama update` asks nothing and still has to know. A fresh server skips the
|
||||||
question entirely:
|
question entirely. It uses the same immutable boot blob and pins as the desktop
|
||||||
|
command; only the final `--server` argument differs:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot) --server
|
(
|
||||||
|
set -euo pipefail
|
||||||
|
bootstrap_commit=0d1841cf86975d52485c94c0caa5d1b67e5c54a1
|
||||||
|
bootstrap_sha=fd8dcd70d1266da109972df346e3d85d1037f6f6e1942b0157064412ab892372
|
||||||
|
bootstrap_dir="$(umask 077; mktemp -d)"
|
||||||
|
trap 'rm -rf -- "$bootstrap_dir"' EXIT
|
||||||
|
bootstrap="$bootstrap_dir/boot"
|
||||||
|
curl --fail --location --connect-timeout 10 --max-time 30 --max-filesize 262144 \
|
||||||
|
--output "$bootstrap" \
|
||||||
|
"https://git.gbrown.org/gib/Panama/raw/commit/$bootstrap_commit/boot"
|
||||||
|
printf '%s %s\n' "$bootstrap_sha" "$bootstrap" >"$bootstrap_dir/boot.sha256"
|
||||||
|
sha256sum -c "$bootstrap_dir/boot.sha256"
|
||||||
|
PANAMA_BOOT_REVISION="$bootstrap_commit" PANAMA_BOOT_SHA256="$bootstrap_sha" \
|
||||||
|
bash "$bootstrap" --server
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
That command also works from a brand-new VPS's **root** login: it creates
|
That command also works from a brand-new VPS's **root** login. It creates or
|
||||||
your user with sudo, moves the SSH key over, offers to harden sshd, and hands
|
reuses your sudo-enabled user, then copies a safe root key when it can or
|
||||||
off to a normal install as that user.
|
verifies the target key before offering SSH hardening. A verified target key
|
||||||
|
means the target user owns `.ssh` with mode `0700` and `authorized_keys` with
|
||||||
|
mode `0600`. Every non-comment line in `authorized_keys` must be valid OpenSSH
|
||||||
|
key material that `ssh-keygen` can parse. Root-key destination writes run as
|
||||||
|
the target user, and Panama rechecks the resulting owner, modes, and keys. SSH
|
||||||
|
hardening is unavailable without a verified target key or installed SSH unit,
|
||||||
|
and the install continues without it. Declining hardening also leaves SSH
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
When you accept hardening, Panama uses an atomic same-directory drop-in named
|
||||||
|
`00-panama.conf`, whose early filename gives it safer precedence. A pre-existing
|
||||||
|
symlink or non-regular object makes hardening unavailable. The effective policy
|
||||||
|
is exactly `PermitRootLogin no`, `PasswordAuthentication no`, and
|
||||||
|
`KbdInteractiveAuthentication no`. Panama validates syntax with `sshd -t` and
|
||||||
|
checks `sshd -T -C` for both root and target-user contexts before it reloads
|
||||||
|
the detected SSH unit. If validation or reload fails, it restores the previous
|
||||||
|
drop-in regular file with its metadata and validates and reloads that restored
|
||||||
|
configuration. Recovery that cannot complete stops the handoff and prints the
|
||||||
|
right manual command for either a prior file or no prior file. The fixture
|
||||||
|
contracts test these branches. No real daemon reload runs under
|
||||||
|
`panama test --safe`, so that suite is not live-host proof.
|
||||||
|
|
||||||
|
After that, it hands off to a normal install as the new user.
|
||||||
|
|
||||||
`install` asks its questions first and then runs the stages in `setup/scripts/`
|
`install` asks its questions first and then runs the stages in `setup/scripts/`
|
||||||
in order, without stopping again:
|
in order, without stopping again:
|
||||||
@@ -53,7 +102,7 @@ in order, without stopping again:
|
|||||||
| `interview` | Every prompt, before anything is installed. Answers last one run and are never written to a durable path |
|
| `interview` | Every prompt, before anything is installed. Answers last one run and are never written to a durable path |
|
||||||
| `install-packages` | Repos (RPM Fusion, Terra, Hyprland COPR), the package lists in `setup/packages/`, then whichever optional categories were chosen |
|
| `install-packages` | Repos (RPM Fusion, Terra, Hyprland COPR), the package lists in `setup/packages/`, then whichever optional categories were chosen |
|
||||||
| `link-dotfiles` | Symlinks `config/dot/<name>` → `~/.config/<name>`, and seeds the wallpaper, cursor theme and Firefox chrome |
|
| `link-dotfiles` | Symlinks `config/dot/<name>` → `~/.config/<name>`, and seeds the wallpaper, cursor theme and Firefox chrome |
|
||||||
| `link-skills` | Links the agent skills in `skills/` into `~/.claude/skills`, one per skill. Every machine gets these; personal ones link after and win a name clash |
|
| `link-skills` | Links the agent skills in `skills/` into `~/.agents/skills` and `~/.claude/skills`, one per skill. Every machine gets these; personal ones link after and win a name clash |
|
||||||
| `link-user` | Links the personal content in `user/` — agent instructions, SSH host aliases — but only on a machine that answered yes. See [user/README.md](user/README.md) |
|
| `link-user` | Links the personal content in `user/` — agent instructions, SSH host aliases — but only on a machine that answered yes. See [user/README.md](user/README.md) |
|
||||||
| `change-settings` | Copies `config/copy/` over `/`, applies gsettings, enables user services |
|
| `change-settings` | Copies `config/copy/` over `/`, applies gsettings, enables user services |
|
||||||
| `link-vicinae-scripts` | Publishes the Vicinae script commands |
|
| `link-vicinae-scripts` | Publishes the Vicinae script commands |
|
||||||
@@ -186,10 +235,13 @@ server/ The server role: compose services (one directory per
|
|||||||
service), the nightly image updater, and its units. See
|
service), the nightly image updater, and its units. See
|
||||||
server/README.md
|
server/README.md
|
||||||
skills/ Agent skills for operating this desktop, linked into
|
skills/ Agent skills for operating this desktop, linked into
|
||||||
~/.claude/skills
|
~/.agents/skills and ~/.claude/skills
|
||||||
setup/
|
setup/
|
||||||
apps/ Applications built from source, one file each
|
apps/ Applications built from source, one file each
|
||||||
lib/ Shared by more than one stage; the extras catalog reader
|
keys/ Pinned signing keys, for publishers that ship no fetchable
|
||||||
|
one; setup/keys/README.md records where each came from
|
||||||
|
lib/ Shared by more than one stage; the extras catalog reader,
|
||||||
|
the machine role, the verified ChatGPT repository
|
||||||
packages/ One package per line; extras/ holds the optional categories
|
packages/ One package per line; extras/ holds the optional categories
|
||||||
scripts/ Run in order by ./install
|
scripts/ Run in order by ./install
|
||||||
tests/ Contracts. See below
|
tests/ Contracts. See below
|
||||||
@@ -198,14 +250,26 @@ docs/ Settings reference, and the design specs behind the work
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
185 of them, under `tests/`. Run the lot, or a subset by pattern:
|
189 of them, under `tests/`. `tests/contracts.manifest` classifies every
|
||||||
|
contract by the capabilities it needs. Run the hermetic set, or grant a
|
||||||
|
specific external capability when automation needs it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
panama test # everything
|
panama test --safe
|
||||||
panama test dock # just the ones matching "dock"
|
panama test --allow live-host updates
|
||||||
|
panama test --allow live-compositor keybinds
|
||||||
|
PANAMA_TEST_TIMEOUT_SECONDS=300 panama test --safe
|
||||||
tests/setup/interview-contract # or one directly; they are plain executables
|
tests/setup/interview-contract # or one directly; they are plain executables
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--safe` means hermetic contracts only. A plain full run prompts in a terminal
|
||||||
|
before it starts any non-hermetic contract. Automation cannot answer that
|
||||||
|
prompt, so it must grant every required capability with a repeatable `--allow`.
|
||||||
|
Each contract has a 180-second outer timeout by default. Set
|
||||||
|
`PANAMA_TEST_TIMEOUT_SECONDS` to a positive integer to override it. When a
|
||||||
|
contract fails, the runner prints its captured stdout and stderr. Successful
|
||||||
|
stdout stays quiet. Successful stderr is surfaced as a warning.
|
||||||
|
|
||||||
They are called contracts rather than unit tests because that is what they are:
|
They are called contracts rather than unit tests because that is what they are:
|
||||||
each one pins a decision that was expensive to get right and is cheap to undo by
|
each one pins a decision that was expensive to get right and is cheap to undo by
|
||||||
accident. Most read or measure the real thing — launching a shell to measure a
|
accident. Most read or measure the real thing — launching a shell to measure a
|
||||||
@@ -243,8 +307,9 @@ panama sync # review, commit and push your changes to this repo
|
|||||||
panama edit # open it in Neovim
|
panama edit # open it in Neovim
|
||||||
panama doctor # what is actually running, not what was installed
|
panama doctor # what is actually running, not what was installed
|
||||||
panama diagnose # hand the health summary and recent errors to your agent
|
panama diagnose # hand the health summary and recent errors to your agent
|
||||||
panama test # every contract, or a subset by pattern
|
panama test # every contract, prompting before non-hermetic work
|
||||||
panama test --safe # the same, minus the ones that take over the desktop
|
panama test --safe # hermetic contracts only
|
||||||
|
panama test --allow live-host updates # grant one capability to automation
|
||||||
panama contracts <file> # which contracts mention a file, and can they be run
|
panama contracts <file> # which contracts mention a file, and can they be run
|
||||||
panama migrate # apply repairs this machine has not had yet
|
panama migrate # apply repairs this machine has not had yet
|
||||||
panama upgrade # re-run ./install from anywhere, interview and all
|
panama upgrade # re-run ./install from anywhere, interview and all
|
||||||
|
|||||||
+379
-112
@@ -10,7 +10,7 @@
|
|||||||
# edit Open the Panama repo in Neovim
|
# edit Open the Panama repo in Neovim
|
||||||
# doctor Report what is actually running on this machine
|
# doctor Report what is actually running on this machine
|
||||||
# diagnose Hand this machine's health and recent errors to your agent
|
# diagnose Hand this machine's health and recent errors to your agent
|
||||||
# test Run every contract under tests/ (--safe skips the hijacking ones)
|
# test Run contracts classified by tests/contracts.manifest
|
||||||
# contracts Name the contracts that mention a given file
|
# contracts Name the contracts that mention a given file
|
||||||
# upgrade Re-run the installer from anywhere, interview included
|
# upgrade Re-run the installer from anywhere, interview included
|
||||||
# migrate Apply repairs this machine has not had yet
|
# migrate Apply repairs this machine has not had yet
|
||||||
@@ -57,7 +57,7 @@ header(){ printf '\n%s%s%s\n' "${MAGENTA}${BOLD}" "$*" "$RESET"; }
|
|||||||
# Ask a yes/no question. Returns 0 for yes, 1 for no. Default = no.
|
# Ask a yes/no question. Returns 0 for yes, 1 for no. Default = no.
|
||||||
confirm() {
|
confirm() {
|
||||||
local prompt="$1" reply
|
local prompt="$1" reply
|
||||||
printf '%s?%s %s %s[y/N]%s ' "${CYAN}${BOLD}" "$RESET" "$prompt" "$BOLD" "$RESET"
|
printf '%s?%s %s %s[y/N]%s ' "${CYAN}${BOLD}" "$RESET" "$prompt" "$BOLD" "$RESET" >&2
|
||||||
read -r reply || true
|
read -r reply || true
|
||||||
[[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]]
|
[[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]]
|
||||||
}
|
}
|
||||||
@@ -85,12 +85,18 @@ ${BOLD}Commands:${RESET}
|
|||||||
${GREEN}diagnose${RESET} Hand the health summary, the recent journal errors and
|
${GREEN}diagnose${RESET} Hand the health summary, the recent journal errors and
|
||||||
whatever you say is wrong to your coding agent, in a terminal.
|
whatever you say is wrong to your coding agent, in a terminal.
|
||||||
Needs an agent chosen on Settings › System › Agents.
|
Needs an agent chosen on Settings › System › Agents.
|
||||||
${GREEN}test${RESET} Run every contract under tests/. Give it a pattern to run
|
${GREEN}test${RESET} Run contracts classified by tests/contracts.manifest. Give it a
|
||||||
a subset: 'panama test dock' runs the ones matching 'dock'.
|
pattern to run a subset. --safe selects hermetic contracts only.
|
||||||
--safe skips the ones that take over the live desktop; what
|
Plain terminal runs prompt before non-hermetic work. Automation
|
||||||
they are and why is tests/desktop-hijacking.
|
must grant each required capability with a repeatable --allow.
|
||||||
${GREEN}contracts${RESET} Name the contracts that mention a given file, each marked
|
Each non-hermetic contract announces its exact capabilities
|
||||||
safe or desktop. A heuristic over the text of tests/, so it
|
before it starts.
|
||||||
|
Failures print captured stdout/stderr. Successful stdout stays
|
||||||
|
quiet; successful stderr is a warning. The default outer timeout
|
||||||
|
is 180 seconds. Set PANAMA_TEST_TIMEOUT_SECONDS to a positive
|
||||||
|
integer to override it.
|
||||||
|
${GREEN}contracts${RESET} Name the contracts that mention a given file, each labeled
|
||||||
|
with manifest capabilities. A heuristic over the text of tests/, so it
|
||||||
answers "what should I run" rather than "what covers this".
|
answers "what should I run" rather than "what covers this".
|
||||||
${GREEN}upgrade${RESET} Re-run ./install from anywhere, interview and all. For a new
|
${GREEN}upgrade${RESET} Re-run ./install from anywhere, interview and all. For a new
|
||||||
machine, or to change an answer you gave. Routine updates are
|
machine, or to change an answer you gave. Routine updates are
|
||||||
@@ -121,11 +127,13 @@ ${BOLD}Examples:${RESET}
|
|||||||
$PROGRAM diagnose the bar disappears after unplugging the monitor
|
$PROGRAM diagnose the bar disappears after unplugging the monitor
|
||||||
$PROGRAM test dock
|
$PROGRAM test dock
|
||||||
$PROGRAM test --safe
|
$PROGRAM test --safe
|
||||||
|
$PROGRAM test --allow live-host updates
|
||||||
|
$PROGRAM test --allow live-compositor keybinds
|
||||||
|
PANAMA_TEST_TIMEOUT_SECONDS=300 $PROGRAM test --safe
|
||||||
$PROGRAM contracts config/dot/quickshell/services/Displays.qml
|
$PROGRAM contracts config/dot/quickshell/services/Displays.qml
|
||||||
$PROGRAM upgrade
|
$PROGRAM upgrade
|
||||||
$PROGRAM apps
|
$PROGRAM apps
|
||||||
$PROGRAM app
|
$PROGRAM app
|
||||||
$PROGRAM app chatgpt-desktop
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,143 +439,401 @@ PROMPT
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# The desktop-hijacking ledger
|
# Contract manifest
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# tests/desktop-hijacking lists, one repo-relative path per line with a '#'
|
# The manifest is the runtime authority for every collected contract. An absent
|
||||||
# comment saying what it does to the live session, the contracts that drive the
|
# manifest is unsafe: this command must never infer that unclassified tests are
|
||||||
# real shell, compositor or machine rather than a harness. Read by `test --safe`
|
# hermetic.
|
||||||
# to decide what to skip, and by `contracts` to mark each hit.
|
CONTRACT_MANIFEST="tests/contracts.manifest"
|
||||||
#
|
CONTRACT_CAPABILITIES=(hermetic live-host live-compositor live-desktop network privileged)
|
||||||
# Prints the paths, comments and blank lines stripped. A missing ledger prints
|
|
||||||
# nothing: no ledger means nothing is known to hijack, which is the honest
|
|
||||||
# reading of an absent file and keeps `--safe` from failing on a fresh checkout.
|
|
||||||
DESKTOP_HIJACKING_LEDGER="tests/desktop-hijacking"
|
|
||||||
|
|
||||||
hijacking_entries() {
|
contract_paths() {
|
||||||
local ledger="$PANAMA_DIR/$DESKTOP_HIJACKING_LEDGER" line
|
local candidate
|
||||||
[[ -r "$ledger" ]] || return 0
|
# The manifest is kept in byte order, so both the discovery sort and the
|
||||||
|
# comparison below have to be byte order too. A UTF-8 collation folds the
|
||||||
|
# punctuation away -- `calendar_agenda_bridge_test.py` sorts before
|
||||||
|
# `calendar-agenda-helper-contract` under en_US and after it under C -- and
|
||||||
|
# a gate that passes or fails on the machine's LANG is not a gate.
|
||||||
|
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__*' | LC_ALL=C sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
contract_manifest_entries() {
|
||||||
|
local line capabilities path
|
||||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
line="${line%%#*}"
|
[[ "$line" =~ ^[[:space:]]*(#|$) ]] && continue
|
||||||
line="${line#"${line%%[![:space:]]*}"}"
|
IFS=$' \t' read -r capabilities path <<<"$line"
|
||||||
line="${line%"${line##*[![:space:]]}"}"
|
printf '%s\t%s\n' "$path" "$capabilities"
|
||||||
[[ -n "$line" ]] && printf '%s\n' "$line"
|
done < "$PANAMA_DIR/$CONTRACT_MANIFEST"
|
||||||
done < "$ledger"
|
}
|
||||||
return 0
|
|
||||||
|
require_contract_manifest() {
|
||||||
|
[[ -r "$PANAMA_DIR/$CONTRACT_MANIFEST" ]] || {
|
||||||
|
err "Contract manifest is missing or unreadable: $PANAMA_DIR/$CONTRACT_MANIFEST"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_contract_manifest() {
|
||||||
|
require_contract_manifest || return 1
|
||||||
|
|
||||||
|
local manifest="$PANAMA_DIR/$CONTRACT_MANIFEST"
|
||||||
|
# Byte order, for the same reason contract_paths sorts in it.
|
||||||
|
local LC_ALL=C
|
||||||
|
local line capabilities path extra previous_comment="" previous_was_comment=0
|
||||||
|
local previous_path="" capability discovered
|
||||||
|
local -a capability_list=() findings=()
|
||||||
|
local -A expected_contracts=() manifest_paths=()
|
||||||
|
|
||||||
|
while IFS= read -r discovered; do
|
||||||
|
expected_contracts["$discovered"]=1
|
||||||
|
done < <(contract_paths)
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*# ]]; then
|
||||||
|
previous_comment="${line#*#}"
|
||||||
|
previous_comment="${previous_comment#"${previous_comment%%[![:space:]]*}"}"
|
||||||
|
previous_comment="${previous_comment%"${previous_comment##*[![:space:]]}"}"
|
||||||
|
previous_was_comment=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*$ ]]; then
|
||||||
|
previous_comment=""
|
||||||
|
previous_was_comment=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=$' \t' read -r capabilities path extra <<<"$line"
|
||||||
|
if [[ -z "${capabilities:-}" || -z "${path:-}" || -n "${extra:-}" ]]; then
|
||||||
|
findings+=("manifest line is not exactly two fields: $line")
|
||||||
|
previous_comment=""
|
||||||
|
previous_was_comment=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$previous_path" && "$path" < "$previous_path" ]]; then
|
||||||
|
findings+=('paths are not lexicographically sorted')
|
||||||
|
fi
|
||||||
|
previous_path="$path"
|
||||||
|
|
||||||
|
if [[ -n "${manifest_paths[$path]:-}" ]]; then
|
||||||
|
findings+=("duplicate path $path")
|
||||||
|
fi
|
||||||
|
manifest_paths["$path"]=1
|
||||||
|
|
||||||
|
local -A line_capabilities=()
|
||||||
|
if [[ "$capabilities" == ,* || "$capabilities" == *, || "$capabilities" == *,,* ]]; then
|
||||||
|
findings+=("empty capability on $path")
|
||||||
|
fi
|
||||||
|
IFS=',' read -r -a capability_list <<<"$capabilities"
|
||||||
|
for capability in "${capability_list[@]}"; do
|
||||||
|
[[ -n "$capability" ]] || continue
|
||||||
|
if [[ -n "${line_capabilities[$capability]:-}" ]]; then
|
||||||
|
findings+=("duplicate capability $capability on $path")
|
||||||
|
fi
|
||||||
|
line_capabilities["$capability"]=1
|
||||||
|
is_contract_capability "$capability" \
|
||||||
|
|| findings+=("unknown capability $capability on $path")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "${line_capabilities[hermetic]:-}" && ${#line_capabilities[@]} -ne 1 ]]; then
|
||||||
|
findings+=("hermetic must appear alone on $path")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$capabilities" != hermetic ]]; then
|
||||||
|
if (( previous_was_comment != 1 )); then
|
||||||
|
findings+=("$path is non-hermetic but lacks a directly preceding comment")
|
||||||
|
elif [[ -z "$previous_comment" ]]; then
|
||||||
|
findings+=("$path is non-hermetic but lacks a non-empty directly preceding comment")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
previous_comment=""
|
||||||
|
previous_was_comment=0
|
||||||
|
done < "$manifest"
|
||||||
|
|
||||||
|
for discovered in "${!expected_contracts[@]}"; do
|
||||||
|
[[ -n "${manifest_paths[$discovered]:-}" ]] \
|
||||||
|
|| findings+=("missing contract $discovered")
|
||||||
|
done
|
||||||
|
for path in "${!manifest_paths[@]}"; do
|
||||||
|
[[ -n "${expected_contracts[$path]:-}" ]] \
|
||||||
|
|| findings+=("stale manifest path $path")
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( ${#findings[@]} > 0 )); then
|
||||||
|
err "Contract manifest validation failed with ${#findings[@]} finding(s):"
|
||||||
|
printf ' - %s\n' "${findings[@]}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_usage() {
|
||||||
|
err "Usage: ${BOLD}$PROGRAM test [--safe] [--allow <capability>] [pattern]${RESET}"
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
is_contract_capability() {
|
||||||
|
local capability="$1" known
|
||||||
|
for known in "${CONTRACT_CAPABILITIES[@]}"; do
|
||||||
|
[[ "$capability" == "$known" ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Command: test
|
# Command: test
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# The contracts are the main safety net in this repository and had no entry
|
# The contracts are the main safety net in this repository. The manifest is the
|
||||||
# point: 121 executables with no runner and no mention in the README, which is
|
# single list of what the runner executes and which external boundaries each
|
||||||
# most of the way to not having them.
|
# contract reaches.
|
||||||
#
|
#
|
||||||
# Each runs in its own process and a failure does not stop the rest, because the
|
# Each runs in its own process and a failure does not stop the rest, because the
|
||||||
# useful output is the whole list of what is broken rather than the first thing
|
# useful output is the whole list of what is broken rather than the first thing
|
||||||
# that broke. The exit code is what a caller can act on.
|
# that broke. The exit code is what a caller can act on.
|
||||||
#
|
#
|
||||||
# --safe exists because a fair number of these contracts ARE the desktop: they
|
# --safe runs only contracts the manifest classifies as hermetic and reports
|
||||||
# open overlays, restart the shell, move your windows. Running the suite while
|
# each external capability it skipped. A plain terminal run asks before any
|
||||||
# sitting in front of the machine used to mean losing the session for a few
|
# selected non-hermetic work. Automation must grant every required capability
|
||||||
# minutes, so the honest options were "run everything" or "run nothing". --safe
|
# with repeatable --allow flags. Non-hermetic contracts announce their exact
|
||||||
# is the third: skip exactly what tests/desktop-hijacking names, and say how
|
# capability list before execution. Each contract gets an outer timeout, 180
|
||||||
# many were skipped, so the gap is stated rather than implied.
|
# seconds by default. PANAMA_TEST_TIMEOUT_SECONDS accepts a positive integer
|
||||||
|
# override. Failures include captured stdout and stderr. Successful stdout
|
||||||
|
# stays quiet, while successful stderr is surfaced as a warning.
|
||||||
|
PANAMA_ACTIVE_CONTRACT_PID=""
|
||||||
|
PANAMA_CONTRACT_CAPTURE_DIR=""
|
||||||
|
|
||||||
|
cleanup_contract_capture() {
|
||||||
|
if [[ -n "$PANAMA_CONTRACT_CAPTURE_DIR" && -d "$PANAMA_CONTRACT_CAPTURE_DIR" ]]; then
|
||||||
|
rm -rf -- "$PANAMA_CONTRACT_CAPTURE_DIR" || true
|
||||||
|
fi
|
||||||
|
PANAMA_CONTRACT_CAPTURE_DIR=""
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate_active_contract() {
|
||||||
|
local pid="$PANAMA_ACTIVE_CONTRACT_PID"
|
||||||
|
PANAMA_ACTIVE_CONTRACT_PID=""
|
||||||
|
[[ "$pid" =~ ^[1-9][0-9]*$ && "$pid" != "$$" ]] || return 0
|
||||||
|
|
||||||
|
# GNU timeout owns a process group whose ID is its PID. Signal that complete
|
||||||
|
# group so a contract cannot leave descendants behind, with a direct-PID
|
||||||
|
# fallback for implementations that do not create the group.
|
||||||
|
kill -TERM -- "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
||||||
|
wait "$pid" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_contract_signal() {
|
||||||
|
local signal_status="$1"
|
||||||
|
trap - INT TERM
|
||||||
|
terminate_active_contract
|
||||||
|
cleanup_contract_capture
|
||||||
|
trap - EXIT
|
||||||
|
exit "$signal_status"
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_contract_capture() {
|
||||||
|
local capture_dir=""
|
||||||
|
if ! capture_dir="$(mktemp -d)"; then
|
||||||
|
err 'Could not create contract capture directory.'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -z "$capture_dir" || ! -d "$capture_dir" ]]; then
|
||||||
|
err 'Could not create contract capture directory.'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PANAMA_CONTRACT_CAPTURE_DIR="$capture_dir"
|
||||||
|
trap cleanup_contract_capture EXIT
|
||||||
|
trap 'handle_contract_signal 130' INT
|
||||||
|
trap 'handle_contract_signal 143' TERM
|
||||||
|
}
|
||||||
|
|
||||||
cmd_test() {
|
cmd_test() {
|
||||||
local pattern="" safe=0 arg
|
local timeout_seconds="${PANAMA_TEST_TIMEOUT_SECONDS:-180}"
|
||||||
# Position-independent, because 'panama test --safe dock' and
|
[[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || {
|
||||||
# 'panama test dock --safe' are the same intent and nobody should have to
|
err 'PANAMA_TEST_TIMEOUT_SECONDS must be a positive integer.'
|
||||||
# remember which one this accepts.
|
return 2
|
||||||
for arg in "$@"; do
|
}
|
||||||
|
|
||||||
|
validate_contract_manifest || return 1
|
||||||
|
cmd_test_impl "$timeout_seconds" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_test_impl() {
|
||||||
|
local timeout_seconds="$1"
|
||||||
|
shift
|
||||||
|
local pattern="" safe=0 arg capability capabilities rel path
|
||||||
|
local -A grants=() manifest_capabilities=() skipped_counts=() missing_grants=() selected_capabilities=()
|
||||||
|
local -a suite=() missing_capability_list=() selected_capability_list=() capability_list=()
|
||||||
|
|
||||||
|
# Position-independent: flags can precede or follow the optional pattern.
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
arg="$1"
|
||||||
|
shift
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--safe) safe=1 ;;
|
--safe) safe=1 ;;
|
||||||
*) pattern="$arg" ;;
|
--allow)
|
||||||
|
(( $# > 0 )) || { test_usage; return 2; }
|
||||||
|
capability="$1"
|
||||||
|
shift
|
||||||
|
is_contract_capability "$capability" || {
|
||||||
|
err "Unknown contract capability: $capability"
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
[[ "$capability" != hermetic ]] || {
|
||||||
|
err 'hermetic contracts do not need --allow.'
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
grants["$capability"]=1
|
||||||
|
;;
|
||||||
|
--*) test_usage; return 2 ;;
|
||||||
|
*)
|
||||||
|
[[ -z "$pattern" ]] || { test_usage; return 2; }
|
||||||
|
pattern="$arg"
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
local -a suite=()
|
(( safe == 0 || ${#grants[@]} == 0 )) || {
|
||||||
local -A hijacking=()
|
err '--safe cannot be combined with --allow.'
|
||||||
local skipped=0 entry rel
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
if (( safe )); then
|
while IFS=$'\t' read -r rel capabilities; do
|
||||||
while IFS= read -r entry; do
|
manifest_capabilities["$rel"]="$capabilities"
|
||||||
hijacking["$entry"]=1
|
[[ -z "$pattern" || "$rel" == *"$pattern"* ]] || continue
|
||||||
done < <(hijacking_entries)
|
if (( safe )) && [[ "$capabilities" != hermetic ]]; then
|
||||||
fi
|
IFS=',' read -r -a capability_list <<<"$capabilities"
|
||||||
|
for capability in "${capability_list[@]}"; do
|
||||||
# Executables, plus the Python suites. Those are unittest files rather than
|
(( ++skipped_counts["$capability"] ))
|
||||||
# executables, and collecting only what has the executable bit would skip them
|
done
|
||||||
# without saying so -- which is how all three came to be run by nothing at all.
|
|
||||||
# A runner with a blind spot is worse than no runner, because it reports PASS.
|
|
||||||
while IFS= read -r path; do
|
|
||||||
[[ -x "$path" || "$path" == *_test.py ]] || continue
|
|
||||||
[[ -z "$pattern" || "$path" == *"$pattern"* ]] || continue
|
|
||||||
rel="tests/${path#"$PANAMA_DIR"/tests/}"
|
|
||||||
if (( safe )) && [[ -n "${hijacking[$rel]:-}" ]]; then
|
|
||||||
(( ++skipped ))
|
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
suite+=("$path")
|
suite+=("$rel")
|
||||||
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
done < <(contract_manifest_entries)
|
||||||
|
|
||||||
if (( ${#suite[@]} == 0 )); then
|
if (( ${#suite[@]} == 0 )); then
|
||||||
# "Nothing matched" and "everything that matched was skipped" are different
|
if (( safe )) && (( ${#skipped_counts[@]} > 0 )); then
|
||||||
# answers, and reporting the first for the second is how --safe would come
|
err "Every contract matching '${pattern}' needs an external capability; --safe skipped all of them."
|
||||||
# to look like a broken pattern.
|
|
||||||
if (( skipped > 0 )); then
|
|
||||||
err "Every contract matching '${pattern}' is desktop-hijacking; --safe skipped all ${skipped}."
|
|
||||||
printf ' What they do to the session: %s/%s\n' "$PANAMA_DIR" "$DESKTOP_HIJACKING_LEDGER" >&2
|
|
||||||
else
|
else
|
||||||
err "No contracts match '${pattern}'"
|
err "No contracts match '${pattern}'"
|
||||||
fi
|
fi
|
||||||
exit 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if (( safe )); then
|
||||||
|
for capability in "${CONTRACT_CAPABILITIES[@]}"; do
|
||||||
|
[[ "$capability" == hermetic ]] && continue
|
||||||
|
printf 'Skipped %d %s contract(s).\n' "${skipped_counts[$capability]:-0}" "$capability"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
for rel in "${suite[@]}"; do
|
||||||
|
capabilities="${manifest_capabilities[$rel]}"
|
||||||
|
[[ "$capabilities" == hermetic ]] && continue
|
||||||
|
IFS=',' read -r -a capability_list <<<"$capabilities"
|
||||||
|
for capability in "${capability_list[@]}"; do
|
||||||
|
selected_capabilities["$capability"]=1
|
||||||
|
[[ -n "${grants[$capability]:-}" ]] || missing_grants["$capability"]=1
|
||||||
|
done
|
||||||
|
done
|
||||||
|
for capability in "${CONTRACT_CAPABILITIES[@]}"; do
|
||||||
|
[[ "$capability" == hermetic ]] && continue
|
||||||
|
[[ -n "${selected_capabilities[$capability]:-}" ]] && selected_capability_list+=("$capability")
|
||||||
|
[[ -n "${missing_grants[$capability]:-}" ]] && missing_capability_list+=("$capability")
|
||||||
|
done
|
||||||
|
if (( ${#missing_capability_list[@]} > 0 )); then
|
||||||
|
if [[ -t 0 && -t 2 ]]; then
|
||||||
|
confirm "Run ${#suite[@]} contract(s) requiring: ${selected_capability_list[*]}?" || {
|
||||||
|
warn 'No contracts were run.'
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
err "Selected contracts require: ${missing_capability_list[*]}."
|
||||||
|
for capability in "${missing_capability_list[@]}"; do
|
||||||
|
printf ' Automation: pass --allow %s\n' "$capability" >&2
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local capture_dir stdout_file stderr_file name run_status index=0 final_status=0
|
||||||
|
local -a failed=() runner=()
|
||||||
|
prepare_contract_capture || return 1
|
||||||
|
capture_dir="$PANAMA_CONTRACT_CAPTURE_DIR"
|
||||||
|
|
||||||
info "Running ${#suite[@]} contract(s)"
|
info "Running ${#suite[@]} contract(s)"
|
||||||
local -a failed=()
|
for index in "${!suite[@]}"; do
|
||||||
local path name
|
rel="${suite[$index]}"
|
||||||
local -a runner
|
path="$PANAMA_DIR/$rel"
|
||||||
for path in "${suite[@]}"; do
|
name="${rel#tests/}"
|
||||||
name="${path#"$PANAMA_DIR"/tests/}"
|
stdout_file="$capture_dir/$index.stdout"
|
||||||
|
stderr_file="$capture_dir/$index.stderr"
|
||||||
if [[ "$path" == *_test.py ]]; then
|
if [[ "$path" == *_test.py ]]; then
|
||||||
runner=(python3 "$path")
|
runner=(python3 "$path")
|
||||||
else
|
else
|
||||||
runner=("$path")
|
runner=("$path")
|
||||||
fi
|
fi
|
||||||
if "${runner[@]}" >/dev/null 2>&1; then
|
capabilities="${manifest_capabilities[$rel]}"
|
||||||
ok "$name"
|
if [[ "$capabilities" != hermetic ]]; then
|
||||||
else
|
info "Running $name [$capabilities]"
|
||||||
err "$name"
|
|
||||||
failed+=("$name")
|
|
||||||
fi
|
fi
|
||||||
|
run_status=0
|
||||||
|
timeout --signal=TERM --kill-after=5 "$timeout_seconds" \
|
||||||
|
"${runner[@]}" >"$stdout_file" 2>"$stderr_file" &
|
||||||
|
PANAMA_ACTIVE_CONTRACT_PID=$!
|
||||||
|
wait "$PANAMA_ACTIVE_CONTRACT_PID" || run_status=$?
|
||||||
|
PANAMA_ACTIVE_CONTRACT_PID=""
|
||||||
|
if (( run_status == 0 )); then
|
||||||
|
ok "$name"
|
||||||
|
if [[ -s "$stderr_file" ]]; then
|
||||||
|
warn "$name wrote to stderr:"
|
||||||
|
cat "$stderr_file" >&2
|
||||||
|
fi
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( run_status == 124 || run_status == 137 )); then
|
||||||
|
err "$name timed out after ${timeout_seconds}s"
|
||||||
|
else
|
||||||
|
err "$name failed (exit $run_status)"
|
||||||
|
fi
|
||||||
|
[[ -s "$stdout_file" ]] && {
|
||||||
|
printf '%s stdout:\n' "$name" >&2
|
||||||
|
cat "$stdout_file" >&2
|
||||||
|
}
|
||||||
|
[[ -s "$stderr_file" ]] && {
|
||||||
|
printf '%s stderr:\n' "$name" >&2
|
||||||
|
cat "$stderr_file" >&2
|
||||||
|
}
|
||||||
|
failed+=("$name")
|
||||||
done
|
done
|
||||||
|
|
||||||
header "Result"
|
header "Result"
|
||||||
if (( ${#failed[@]} == 0 )); then
|
if (( ${#failed[@]} == 0 )); then
|
||||||
ok "${#suite[@]} contract(s) passed"
|
ok "${#suite[@]} contract(s) passed"
|
||||||
if (( safe )); then
|
else
|
||||||
printf 'Skipped %d desktop-hijacking contract(s) (%s).\n' "$skipped" "$DESKTOP_HIJACKING_LEDGER"
|
err "${#failed[@]} of ${#suite[@]} failed:"
|
||||||
fi
|
printf ' %s\n' "${failed[@]}" >&2
|
||||||
return 0
|
final_status=1
|
||||||
fi
|
fi
|
||||||
err "${#failed[@]} of ${#suite[@]} failed:"
|
|
||||||
printf ' %s\n' "${failed[@]}" >&2
|
cleanup_contract_capture
|
||||||
warn "Run one on its own to see why: ${BOLD}${PANAMA_DIR}/tests/<name>${RESET}"
|
trap - EXIT INT TERM
|
||||||
if (( safe )); then
|
return "$final_status"
|
||||||
printf 'Skipped %d desktop-hijacking contract(s) (%s).\n' "$skipped" "$DESKTOP_HIJACKING_LEDGER"
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Command: contracts
|
# Command: contracts
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# "I changed this file -- what should I run?" There are 177 contracts and no
|
# "I changed this file -- what should I run?" The suite is large enough that
|
||||||
# index, so the honest answers were "all of them" (minutes, and half of them
|
# running all of it or guessing from contract names are both poor answers.
|
||||||
# take the desktop away) or "the ones whose name sounds related" (which is how
|
|
||||||
# a covering contract gets skipped).
|
|
||||||
#
|
#
|
||||||
# This is a grep, and says so. A contract that names the file, or a
|
# This is a grep, and says so. A contract that names the file, or a
|
||||||
# parent-trimmed suffix of it, or just its basename, is a contract worth
|
# parent-trimmed suffix of it, or just its basename, is a contract worth
|
||||||
@@ -577,8 +843,8 @@ cmd_test() {
|
|||||||
# a real answer -- exit 1 so a script can tell the difference -- but it is a
|
# a real answer -- exit 1 so a script can tell the difference -- but it is a
|
||||||
# statement about this search, not about the file.
|
# statement about this search, not about the file.
|
||||||
#
|
#
|
||||||
# Each hit is marked from tests/desktop-hijacking, so the output also answers
|
# Each hit is labeled from tests/contracts.manifest, so the output also answers
|
||||||
# "and can I run them right now".
|
# which boundary the matching contract reaches.
|
||||||
cmd_contracts() {
|
cmd_contracts() {
|
||||||
local target="${1:-}"
|
local target="${1:-}"
|
||||||
if [[ -z "$target" ]]; then
|
if [[ -z "$target" ]]; then
|
||||||
@@ -621,21 +887,22 @@ cmd_contracts() {
|
|||||||
suffix="${suffix#*/}"
|
suffix="${suffix#*/}"
|
||||||
done
|
done
|
||||||
|
|
||||||
local -A hijacking=()
|
validate_contract_manifest || return 1
|
||||||
local entry
|
local -A manifest_capabilities=()
|
||||||
while IFS= read -r entry; do
|
local capabilities
|
||||||
hijacking["$entry"]=1
|
while IFS=$'\t' read -r rel capabilities; do
|
||||||
done < <(hijacking_entries)
|
manifest_capabilities["$rel"]="$capabilities"
|
||||||
|
done < <(contract_manifest_entries)
|
||||||
|
|
||||||
# The same collection `test` runs, so anything named here is something the
|
# The same collection `test` runs, so anything named here is something the
|
||||||
# runner would actually execute.
|
# runner would actually execute.
|
||||||
local -a hits=()
|
local -a hits=()
|
||||||
local candidate rel
|
local candidate rel
|
||||||
while IFS= read -r candidate; do
|
while IFS= read -r rel; do
|
||||||
[[ -x "$candidate" || "$candidate" == *_test.py ]] || continue
|
candidate="$PANAMA_DIR/$rel"
|
||||||
grep -qF "${patterns[@]}" "$candidate" 2>/dev/null || continue
|
grep -qF "${patterns[@]}" "$candidate" 2>/dev/null || continue
|
||||||
hits+=("tests/${candidate#"$PANAMA_DIR"/tests/}")
|
hits+=("$rel")
|
||||||
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
done < <(contract_paths)
|
||||||
|
|
||||||
if (( ${#hits[@]} == 0 )); then
|
if (( ${#hits[@]} == 0 )); then
|
||||||
printf 'No contract mentions %s — coverage may be indirect (a harness or a generated artifact); nothing verified.\n' "$path" >&2
|
printf 'No contract mentions %s — coverage may be indirect (a harness or a generated artifact); nothing verified.\n' "$path" >&2
|
||||||
@@ -643,11 +910,11 @@ cmd_contracts() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
for rel in "${hits[@]}"; do
|
for rel in "${hits[@]}"; do
|
||||||
if [[ -n "${hijacking[$rel]:-}" ]]; then
|
[[ -n "${manifest_capabilities[$rel]:-}" ]] || {
|
||||||
printf '%s [desktop]\n' "$rel"
|
err "Contract has no manifest capability label: $rel"
|
||||||
else
|
return 1
|
||||||
printf '%s [safe]\n' "$rel"
|
}
|
||||||
fi
|
printf '%s [%s]\n' "$rel" "${manifest_capabilities[$rel]}"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
# Panama's front door: the one command a fresh Fedora machine needs.
|
# Panama's front door: the one command a fresh Fedora machine needs.
|
||||||
#
|
#
|
||||||
# bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot)
|
# Download this file from the documented commit URL, verify its documented
|
||||||
# bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot) --server
|
# SHA-256, then pass both immutable values as PANAMA_BOOT_REVISION and
|
||||||
|
# PANAMA_BOOT_SHA256 when invoking it.
|
||||||
#
|
#
|
||||||
# Deliberately dumb, because a copy of this script leaves the repository the
|
# Deliberately dumb, because a copy of this script leaves the repository the
|
||||||
# moment somebody curls it -- nothing here can be fixed by re-running
|
# moment somebody curls it -- nothing here can be fixed by re-running
|
||||||
@@ -17,9 +18,85 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ ! "${PANAMA_BOOT_REVISION:-}" =~ ^[0-9a-f]{40}$ ]]; then
|
||||||
|
echo "boot: PANAMA_BOOT_REVISION must be a full lowercase commit" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! "${PANAMA_BOOT_SHA256:-}" =~ ^[0-9a-f]{64}$ ]]; then
|
||||||
|
echo "boot: PANAMA_BOOT_SHA256 must be a lowercase SHA-256" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
actual_boot_sha="$(sha256sum "${BASH_SOURCE[0]}" | cut -d' ' -f1)"
|
||||||
|
if [[ "$actual_boot_sha" != "$PANAMA_BOOT_SHA256" ]]; then
|
||||||
|
echo "boot: downloaded boot file does not match PANAMA_BOOT_SHA256" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
REPO_URL="https://git.gbrown.org/gib/Panama.git"
|
REPO_URL="https://git.gbrown.org/gib/Panama.git"
|
||||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
export PANAMA_PATH
|
export PANAMA_PATH
|
||||||
|
BOOTSTRAP_USER=""
|
||||||
|
|
||||||
|
checkout_command() {
|
||||||
|
if [[ -n "$BOOTSTRAP_USER" ]]; then
|
||||||
|
runuser -u "$BOOTSTRAP_USER" -- "$@"
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_panama_checkout() {
|
||||||
|
local checkout="$1" actual_head checkout_status
|
||||||
|
|
||||||
|
if [[ -e "$checkout" && ! -d "$checkout/.git" ]]; then
|
||||||
|
printf 'boot: %s exists but is not a Panama Git checkout\n' "$checkout" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$checkout/.git" ]]; then
|
||||||
|
checkout_status="$(checkout_command git -C "$checkout" status --porcelain)" || {
|
||||||
|
printf 'boot: could not inspect the existing checkout at %s\n' "$checkout" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if [[ -n "$checkout_status" ]]; then
|
||||||
|
printf 'boot: existing checkout at %s has local changes\n' "$checkout" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Panama is already cloned at $checkout; fetching the verified revision"
|
||||||
|
checkout_command git -C "$checkout" fetch origin "$PANAMA_BOOT_REVISION" || return 1
|
||||||
|
if ! checkout_command git -C "$checkout" merge-base --is-ancestor \
|
||||||
|
HEAD "$PANAMA_BOOT_REVISION"; then
|
||||||
|
echo "boot: existing checkout cannot fast-forward to the verified revision" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
checkout_command git -C "$checkout" merge --ff-only "$PANAMA_BOOT_REVISION" || return 1
|
||||||
|
else
|
||||||
|
checkout_command mkdir -p "$(dirname "$checkout")" || return 1
|
||||||
|
checkout_command git init "$checkout" || return 1
|
||||||
|
checkout_command git -C "$checkout" remote add origin "$REPO_URL" || return 1
|
||||||
|
checkout_command git -C "$checkout" fetch --depth=1 origin \
|
||||||
|
"$PANAMA_BOOT_REVISION" || return 1
|
||||||
|
checkout_command git -C "$checkout" checkout --detach \
|
||||||
|
"$PANAMA_BOOT_REVISION" || return 1
|
||||||
|
|
||||||
|
actual_head="$(checkout_command git -C "$checkout" rev-parse 'HEAD^{commit}')" || return 1
|
||||||
|
if [[ "$actual_head" != "$PANAMA_BOOT_REVISION" ]]; then
|
||||||
|
echo "boot: fetched checkout does not match PANAMA_BOOT_REVISION" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
checkout_command git -C "$checkout" checkout -b main || return 1
|
||||||
|
checkout_command git -C "$checkout" config branch.main.remote origin || return 1
|
||||||
|
checkout_command git -C "$checkout" config branch.main.merge refs/heads/main || return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
actual_head="$(checkout_command git -C "$checkout" rev-parse 'HEAD^{commit}')" || return 1
|
||||||
|
if [[ "$actual_head" != "$PANAMA_BOOT_REVISION" ]]; then
|
||||||
|
echo "boot: checkout HEAD does not match PANAMA_BOOT_REVISION" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
SERVER=0
|
SERVER=0
|
||||||
INSTALL_ARGS=()
|
INSTALL_ARGS=()
|
||||||
@@ -33,6 +110,266 @@ for arg in "$@"; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# The public bootstrap contract runs this branch as an ordinary user with a
|
||||||
|
# stubbed root identity. Keep its filesystem adapter unavailable to a real root
|
||||||
|
# shell so it cannot redirect a real installation by accident.
|
||||||
|
BOOT_ROOT="${PANAMA_BOOT_FIXTURE_ROOT:-}"
|
||||||
|
if [[ -n "$BOOT_ROOT" && "$EUID" -eq 0 ]]; then
|
||||||
|
echo "boot: PANAMA_BOOT_FIXTURE_ROOT is test-only" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
system_path() {
|
||||||
|
local path="$1"
|
||||||
|
[[ "$path" == /* ]] || return 2
|
||||||
|
printf '%s%s\n' "$BOOT_ROOT" "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_authorized_keys() {
|
||||||
|
local keys="$1" line saw_key=0
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! ssh-keygen -l -f /dev/stdin >/dev/null 2>&1 <<<"$line"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
saw_key=1
|
||||||
|
done <"$keys"
|
||||||
|
(( saw_key ))
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_authorized_keys() {
|
||||||
|
local username="$1" user_home="$2" uid ssh_dir keys
|
||||||
|
uid="$(id -u "$username")" || return 1
|
||||||
|
[[ "$uid" =~ ^[0-9]+$ && "$uid" != 0 && "$user_home" == /* ]] || return 1
|
||||||
|
ssh_dir="$user_home/.ssh"
|
||||||
|
keys="$ssh_dir/authorized_keys"
|
||||||
|
[[ -d "$ssh_dir" && ! -L "$ssh_dir" && -f "$keys" && ! -L "$keys" ]] || return 1
|
||||||
|
[[ "$(stat -Lc '%u:%a' "$ssh_dir")" == "$uid:700" ]] || return 1
|
||||||
|
[[ "$(stat -Lc '%u:%a' "$keys")" == "$uid:600" ]] || return 1
|
||||||
|
valid_authorized_keys "$keys"
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_root_authorized_keys() {
|
||||||
|
local keys
|
||||||
|
keys="$(system_path /root/.ssh/authorized_keys)" || return 1
|
||||||
|
[[ -f "$keys" && ! -L "$keys" ]] || return 1
|
||||||
|
[[ "$(stat -Lc '%u:%a' "$keys")" == '0:600' ]] || return 1
|
||||||
|
valid_authorized_keys "$keys"
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_ssh_unit() {
|
||||||
|
local unit
|
||||||
|
for unit in sshd.service ssh.service; do
|
||||||
|
systemctl cat "$unit" >/dev/null 2>&1 && {
|
||||||
|
printf '%s\n' "$unit"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_ssh_dropin() {
|
||||||
|
local restore
|
||||||
|
if (( ssh_had_prior )); then
|
||||||
|
[[ -n "$ssh_backup" && -f "$ssh_backup" && ! -L "$ssh_backup" ]] || return 1
|
||||||
|
restore="$(mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.restore)" || return 1
|
||||||
|
if ! cp -a -- "$ssh_backup" "$restore"; then
|
||||||
|
remove_ssh_artifact "$restore" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! mv -f -- "$restore" "$ssh_dropin"; then
|
||||||
|
remove_ssh_artifact "$restore" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
remove_ssh_artifact "$ssh_dropin"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_ssh_transaction_traps() {
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
[[ -n "${ssh_saved_exit_trap:-}" ]] && eval "$ssh_saved_exit_trap"
|
||||||
|
[[ -n "${ssh_saved_int_trap:-}" ]] && eval "$ssh_saved_int_trap"
|
||||||
|
[[ -n "${ssh_saved_term_trap:-}" ]] && eval "$ssh_saved_term_trap"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_ssh_artifact() {
|
||||||
|
local artifact="$1"
|
||||||
|
[[ -n "$artifact" && ( -e "$artifact" || -L "$artifact" ) ]] || return 0
|
||||||
|
if rm -f -- "$artifact"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf 'SSH transaction cleanup failed. Retained artifact: %s\n' "$artifact" >&2
|
||||||
|
printf ' rm -f -- %q\n' "$artifact" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
print_ssh_recovery() {
|
||||||
|
if (( ssh_had_prior )); then
|
||||||
|
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
|
||||||
|
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
|
||||||
|
else
|
||||||
|
printf 'SSH rollback needs manual recovery. No prior drop-in existed.\n' >&2
|
||||||
|
printf ' rm -f -- %q\n' "$ssh_dropin" >&2
|
||||||
|
fi
|
||||||
|
printf ' sshd -t\n' >&2
|
||||||
|
printf ' systemctl reload %s\n' "$ssh_unit" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
policy_is_no() {
|
||||||
|
local policy="$1" setting="$2"
|
||||||
|
awk -v setting="$setting" '
|
||||||
|
$1 == setting { count += 1; if ($2 != "no") bad = 1 }
|
||||||
|
END { exit count != 1 || bad }
|
||||||
|
' <<<"$policy"
|
||||||
|
}
|
||||||
|
|
||||||
|
effective_ssh_policy_is_hardened() {
|
||||||
|
local username="$1" root_policy target_policy context
|
||||||
|
context='host=localhost,addr=127.0.0.1'
|
||||||
|
root_policy="$(sshd -T -C "user=root,$context")" || return 1
|
||||||
|
policy_is_no "$root_policy" permitrootlogin || return 1
|
||||||
|
policy_is_no "$root_policy" passwordauthentication || return 1
|
||||||
|
policy_is_no "$root_policy" kbdinteractiveauthentication || return 1
|
||||||
|
|
||||||
|
target_policy="$(sshd -T -C "user=$username,$context")" || return 1
|
||||||
|
policy_is_no "$target_policy" passwordauthentication || return 1
|
||||||
|
policy_is_no "$target_policy" kbdinteractiveauthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback_ssh_transaction() {
|
||||||
|
local reload_restored="$1" rollback_failed=0
|
||||||
|
restore_ssh_dropin || rollback_failed=1
|
||||||
|
sshd -t || rollback_failed=1
|
||||||
|
if (( reload_restored )); then
|
||||||
|
systemctl reload "$ssh_unit" || rollback_failed=1
|
||||||
|
fi
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
if (( rollback_failed )); then
|
||||||
|
print_ssh_recovery
|
||||||
|
else
|
||||||
|
remove_ssh_artifact "$ssh_backup" || true
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_ssh_transaction_exit() {
|
||||||
|
if [[ "$ssh_transaction_state" == preparing \
|
||||||
|
|| ( "$ssh_transaction_state" == activating && -e "$ssh_candidate" ) ]]; then
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
remove_ssh_artifact "$ssh_backup" || true
|
||||||
|
elif [[ "$ssh_transaction_state" == activating || "$ssh_transaction_state" == activated ]]; then
|
||||||
|
restore_ssh_dropin || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_ssh_transaction_signal() {
|
||||||
|
local signal_status="$1"
|
||||||
|
trap - INT TERM
|
||||||
|
if [[ "$ssh_transaction_state" == preparing \
|
||||||
|
|| ( "$ssh_transaction_state" == activating && -e "$ssh_candidate" ) ]]; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
remove_ssh_artifact "$ssh_backup" || true
|
||||||
|
else
|
||||||
|
rollback_ssh_transaction 1 || true
|
||||||
|
fi
|
||||||
|
exit "$signal_status"
|
||||||
|
}
|
||||||
|
|
||||||
|
harden_server_ssh() {
|
||||||
|
local username="$1" user_home="$2" sshd_dir ssh_dropin harden ssh_unit
|
||||||
|
local ssh_candidate="" ssh_backup="" ssh_had_prior=0
|
||||||
|
local ssh_transaction_state=""
|
||||||
|
local ssh_saved_exit_trap ssh_saved_int_trap ssh_saved_term_trap
|
||||||
|
sshd_dir="$(system_path /etc/ssh/sshd_config.d)" || return 1
|
||||||
|
ssh_dropin="$sshd_dir/00-panama.conf"
|
||||||
|
|
||||||
|
if [[ -L "$ssh_dropin" || ( -e "$ssh_dropin" && ! -f "$ssh_dropin" ) ]]; then
|
||||||
|
printf 'SSH hardening unavailable: %s is not a regular file\n' "$ssh_dropin" >&2
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ssh_unit="$(detect_ssh_unit)"; then
|
||||||
|
echo "SSH hardening unavailable: neither sshd.service nor ssh.service is installed" >&2
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Harden sshd (disable root, password, and keyboard-interactive authentication)? [Y/n]: '
|
||||||
|
read -r harden </dev/tty || harden=""
|
||||||
|
if [[ "$harden" =~ ^[Nn] ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh_saved_exit_trap="$(trap -p EXIT)"
|
||||||
|
ssh_saved_int_trap="$(trap -p INT)"
|
||||||
|
ssh_saved_term_trap="$(trap -p TERM)"
|
||||||
|
ssh_transaction_state=preparing
|
||||||
|
trap 'handle_ssh_transaction_exit' EXIT
|
||||||
|
trap 'handle_ssh_transaction_signal 130' INT
|
||||||
|
trap 'handle_ssh_transaction_signal 143' TERM
|
||||||
|
|
||||||
|
if ! ssh_candidate="$(umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.tmp)"; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! printf 'PermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\n' >"$ssh_candidate"; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e "$ssh_dropin" ]]; then
|
||||||
|
ssh_had_prior=1
|
||||||
|
if ! ssh_backup="$(umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.backup)"; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! cp -a -- "$ssh_dropin" "$ssh_backup"; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
remove_ssh_artifact "$ssh_backup" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh_transaction_state=activating
|
||||||
|
if ! mv -f -- "$ssh_candidate" "$ssh_dropin"; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
remove_ssh_artifact "$ssh_backup" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
ssh_candidate=""
|
||||||
|
ssh_transaction_state=activated
|
||||||
|
|
||||||
|
if ! sshd -t || ! effective_ssh_policy_is_hardened "$username"; then
|
||||||
|
rollback_ssh_transaction 0 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! systemctl reload "$ssh_unit"; then
|
||||||
|
rollback_ssh_transaction 1 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_backup" || return 1
|
||||||
|
echo "Wrote $ssh_dropin; make sure your key works before logging out."
|
||||||
|
}
|
||||||
|
|
||||||
# Panama assumes Fedora's repositories and package names.
|
# Panama assumes Fedora's repositories and package names.
|
||||||
if ! grep -qi '^ID=fedora' /etc/os-release 2>/dev/null; then
|
if ! grep -qi '^ID=fedora' /etc/os-release 2>/dev/null; then
|
||||||
echo "This looks like something other than Fedora; Panama only supports Fedora." >&2
|
echo "This looks like something other than Fedora; Panama only supports Fedora." >&2
|
||||||
@@ -84,33 +421,52 @@ if [[ "$(id -u)" -eq 0 ]]; then
|
|||||||
passwd "$username" </dev/tty
|
passwd "$username" </dev/tty
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The key that reached root is the key that should reach the user, or the
|
# Do not close root/password access until the account's key is an exact,
|
||||||
# next SSH login has no way in once root logins are closed below.
|
# usable login path. The fixture adapter resolves these logical system paths
|
||||||
user_home="$(getent passwd "$username" | cut -d: -f6)"
|
# beneath a temporary root; ordinary execution receives the original paths.
|
||||||
if [[ -s /root/.ssh/authorized_keys && ! -s "$user_home/.ssh/authorized_keys" ]]; then
|
logical_user_home="$(getent passwd "$username" | cut -d: -f6)"
|
||||||
echo "Copying root's authorized_keys to $username"
|
user_home=""
|
||||||
mkdir -p "$user_home/.ssh"
|
if [[ "$logical_user_home" == /* ]]; then
|
||||||
cp /root/.ssh/authorized_keys "$user_home/.ssh/authorized_keys"
|
user_home="$(system_path "$logical_user_home")" || true
|
||||||
chmod 700 "$user_home/.ssh"
|
fi
|
||||||
chmod 600 "$user_home/.ssh/authorized_keys"
|
bootstrap_home="$user_home"
|
||||||
chown -R "$username:$username" "$user_home/.ssh"
|
if [[ -z "$bootstrap_home" ]]; then
|
||||||
|
bootstrap_home="$(system_path "/home/$username")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Offered rather than imposed, defaulting to yes: a VPS keeps its provider's
|
user_ssh_dir="$user_home/.ssh"
|
||||||
# web console, so locking password and root logins out of sshd is
|
user_keys="$user_ssh_dir/authorized_keys"
|
||||||
# recoverable even when it goes wrong. Written as a drop-in so it never
|
if [[ -n "$user_home" && ! -e "$user_keys" && ! -L "$user_keys" \
|
||||||
# fights the distribution's own sshd_config.
|
&& ! -L "$user_ssh_dir" ]] && safe_root_authorized_keys; then
|
||||||
SSHD_DROPIN=/etc/ssh/sshd_config.d/90-panama.conf
|
copy_root_key=0
|
||||||
if [[ -f "$SSHD_DROPIN" ]]; then
|
if [[ ! -e "$user_ssh_dir" ]]; then
|
||||||
echo "sshd is already hardened ($SSHD_DROPIN)"
|
copy_root_key=1
|
||||||
else
|
elif [[ ! -d "$user_ssh_dir" \
|
||||||
printf 'Harden sshd (disable root login and password auth)? [Y/n]: '
|
|| "$(stat -Lc '%u:%a' "$user_ssh_dir")" != "$(id -u "$username"):700" ]]; then
|
||||||
read -r harden </dev/tty || harden=""
|
echo "SSH hardening unavailable: $username has no safe authorized_keys" >&2
|
||||||
if [[ ! "$harden" =~ ^[Nn] ]]; then
|
else
|
||||||
printf 'PermitRootLogin no\nPasswordAuthentication no\n' >"$SSHD_DROPIN"
|
copy_root_key=1
|
||||||
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
|
|
||||||
echo "Wrote $SSHD_DROPIN; make sure your key works before logging out."
|
|
||||||
fi
|
fi
|
||||||
|
if (( copy_root_key )); then
|
||||||
|
echo "Copying root's authorized_keys to $username"
|
||||||
|
root_keys="$(system_path /root/.ssh/authorized_keys)"
|
||||||
|
if ! runuser -u "$username" -- install -d -m 0700 -- "$user_ssh_dir" \
|
||||||
|
|| ! runuser -u "$username" -- install -m 0600 -- /dev/stdin "$user_keys" \
|
||||||
|
<"$root_keys"; then
|
||||||
|
echo "SSH hardening unavailable: could not install root's key for $username" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if safe_authorized_keys "$username" "$user_home"; then
|
||||||
|
harden_status=0
|
||||||
|
harden_server_ssh "$username" "$user_home" || harden_status=$?
|
||||||
|
if (( harden_status != 0 && harden_status != 2 )); then
|
||||||
|
echo "SSH hardening failed; stopping before install handoff." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "SSH hardening unavailable: $username has no safe authorized_keys" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v git >/dev/null 2>&1; then
|
if ! command -v git >/dev/null 2>&1; then
|
||||||
@@ -118,18 +474,11 @@ if [[ "$(id -u)" -eq 0 ]]; then
|
|||||||
dnf install -y git
|
dnf install -y git
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Cloned straight into the user's home and owned by them: this is the
|
# Create or advance the checkout as the target user. A root-owned .git in a
|
||||||
# checkout `panama update` will pull from for the life of the machine, and
|
# user's home would break every later update.
|
||||||
# a root-owned .git in a user's home is a wound that never heals.
|
PANAMA_PATH="$bootstrap_home/.local/share/Panama"
|
||||||
PANAMA_PATH="$user_home/.local/share/Panama"
|
BOOTSTRAP_USER="$username"
|
||||||
if [[ -d "$PANAMA_PATH/.git" ]]; then
|
prepare_panama_checkout "$PANAMA_PATH"
|
||||||
echo "Panama is already cloned at $PANAMA_PATH; updating"
|
|
||||||
runuser -u "$username" -- git -C "$PANAMA_PATH" pull --ff-only \
|
|
||||||
|| echo "Could not fast-forward; installing from the clone as it is" >&2
|
|
||||||
else
|
|
||||||
runuser -u "$username" -- mkdir -p "$user_home/.local/share"
|
|
||||||
runuser -u "$username" -- git clone "$REPO_URL" "$PANAMA_PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Handing off to install as $username"
|
echo "Handing off to install as $username"
|
||||||
exec runuser -u "$username" -- env PANAMA_PATH="$PANAMA_PATH" \
|
exec runuser -u "$username" -- env PANAMA_PATH="$PANAMA_PATH" \
|
||||||
@@ -143,19 +492,10 @@ if ! command -v git >/dev/null 2>&1; then
|
|||||||
sudo dnf install -y git
|
sudo dnf install -y git
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -d "$PANAMA_PATH/.git" ]]; then
|
prepare_panama_checkout "$PANAMA_PATH"
|
||||||
# An existing clone makes this the recovery command too. Only a fast-forward:
|
|
||||||
# local work is never rewritten, and a diverged clone still installs from
|
|
||||||
# what it has rather than stopping someone mid-repair.
|
|
||||||
echo "Panama is already cloned at $PANAMA_PATH; updating"
|
|
||||||
git -C "$PANAMA_PATH" pull --ff-only \
|
|
||||||
|| echo "Could not fast-forward; installing from the clone as it is" >&2
|
|
||||||
else
|
|
||||||
git clone "$REPO_URL" "$PANAMA_PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# `curl | bash` and `bash <(curl ...)` can leave stdin as the pipe, and the
|
# A shell invoked from automation can have a pipe as stdin, while the first
|
||||||
# first thing install runs is the interview, which has to be able to ask.
|
# thing install runs is the interview, which has to be able to ask.
|
||||||
# Reattach the terminal when there is one; without one the interview will say
|
# Reattach the terminal when there is one; without one the interview will say
|
||||||
# so itself.
|
# so itself.
|
||||||
# The probe actually opens /dev/tty rather than testing -r: a process with no
|
# The probe actually opens /dev/tty rather than testing -r: a process with no
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ alias avante='nvim -c "lua vim.defer_fn(function()require(\"avante.api\").zen_mo
|
|||||||
alias nlc='nvim leetcode.nvim'
|
alias nlc='nvim leetcode.nvim'
|
||||||
alias clauded='claude --allow-dangerously-skip-permissions'
|
alias clauded='claude --allow-dangerously-skip-permissions'
|
||||||
alias claudedr='claude --allow-dangerously-skip-permissions --resume'
|
alias claudedr='claude --allow-dangerously-skip-permissions --resume'
|
||||||
|
alias klaude='CLAUDE_CONFIG_DIR="$HOME/.klaude" claude'
|
||||||
alias commit-repo='git add -A && git commit -m "Update stuff" && git push'
|
alias commit-repo='git add -A && git commit -m "Update stuff" && git push'
|
||||||
|
|
||||||
# Docker Shortcuts
|
# Docker Shortcuts
|
||||||
|
|||||||
@@ -1805,7 +1805,7 @@ Singleton {
|
|||||||
"org.mozilla.thunderbird_esr", "com.slack.Slack",
|
"org.mozilla.thunderbird_esr", "com.slack.Slack",
|
||||||
"app.bluebubbles.BlueBubbles", "rustdesk",
|
"app.bluebubbles.BlueBubbles", "rustdesk",
|
||||||
"io.podman_desktop.PodmanDesktop", "com.anthropic.Claude",
|
"io.podman_desktop.PodmanDesktop", "com.anthropic.Claude",
|
||||||
"codex-desktop", "md.obsidian.Obsidian",
|
"chatgpt", "md.obsidian.Obsidian",
|
||||||
"com.obsproject.Studio", "steam"
|
"com.obsproject.Studio", "steam"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ takes to run the suite.
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
panama-settings-docs write docs/settings.md
|
panama-settings-docs write docs/settings.md
|
||||||
|
panama-settings-docs --output PATH
|
||||||
|
write PATH instead
|
||||||
panama-settings-docs --check exit 1 if the committed copy is stale
|
panama-settings-docs --check exit 1 if the committed copy is stale
|
||||||
|
panama-settings-docs --check --output PATH
|
||||||
|
exit 1 if PATH is stale
|
||||||
panama-settings-docs --stdout print without writing
|
panama-settings-docs --stdout print without writing
|
||||||
|
|
||||||
Where each group of settings lives comes from SettingsSearch.qml's routing and
|
Where each group of settings lives comes from SettingsSearch.qml's routing and
|
||||||
@@ -264,7 +268,14 @@ def main():
|
|||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--check", action="store_true")
|
parser.add_argument("--check", action="store_true")
|
||||||
parser.add_argument("--stdout", action="store_true")
|
parser.add_argument("--stdout", action="store_true")
|
||||||
|
parser.add_argument("--output", type=pathlib.Path, default=OUTPUT)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
output = (args.output if args.output.is_absolute()
|
||||||
|
else (pathlib.Path.cwd() / args.output).resolve())
|
||||||
|
try:
|
||||||
|
displayed_output = output.relative_to(ROOT)
|
||||||
|
except ValueError:
|
||||||
|
displayed_output = output
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rendered = render(read_entries(), read_routes(), read_titles())
|
rendered = render(read_entries(), read_routes(), read_titles())
|
||||||
@@ -279,19 +290,19 @@ def main():
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
if args.check:
|
if args.check:
|
||||||
if not OUTPUT.exists():
|
if not output.exists():
|
||||||
print("panama-settings-docs: docs/settings.md has never been generated",
|
print(f"panama-settings-docs: {displayed_output} has never been generated",
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
if OUTPUT.read_text() != rendered:
|
if output.read_text() != rendered:
|
||||||
print("panama-settings-docs: docs/settings.md is stale; re-run this "
|
print(f"panama-settings-docs: {displayed_output} is stale; re-run this "
|
||||||
"script and commit the result", file=sys.stderr)
|
"script and commit the result", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
OUTPUT.write_text(rendered)
|
output.write_text(rendered)
|
||||||
print(f"wrote {OUTPUT.relative_to(ROOT)}")
|
print(f"wrote {displayed_output}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,722 @@
|
|||||||
|
# Verification Gate Remediation Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan.
|
||||||
|
|
||||||
|
**Goal:** Make Panama's contract gate honest, safe by default when requested, diagnostic on failure, and capable of proving the later repository-audit fixes.
|
||||||
|
|
||||||
|
**Architecture:** Replace the incomplete desktop-hijacking ledger with one explicit manifest covering every collected contract. `bin/panama` remains the public runner: it parses that manifest, applies capability policy, executes each selected contract under a bounded timeout, and preserves its diagnostics. Contracts that currently depend on the calendar, tracked files, the live settings store, or source-text inspection are changed to exercise disposable fixtures and real public commands.
|
||||||
|
|
||||||
|
**Tech Stack:** Bash 5, Python 3 standard library, Git fixture repositories, existing Panama contract executables, Quickshell test harnesses.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-26-repository-audit-remediation-design.md`, Package 1: Verification gate.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Preserve the user's existing `config/bash/.bashrc` changes. Do not stage or edit that file.
|
||||||
|
- Do not run plain `panama test` during implementation. It is intentionally interactive once this plan lands and several entries manipulate the live desktop.
|
||||||
|
- Do not start or restart the production Quickshell, Hyprland, or system services.
|
||||||
|
- Use `apply_patch` for every repository edit.
|
||||||
|
- Test public behavior. Source inspection may enforce a narrow declarative invariant, but must not substitute for executing `bin/panama` or a helper.
|
||||||
|
- Keep every contract self-cleaning on success, failure, signal, and timeout.
|
||||||
|
- Commit after each task with only that task's files staged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Replace the desktop ledger with a complete contract manifest
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `tests/contracts.manifest`
|
||||||
|
- Create: `tests/setup/contract-manifest-contract`
|
||||||
|
- Delete: `tests/setup/desktop-hijacking-contract`
|
||||||
|
- Delete: `tests/desktop-hijacking`
|
||||||
|
- Modify: `README.md`
|
||||||
|
|
||||||
|
### Step 1: Write the failing manifest contract
|
||||||
|
|
||||||
|
Create `tests/setup/contract-manifest-contract` as an executable Bash contract. It must independently discover contracts exactly as the runner does:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
while IFS= read -r path; do
|
||||||
|
[[ -x "$path" || "$path" == *_test.py ]] || continue
|
||||||
|
printf 'tests/%s\n' "${path#"$repo_dir/tests/"}"
|
||||||
|
done < <(find "$repo_dir/tests" -type f \
|
||||||
|
-not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse non-comment manifest lines as two whitespace-separated fields:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<comma-separated-capabilities> <repo-relative-contract-path>
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert all of the following:
|
||||||
|
|
||||||
|
- every discovered contract appears exactly once;
|
||||||
|
- every manifest path is discovered by the runner;
|
||||||
|
- paths are sorted lexicographically;
|
||||||
|
- `hermetic` appears alone;
|
||||||
|
- non-hermetic entries use only `live-host`, `live-compositor`, `live-desktop`, `network`, and `privileged`;
|
||||||
|
- every non-hermetic entry has a directly preceding comment explaining the external effect;
|
||||||
|
- no path or capability is duplicated on one line;
|
||||||
|
- the manifest contains at least one entry for each capability except `privileged`, which is supported but not currently required by a contract.
|
||||||
|
|
||||||
|
Include a parser-fixture section in the same contract. Copy the manifest parser logic into a temporary malformed manifest for each rejection case (missing contract, stale path, duplicate path, unknown capability, `hermetic,network`, unsorted paths, and uncommented non-hermetic entry) and assert each fixture fails with a message naming the defect.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x tests/setup/contract-manifest-contract
|
||||||
|
tests/setup/contract-manifest-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL because `tests/contracts.manifest` does not exist.
|
||||||
|
|
||||||
|
### Step 2: Add the complete manifest
|
||||||
|
|
||||||
|
Add one line for each contract discovered by the command in Step 1. Start every path not named below as `hermetic`. Apply this audited non-hermetic classification exactly:
|
||||||
|
|
||||||
|
| Capabilities | Contract |
|
||||||
|
|---|---|
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/activity-indicator-contract` |
|
||||||
|
| `live-desktop` | `tests/quickshell/activity-state-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/adwaita-accent-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/application-volume-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/calendar-agenda-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/calendar-agenda-helper-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/cheatsheet-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/connectivity-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/control-center-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/control-center-services-contract` |
|
||||||
|
| `live-desktop` | `tests/quickshell/default-apps-roles-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/default-apps-family-contract` |
|
||||||
|
| `live-desktop` | `tests/quickshell/display-arrangement-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/displays-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/disks-contract` |
|
||||||
|
| `live-desktop` | `tests/quickshell/dock-position-contract` |
|
||||||
|
| `live-compositor` | `tests/quickshell/enum-hypr-map-contract` |
|
||||||
|
| `network` | `tests/setup/extras-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/focus-session-contract` |
|
||||||
|
| `live-desktop` | `tests/quickshell/focus-session-expiry` |
|
||||||
|
| `live-desktop` | `tests/quickshell/focus-session-restart` |
|
||||||
|
| `live-host` | `tests/quickshell/firewall-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/gaming-contract` |
|
||||||
|
| `live-desktop` | `tests/quickshell/health-ui-contract` |
|
||||||
|
| `live-compositor` | `tests/hypr/keybind-categories-contract` |
|
||||||
|
| `live-compositor` | `tests/quickshell/keybind-rebind-contract` |
|
||||||
|
| `live-compositor` | `tests/quickshell/keybinds-contract` |
|
||||||
|
| `network` | `tests/quickshell/home-assistant-helper-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/my-home-settings-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/overview-window-actions-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/overview-live-actions` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/overview-search-contract` |
|
||||||
|
| `live-desktop` | `tests/quickshell/panama-action-ipc-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/phone-page-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/polkit-agent-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/printers-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/scratchpad-shelf-contract` |
|
||||||
|
| `live-compositor` | `tests/quickshell/schema-hypr-shape-contract` |
|
||||||
|
| `live-desktop` | `tests/quickshell/screen-intelligence-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/settings-hyprland-write-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/settings-pages-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/settings-system-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/settings-window-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/settings-write-sweep-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/sharing-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/signal-glass-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/snapshots-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/sound-page-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/ssh-keys-contract` |
|
||||||
|
| `live-desktop` | `tests/quickshell/status-events-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/switcher-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/updates-contract` |
|
||||||
|
| `live-host` | `tests/quickshell/user-accounts-contract` |
|
||||||
|
| `live-host` | `tests/setup/hardware-predicates-contract` |
|
||||||
|
| `live-compositor,live-desktop` | `tests/quickshell/welcome-contract` |
|
||||||
|
|
||||||
|
For each non-hermetic line, move the useful explanation from `tests/desktop-hijacking` directly above the entry. Add concise comments for the newly discovered entries describing the exact network, compositor-read, or window-mapping behavior.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/contract-manifest-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS with the discovered and manifested counts equal.
|
||||||
|
|
||||||
|
### Step 3: Remove the old ledger and update the documented count
|
||||||
|
|
||||||
|
Delete `tests/desktop-hijacking` and `tests/setup/desktop-hijacking-contract`. Update the exact contract count in `README.md`; removing one executable and adding one executable keeps the current count at 185 at this point.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/readme-contract
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both PASS.
|
||||||
|
|
||||||
|
### Step 4: Commit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/contracts.manifest tests/setup/contract-manifest-contract \
|
||||||
|
tests/setup/desktop-hijacking-contract tests/desktop-hijacking README.md
|
||||||
|
git commit -m "Test: Classify every contract capability"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Make `panama test` enforce the manifest and preserve diagnostics
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `tests/setup/test-runner-contract`
|
||||||
|
- Modify: `bin/panama`
|
||||||
|
- Modify: `tests/contracts.manifest`
|
||||||
|
- Modify: `README.md`
|
||||||
|
|
||||||
|
### Step 1: Write a public runner contract
|
||||||
|
|
||||||
|
Create `tests/setup/test-runner-contract`. Build a temporary repository containing a copy of `bin/panama`, a minimal `tests/contracts.manifest`, and executable fixture contracts named `pass-contract`, `fail-contract`, `stderr-contract`, `hang-contract`, `host-contract`, `desktop-contract`, and `network-contract`. Initialize it as a Git repository so the copied CLI behaves like the real installation.
|
||||||
|
|
||||||
|
The fixtures must record executions beneath the temporary directory and produce these behaviors:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# pass-contract
|
||||||
|
printf 'pass stdout\n'
|
||||||
|
|
||||||
|
# fail-contract
|
||||||
|
printf 'failure stdout\n'
|
||||||
|
printf 'failure stderr\n' >&2
|
||||||
|
exit 7
|
||||||
|
|
||||||
|
# stderr-contract
|
||||||
|
printf 'warning on success\n' >&2
|
||||||
|
|
||||||
|
# hang-contract
|
||||||
|
trap 'printf terminated >"$PANAMA_TEST_FIXTURE/terminated"; exit 124' TERM
|
||||||
|
while :; do sleep 1; done
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert the copied `panama` executable provides these public behaviors:
|
||||||
|
|
||||||
|
1. `panama test --safe` runs only `hermetic` fixtures and reports each skipped capability count, including `live-host`.
|
||||||
|
2. `panama test desktop` in a non-TTY fails without running the fixture and tells automation to pass `--allow live-desktop`.
|
||||||
|
3. `panama test --allow live-desktop desktop` runs it without a prompt.
|
||||||
|
4. Multiple `--allow` flags are repeatable and capability-specific; allowing desktop does not allow network.
|
||||||
|
5. An unknown flag, a second pattern, an unknown capability, and `--safe --allow live-desktop` are usage errors.
|
||||||
|
6. `PANAMA_TEST_TIMEOUT_SECONDS=1 panama test hang` terminates the fixture, reports a timeout, and exits nonzero.
|
||||||
|
7. A failed contract prints both captured stdout and captured stderr.
|
||||||
|
8. A successful contract with stderr prints a warning and the captured stderr.
|
||||||
|
9. Successful stdout stays hidden to keep the normal run scannable.
|
||||||
|
10. A pattern matching only skipped contracts is a distinct error from no pattern match.
|
||||||
|
11. Capture files are removed after success and failure.
|
||||||
|
12. `panama contracts <file>` labels hits with all manifest capabilities.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x tests/setup/test-runner-contract
|
||||||
|
tests/setup/test-runner-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL against the current runner at the first manifest-policy assertion.
|
||||||
|
|
||||||
|
### Step 2: Replace ledger parsing with manifest parsing
|
||||||
|
|
||||||
|
In `bin/panama`, replace `DESKTOP_HIJACKING_LEDGER` and `hijacking_entries` with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CONTRACT_MANIFEST="tests/contracts.manifest"
|
||||||
|
CONTRACT_CAPABILITIES=(hermetic live-host live-compositor live-desktop network privileged)
|
||||||
|
|
||||||
|
contract_manifest_entries() {
|
||||||
|
local line capabilities path
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
line="${line%%#*}"
|
||||||
|
read -r capabilities path _ <<<"$line"
|
||||||
|
[[ -n "${capabilities:-}" && -n "${path:-}" ]] || continue
|
||||||
|
printf '%s\t%s\n' "$path" "$capabilities"
|
||||||
|
done < "$PANAMA_DIR/$CONTRACT_MANIFEST"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not make a missing or malformed manifest mean “everything is safe.” Before either `cmd_test` or `cmd_contracts` consumes manifest entries, the public CLI independently validates the complete actual manifest: exactly two fields, known/non-empty/non-duplicate capabilities, exclusive `hermetic`, unique sorted paths, the executable/`*_test.py` fixture-excluded discovery set, and a non-empty directly preceding comment for every non-hermetic entry. Missing discovered contracts and stale manifest paths are fatal before selection or execution. Keep `tests/setup/contract-manifest-contract` as an independent validator rather than sourcing runtime code; a later contract cannot protect earlier execution.
|
||||||
|
|
||||||
|
### Step 3: Implement argument and capability policy
|
||||||
|
|
||||||
|
Parse position-independent arguments with one optional pattern and repeatable `--allow <capability>`. Apply these rules:
|
||||||
|
|
||||||
|
- `--safe` means only `hermetic` entries;
|
||||||
|
- `--safe` and any `--allow` together are a usage error;
|
||||||
|
- `--allow hermetic` is a usage error because hermetic contracts need no grant;
|
||||||
|
- plain `panama test` selects all matching manifest entries;
|
||||||
|
- selected non-hermetic capabilities missing from `--allow` require confirmation only when stdin and stderr are TTYs;
|
||||||
|
- non-TTY execution fails before running anything and names every missing capability;
|
||||||
|
- the prompt lists selected contract count and each external capability; default is no;
|
||||||
|
- there is no `--allow-all` path.
|
||||||
|
|
||||||
|
Use associative arrays for selected grants, manifest capabilities, and skipped counts so ordering stays deterministic.
|
||||||
|
|
||||||
|
### Step 4: Bound execution and retain output
|
||||||
|
|
||||||
|
At the beginning of `cmd_test`, validate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
local timeout_seconds="${PANAMA_TEST_TIMEOUT_SECONDS:-180}"
|
||||||
|
[[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || {
|
||||||
|
err 'PANAMA_TEST_TIMEOUT_SECONDS must be a positive integer.'
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The public CLI process must own contract supervision. Check that `mktemp -d` succeeds before constructing any capture path, then install top-level `EXIT`, `INT`, and `TERM` cleanup around the checked directory. Launch the active `timeout` asynchronously and retain its PID/process-group ownership. On INT or TERM sent to the exact CLI PID, signal the active timeout/process group, wait for it, remove capture storage, and exit 130 or 143. On normal completion, remove capture storage and clear the temporary traps without changing the aggregate test status. For each contract, run either `python3 path` or the executable through:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
timeout --signal=TERM --kill-after=5 "$timeout_seconds" \
|
||||||
|
"${runner[@]}" >"$stdout_file" 2>"$stderr_file"
|
||||||
|
```
|
||||||
|
|
||||||
|
Capture the exit status explicitly instead of relying on `set -e`. Treat status 124 or 137 as timeout. On failure, print a labeled stdout section when nonempty and a labeled stderr section when nonempty. On success, suppress stdout; if stderr is nonempty, call `warn` and print it. Continue through the selected suite and return nonzero if any contract failed or timed out.
|
||||||
|
|
||||||
|
### Step 5: Update `panama contracts`
|
||||||
|
|
||||||
|
Load the same manifest mapping and render labels such as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/quickshell/settings-pages-contract [live-compositor,live-desktop]
|
||||||
|
tests/setup/readme-contract [hermetic]
|
||||||
|
```
|
||||||
|
|
||||||
|
The grep-based relationship search remains unchanged. Its capability label must come only from the manifest.
|
||||||
|
|
||||||
|
### Step 6: Make the tests pass and update the count
|
||||||
|
|
||||||
|
Add `hermetic tests/setup/test-runner-contract` to `tests/contracts.manifest`. Update the README count from 185 to 186.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/contract-manifest-contract
|
||||||
|
tests/setup/test-runner-contract
|
||||||
|
tests/setup/readme-contract
|
||||||
|
./bin/panama test --safe test-runner
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all PASS. The final command reports one passing contract and does not request permission.
|
||||||
|
|
||||||
|
### Step 7: Commit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add bin/panama tests/contracts.manifest tests/setup/test-runner-contract README.md
|
||||||
|
git commit -m "Fix: Make contract execution safe and diagnostic"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Remove the calendar-pinned agent usage failure
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `tests/quickshell/agent-usage-contract`
|
||||||
|
|
||||||
|
### Step 1: Prove the current fixture is date-dependent
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/quickshell/agent-usage-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected before the change: FAIL on `todayTotalTokens` when the current day is not 2026-08-25.
|
||||||
|
|
||||||
|
### Step 2: Generate the session path and timestamps dynamically
|
||||||
|
|
||||||
|
Replace the fixed `2026/08/25` directory and timestamps with local-day fixture values, matching the collector's `datetime.now()` and `astimezone()` behavior:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
session_day="$(date +%Y/%m/%d)"
|
||||||
|
session_date="$(date +%Y-%m-%d)"
|
||||||
|
session_offset="$(date +%:z)"
|
||||||
|
mkdir -p "$work/codex/sessions/$session_day"
|
||||||
|
|
||||||
|
jq -nc --arg timestamp "${session_date}T10:00:00${session_offset}" \
|
||||||
|
'{type:"token_count",timestamp:$timestamp,payload:{type:"token_count",info:{total_token_usage:{input_tokens:999999,output_tokens:999999},last_token_usage:{input_tokens:1200,cached_input_tokens:1000,output_tokens:300}}}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Write two records at local `10:00:00` and `10:05:00` with `session_offset`, preserving the existing expectation of 3,000 last-turn tokens. Do not add a production clock override: only the fixture was wrong.
|
||||||
|
|
||||||
|
### Step 3: Verify and commit
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/quickshell/agent-usage-contract
|
||||||
|
./bin/panama test --safe agent-usage
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both PASS with `todayTotalTokens == 3000`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/quickshell/agent-usage-contract
|
||||||
|
git commit -m "Test: Make agent usage fixtures date independent"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Replace regex secret scanning with semantic fixtures
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `tests/server/scan-tracked-secrets.py`
|
||||||
|
- Create: `tests/server/fixtures/secrets/clean/compose.yml`
|
||||||
|
- Create: `tests/server/fixtures/secrets/clean/.env.example`
|
||||||
|
- Create: `tests/server/fixtures/secrets/clean/README.md`
|
||||||
|
- Create: `tests/server/fixtures/secrets/leaked/compose.yml`
|
||||||
|
- Create: `tests/server/fixtures/secrets/leaked/.env.example`
|
||||||
|
- Modify: `tests/server/compose-secrets-contract`
|
||||||
|
|
||||||
|
### Step 1: Add false-positive and true-positive fixtures
|
||||||
|
|
||||||
|
The clean fixture must include:
|
||||||
|
|
||||||
|
- Compose interpolation such as `POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`;
|
||||||
|
- `CHANGE_ME`, empty values, and boolean switches in `.env.example`;
|
||||||
|
- prose containing `password: example`, `secret key`, and the current server README sentence that triggered the audit.
|
||||||
|
|
||||||
|
The leaked fixture must include:
|
||||||
|
|
||||||
|
- a literal YAML credential `POSTGRES_PASSWORD: fixture-should-be-rejected`;
|
||||||
|
- a literal env credential `API_TOKEN=fixture-should-be-rejected`.
|
||||||
|
- a plain-text PEM private-key header with an exact `path:line: private key` finding;
|
||||||
|
- plain-text synthetic `sk-ant-`, minimum-supported-length `ghp_`, and `xoxb-` signatures, each with an exact `path:line: provider token` finding.
|
||||||
|
|
||||||
|
Add clean plain-text near-misses for each signature. Keep semantic fixture credentials intentionally invalid and signature fixtures clearly synthetic while still matching the supported signature shapes.
|
||||||
|
|
||||||
|
### Step 2: Write the scanner with only the Python standard library
|
||||||
|
|
||||||
|
`tests/server/scan-tracked-secrets.py` takes a root directory followed by tracked paths on argv. It must:
|
||||||
|
|
||||||
|
- parse `.env` and `.env.example` lines as key/value records;
|
||||||
|
- parse YAML Compose files by indentation-aware `KEY: VALUE` / `- KEY=VALUE` environment entries and top-level secret-bearing scalar assignments;
|
||||||
|
- treat `${VAR}`, `${VAR:-default}`, empty strings, `CHANGE_ME`, and booleans as non-secret;
|
||||||
|
- ignore keys ending `_FILE`, `_PATH`, `_NAME`, or `_KEY_NAME`;
|
||||||
|
- scan all text file types only for PEM private-key headers and known provider-token signatures;
|
||||||
|
- never interpret Markdown credential-shaped prose as a YAML assignment;
|
||||||
|
- print `path:line: key` for semantic leaks and return 1 when any are found.
|
||||||
|
|
||||||
|
Keep the parser deliberately scoped to Compose and env syntax; do not add a general YAML dependency that the installer does not provide.
|
||||||
|
|
||||||
|
### Step 3: Make the contract prove both directions
|
||||||
|
|
||||||
|
Before scanning the real tracked `server/` paths, have `tests/server/compose-secrets-contract` call the scanner on the clean fixture and require success, then on each leaked fixture and require failure with the expected key named. Finally pass the actual paths from:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C "$repo_dir" ls-files 'server/**' 'server/*'
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the existing `.gitignore` and tracked-`.env` assertions.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/server/compose-secrets-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS; the README prose is accepted while both literal fixture secrets are rejected.
|
||||||
|
|
||||||
|
### Step 4: Commit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/server/scan-tracked-secrets.py tests/server/fixtures/secrets \
|
||||||
|
tests/server/compose-secrets-contract
|
||||||
|
git commit -m "Test: Scan compose secrets by data shape"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Make the update contract execute the real CLI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `tests/setup/update-command-contract`
|
||||||
|
|
||||||
|
### Step 1: Replace the source-body assertions with a failing CLI fixture
|
||||||
|
|
||||||
|
Keep the existing `install --upgrade` stage and package-hash assertions, but replace the `sed`/`grep` inspection of `cmd_update` and `cmd_sync` with disposable Git repositories:
|
||||||
|
|
||||||
|
1. create a bare `origin.git`;
|
||||||
|
2. create an `upstream` clone with a copied `bin/panama` and a stub executable `install`;
|
||||||
|
3. push the initial branch;
|
||||||
|
4. clone it to `machine`;
|
||||||
|
5. append an upstream commit;
|
||||||
|
6. run `machine/bin/panama update` with `PANAMA_UPDATE_FIXTURE_LOG` consumed by the copied stub installer.
|
||||||
|
|
||||||
|
The installer stub must record its argv and optionally fail according to `PANAMA_UPDATE_INSTALL_RC`.
|
||||||
|
|
||||||
|
Add assertions that the real CLI:
|
||||||
|
|
||||||
|
- fast-forwards a clean machine clone to the new upstream commit;
|
||||||
|
- invokes the fixture installer with `--upgrade`;
|
||||||
|
- returns the installer's nonzero status;
|
||||||
|
- leaves `panama sync` separate from the installer path;
|
||||||
|
- on a dirty-tree stash-pop conflict, leaves no conflict markers and preserves a recoverable stash.
|
||||||
|
|
||||||
|
Do not yet assert that installation happens after WIP restoration. Package 3 of the approved design changes that ordering and will extend this same executable fixture with the new failing assertion before changing production code.
|
||||||
|
|
||||||
|
### Step 2: Repair status propagation in the contract harness
|
||||||
|
|
||||||
|
Change `run_install` so it returns the installer's status rather than `cat`'s status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
local status=0
|
||||||
|
PATH="$root/shim:$PATH" PANAMA_PATH="$root" PANAMA_RAN="$root/ran" \
|
||||||
|
XDG_STATE_HOME="$root/state" bash "$root/install" "$@" \
|
||||||
|
>"$root/out" 2>&1 || status=$?
|
||||||
|
cat "$root/ran"
|
||||||
|
return "$status"
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the indented-assignment parser with a tolerant anchored expression:
|
||||||
|
|
||||||
|
```python
|
||||||
|
match = re.match(r'^\s*STAGES=\((.*)\)\s*$', line)
|
||||||
|
```
|
||||||
|
|
||||||
|
Fail explicitly when the assignment is not found. Do not run parser code in process substitution whose exit status is ignored.
|
||||||
|
|
||||||
|
### Step 3: Verify and commit
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/update-command-contract
|
||||||
|
./bin/panama test --safe update-command
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both PASS and the fixture log proves `bin/panama update` actually ran.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/setup/update-command-contract
|
||||||
|
git commit -m "Test: Exercise updates through the real command"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Stop contracts from mutating tracked files or reading live preferences
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `config/dot/quickshell/scripts/panama-settings-docs`
|
||||||
|
- Modify: `tests/quickshell/settings-docs-contract`
|
||||||
|
- Modify: `tests/quickshell/settings-sync-contract`
|
||||||
|
|
||||||
|
### Step 1: Add a failing output-path contract for generated settings docs
|
||||||
|
|
||||||
|
Extend `tests/quickshell/settings-docs-contract` to copy the committed document to a temporary path, append drift to the copy, and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"$generator" --check --output "$scratch/settings.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
Capture `git diff -- docs/settings.md` before and after the contract and require byte-for-byte equality. Run the contract now.
|
||||||
|
|
||||||
|
Expected: FAIL because the generator does not accept `--output` and the current test still edits the tracked document.
|
||||||
|
|
||||||
|
### Step 2: Add a real output seam to the generator
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument("--output", type=pathlib.Path, default=OUTPUT)
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolve relative explicit paths against the current working directory, use that path for write/check behavior, and keep the existing default unchanged. `--stdout` must remain mutually exclusive with writing but may coexist with no explicit output.
|
||||||
|
|
||||||
|
Update the contract so the stale-copy check never writes `docs/settings.md`. Its trap should only remove the temporary directory.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/quickshell/settings-docs-contract
|
||||||
|
git diff --exit-code -- docs/settings.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS and no diff.
|
||||||
|
|
||||||
|
### Step 3: Seed settings-sync from a fixture instead of the live settings store
|
||||||
|
|
||||||
|
In `tests/quickshell/settings-sync-contract`, set `XDG_CONFIG_HOME="$work/source-config"` before the first export and create a representative `panama/settings.json` there from schema-valid values. Include at least:
|
||||||
|
|
||||||
|
- one boolean;
|
||||||
|
- one integer;
|
||||||
|
- one numeric enum;
|
||||||
|
- one string;
|
||||||
|
- one setting omitted from the import bundle to prove merge semantics.
|
||||||
|
|
||||||
|
Remove every read of `${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json` that occurs before the temporary home is established. Keep the allow-list completeness assertion, but compare against the explicit fixture rather than the user's preferences.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/quickshell/settings-sync-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS without reading or writing the real settings store.
|
||||||
|
|
||||||
|
### Step 4: Commit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add config/dot/quickshell/scripts/panama-settings-docs \
|
||||||
|
tests/quickshell/settings-docs-contract tests/quickshell/settings-sync-contract
|
||||||
|
git commit -m "Test: Isolate generated docs and settings fixtures"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Isolate the dock geometry contract
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `tests/quickshell/dock-position-contract`
|
||||||
|
|
||||||
|
### Step 1: Write the safety assertions before changing the harness
|
||||||
|
|
||||||
|
At contract start, record whether `config/dot/quickshell/dock-position-probe.qml` exists and its checksum if present. At every exit, assert the source tree matches that initial state. Also fail if the contract reads `${HOME}/.config/panama/settings.json`.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/quickshell/dock-position-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL because the contract creates its probe inside the tracked QML tree and reads live settings.
|
||||||
|
|
||||||
|
### Step 2: Copy the QML tree and seed deterministic settings
|
||||||
|
|
||||||
|
After creating `work`, copy the Quickshell tree to a fixture root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fixture_shell="$work/quickshell"
|
||||||
|
cp -a "$shell_dir/." "$fixture_shell/"
|
||||||
|
probe="$fixture_shell/dock-position-probe.qml"
|
||||||
|
```
|
||||||
|
|
||||||
|
Write the probe only to `$probe`. Seed `$work/config/panama/settings.json` with the schema defaults needed by the dock plus the position under test; do not copy the user's settings. Run each `qs -p` invocation from `$fixture_shell` with `XDG_CONFIG_HOME="$work/config"`.
|
||||||
|
|
||||||
|
The contract still maps isolated panel surfaces into the current Wayland session. Keep its manifest capability as `live-desktop`: moving its files and settings into temporary storage removes mutation risk but does not make the visible surfaces hermetic.
|
||||||
|
|
||||||
|
### Step 3: Verify isolation under an explicit desktop grant
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/quickshell/dock-position-contract
|
||||||
|
tests/setup/contract-manifest-contract
|
||||||
|
./bin/panama test --allow live-desktop dock-position
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all PASS, the source-tree checksum is unchanged, and the manifest entry remains `live-desktop`.
|
||||||
|
|
||||||
|
### Step 4: Commit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/quickshell/dock-position-contract
|
||||||
|
git commit -m "Test: Isolate dock geometry fixtures"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: Update operator documentation and run the Package 1 gate
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `README.md`
|
||||||
|
- Modify: `.claude/skills/panama/SKILL.md`
|
||||||
|
- Modify: `skills/panama-desktop/SKILL.md`
|
||||||
|
- Modify: `bin/panama`
|
||||||
|
|
||||||
|
### Step 1: Update the public command documentation
|
||||||
|
|
||||||
|
Document:
|
||||||
|
|
||||||
|
```text
|
||||||
|
panama test --safe
|
||||||
|
panama test --allow live-host updates
|
||||||
|
panama test --allow live-compositor keybinds
|
||||||
|
PANAMA_TEST_TIMEOUT_SECONDS=300 panama test --safe
|
||||||
|
```
|
||||||
|
|
||||||
|
State that:
|
||||||
|
|
||||||
|
- `tests/contracts.manifest` classifies every contract;
|
||||||
|
- `--safe` means hermetic only;
|
||||||
|
- plain full runs prompt in a terminal before any non-hermetic contract;
|
||||||
|
- automation must grant each required capability with repeatable `--allow`;
|
||||||
|
- failures print captured stdout/stderr and every contract has a default 180-second outer timeout.
|
||||||
|
|
||||||
|
Remove current claims that `--safe` is defined by `tests/desktop-hijacking`. Update `bin/panama`'s top comment, usage text, `cmd_test` comments, and `cmd_contracts` comments to match behavior. Leave historical design specs unchanged.
|
||||||
|
|
||||||
|
### Step 2: Run focused verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n bin/panama \
|
||||||
|
tests/setup/contract-manifest-contract \
|
||||||
|
tests/setup/test-runner-contract \
|
||||||
|
tests/setup/update-command-contract \
|
||||||
|
tests/quickshell/agent-usage-contract \
|
||||||
|
tests/quickshell/settings-docs-contract \
|
||||||
|
tests/quickshell/settings-sync-contract \
|
||||||
|
tests/server/compose-secrets-contract
|
||||||
|
python3 -m py_compile \
|
||||||
|
config/dot/quickshell/scripts/panama-settings-docs \
|
||||||
|
tests/server/scan-tracked-secrets.py
|
||||||
|
tests/setup/contract-manifest-contract
|
||||||
|
tests/setup/test-runner-contract
|
||||||
|
tests/setup/update-command-contract
|
||||||
|
tests/quickshell/agent-usage-contract
|
||||||
|
tests/quickshell/settings-docs-contract
|
||||||
|
tests/quickshell/settings-sync-contract
|
||||||
|
tests/server/compose-secrets-contract
|
||||||
|
tests/setup/readme-contract
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: every command exits 0.
|
||||||
|
|
||||||
|
### Step 3: Run the complete hermetic gate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/panama test --safe
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all selected hermetic contracts pass; the summary reports skipped counts for `live-host`, `live-compositor`, `live-desktop`, and `network`; no confirmation appears; no production shell or service is restarted.
|
||||||
|
|
||||||
|
If a manifest-labeled hermetic contract reaches the network, live compositor, live settings, root prompt, or tracked-file write, stop and reclassify or isolate it before continuing. Do not grant a capability merely to make this gate green.
|
||||||
|
|
||||||
|
### Step 4: Confirm worktree scope
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short
|
||||||
|
git diff -- config/bash/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: the pre-existing `.bashrc` modification remains unstaged and unchanged from its pre-plan state; only Package 1 files are staged or committed.
|
||||||
|
|
||||||
|
### Step 5: Commit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add README.md .claude/skills/panama/SKILL.md skills/panama-desktop/SKILL.md bin/panama
|
||||||
|
git commit -m "Docs: Explain contract capability gates"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Record the package handoff
|
||||||
|
|
||||||
|
Capture these facts in the implementation summary before beginning Package 2:
|
||||||
|
|
||||||
|
- final hermetic pass/fail/skip counts;
|
||||||
|
- every non-hermetic capability (`live-host`, `live-compositor`, `live-desktop`, `network`, and `privileged`) and its contract count;
|
||||||
|
- focused verification commands and exit codes;
|
||||||
|
- confirmation that `config/bash/.bashrc` stayed out of all commits;
|
||||||
|
- any contract retained as non-hermetic after an attempted isolation, with the exact reason.
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
# Server firewall transaction implementation plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make `setup-server` establish Cloudflare-scoped public web access and WireGuard-only admin access as one recoverable firewalld transaction.
|
||||||
|
|
||||||
|
**Architecture:** Put validation, desired-state calculation, journaling, and rollback in one standard-library Python command. Keep `setup-server` as the public orchestrator that verifies firewalld and invokes the updater; test both through stateful curl/systemd/firewalld command adapters with separate permanent/runtime state. Production endpoint URLs stay hardcoded, so no environment variable can redirect policy input.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3 standard library (`argparse`, `dataclasses`, `fcntl`, `hashlib`, `ipaddress`, `json`, `os`, `pathlib`, `signal`, `subprocess`), Bash 5, curl/firewalld/systemd command adapters, Panama contract runner.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md`
|
||||||
|
|
||||||
|
## Global constraints
|
||||||
|
|
||||||
|
- Do not call the host's `firewall-cmd`, mutate firewalld/systemd, contact Cloudflare from a contract, or apply a real server policy.
|
||||||
|
- `setup-server` must fail unless firewalld is installed, enabled, and active after one `enable --now` attempt.
|
||||||
|
- Validate both Cloudflare families completely before any privileged mutation. One bad family changes nothing.
|
||||||
|
- Public 80/443 must use dedicated Panama ipsets and exactly four rich rules. Port 81 belongs only to a unique WireGuard-only zone; no WireGuard zone leaves it closed.
|
||||||
|
- Never remove unrelated user-managed rules or service-specific ports. Only exact old direct 80/443/81 rules are eligible for the documented legacy migration.
|
||||||
|
- Keep old runtime rules until one successful reload. Roll back every failed transaction and retain the journal when rollback cannot be verified.
|
||||||
|
- Run the repository updater as the normal user. Only narrow `systemctl`/`firewall-cmd` mutations use sudo.
|
||||||
|
- No privileged timer or background service is added.
|
||||||
|
- The new contract is hermetic and runs under `panama test --safe`.
|
||||||
|
- Preserve `/home/gib/.local/share/Panama/config/bash/.bashrc` outside this worktree.
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
- `server/scripts/update-firewall`: parser, range validation, zone/policy inspection, desired state, journal, mutation, reload/readback, rollback.
|
||||||
|
- `server/firewall/cloudflare-v4.cidrs`: committed current canonical IPv4 ranges.
|
||||||
|
- `server/firewall/cloudflare-v6.cidrs`: committed current canonical IPv6 ranges.
|
||||||
|
- `tests/setup/server-firewall-contract`: stateful public `setup-server` fixture.
|
||||||
|
- `tests/setup/fixtures/firewall/`: valid/invalid/refreshed CIDR response data.
|
||||||
|
- `setup/scripts/setup-server`: required firewalld postconditions and updater call.
|
||||||
|
- `setup/packages/server-packages`: document firewalld as a required package, not a best-effort extra.
|
||||||
|
- `tests/contracts.manifest`: one new hermetic contract.
|
||||||
|
- `README.md`, `server/README.md`: exact exposure, refresh, and rollback policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Require firewalld and establish the first desired policy
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `server/scripts/update-firewall`
|
||||||
|
- Create: `server/firewall/cloudflare-v4.cidrs`
|
||||||
|
- Create: `server/firewall/cloudflare-v6.cidrs`
|
||||||
|
- Create: `tests/setup/server-firewall-contract`
|
||||||
|
- Create: `tests/setup/fixtures/firewall/valid-v4.cidrs`
|
||||||
|
- Create: `tests/setup/fixtures/firewall/valid-v6.cidrs`
|
||||||
|
- Modify: `setup/scripts/setup-server:40-60`
|
||||||
|
- Modify: `setup/packages/server-packages`
|
||||||
|
- Modify: `tests/contracts.manifest`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `server/scripts/update-firewall apply`, `server/scripts/update-firewall --validate-only DIRECTORY`, `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, PATH adapters, and committed bootstrap CIDRs.
|
||||||
|
- Produces: `main(argv) -> int`, `parse_cidrs(text, family) -> tuple[str, ...]`, `generation_name(family, cidrs) -> str`, `run_firewall(args, permanent=True)`, validation-only mode, and first-run policy application.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create current canonical range fixtures**
|
||||||
|
|
||||||
|
Use the official 2026-08-27 Cloudflare list as both committed bootstrap and initial valid fixture.
|
||||||
|
|
||||||
|
`cloudflare-v4.cidrs` must contain exactly:
|
||||||
|
|
||||||
|
```text
|
||||||
|
103.21.244.0/22
|
||||||
|
103.22.200.0/22
|
||||||
|
103.31.4.0/22
|
||||||
|
104.16.0.0/13
|
||||||
|
104.24.0.0/14
|
||||||
|
108.162.192.0/18
|
||||||
|
131.0.72.0/22
|
||||||
|
141.101.64.0/18
|
||||||
|
162.158.0.0/15
|
||||||
|
172.64.0.0/13
|
||||||
|
173.245.48.0/20
|
||||||
|
188.114.96.0/20
|
||||||
|
190.93.240.0/20
|
||||||
|
197.234.240.0/22
|
||||||
|
198.41.128.0/17
|
||||||
|
```
|
||||||
|
|
||||||
|
`cloudflare-v6.cidrs` must contain exactly:
|
||||||
|
|
||||||
|
```text
|
||||||
|
2400:cb00::/32
|
||||||
|
2405:8100::/32
|
||||||
|
2405:b500::/32
|
||||||
|
2606:4700::/32
|
||||||
|
2803:f800::/32
|
||||||
|
2a06:98c0::/29
|
||||||
|
2c0f:f248::/32
|
||||||
|
```
|
||||||
|
|
||||||
|
The production refresh endpoints are exactly `https://www.cloudflare.com/ips-v4` and `https://www.cloudflare.com/ips-v6`. The contract's PATH `curl` adapter returns fixture bytes for only those exact URLs; no environment variable or config can replace them, and the contract never contacts either endpoint.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build the stateful command model and red first-run cases**
|
||||||
|
|
||||||
|
Store fixture state in JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"systemctl": {"installed": true, "enabled": true, "active": true},
|
||||||
|
"permanent": {
|
||||||
|
"defaultZone": "public",
|
||||||
|
"zones": {
|
||||||
|
"public": {"interfaces": ["eth0"], "target": "default", "services": ["ssh"], "ports": ["80/tcp", "443/tcp", "81/tcp", "2222/tcp"], "richRules": []},
|
||||||
|
"wireguard": {"interfaces": ["wg0"], "target": "default", "services": [], "ports": [], "richRules": []}
|
||||||
|
},
|
||||||
|
"ipsets": {}
|
||||||
|
},
|
||||||
|
"runtime": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The Python `firewall-cmd` stub must support only the exact query/mutation argv used by the updater, append JSON argv arrays to `calls.jsonl`, keep permanent/runtime separate, copy permanent to runtime on reload, and reject unknown argv with status `97`. The curl stub accepts only `--fail --silent --show-error --location --connect-timeout 10 --max-time 30 --max-filesize 65536 --output PATH URL`, logs argv, and copies the selected fixture response. `systemctl` supports `list-unit-files`, `is-enabled`, `is-active`, and `enable --now`. `sudo` logs and execs. Stub linger, sysctl, podman, and user-systemctl paths so the real `setup-server` finishes without host access.
|
||||||
|
|
||||||
|
Write red cases:
|
||||||
|
|
||||||
|
```text
|
||||||
|
firewalld missing -> nonzero; zero firewall mutations
|
||||||
|
enable/start failure -> nonzero; zero firewall mutations
|
||||||
|
postcheck inactive/disabled -> nonzero; zero firewall mutations
|
||||||
|
valid first run -> content-addressed v4/v6 ipsets, four rich rules, 81 only in wireguard, legacy direct 80/443/81 removed, ssh and 2222 preserved, one check-config and one reload
|
||||||
|
no wireguard zone -> 80/443 policy succeeds, 81 absent, exact closed-port diagnostic
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the contract and confirm current setup is red**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n setup/scripts/setup-server tests/setup/server-firewall-contract
|
||||||
|
tests/setup/server-firewall-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: nonzero because current setup treats inactive firewalld as success and opens all three ports directly.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement validation, desired state, and first apply**
|
||||||
|
|
||||||
|
Start the updater with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def parse_cidrs(text: str, family: int) -> tuple[str, ...]:
|
||||||
|
if len(text.encode("utf-8")) > 65536:
|
||||||
|
raise PolicyError("CIDR response exceeds 65536 bytes")
|
||||||
|
lines = text.splitlines()
|
||||||
|
if not lines:
|
||||||
|
raise PolicyError("CIDR list is empty")
|
||||||
|
parsed: list[str] = []
|
||||||
|
for raw in lines:
|
||||||
|
if not raw or raw != raw.strip() or any(ch.isspace() for ch in raw):
|
||||||
|
raise PolicyError(f"invalid CIDR line: {raw!r}")
|
||||||
|
network = ipaddress.ip_network(raw, strict=True)
|
||||||
|
if network.version != family or network.prefixlen == 0 or str(network) != raw:
|
||||||
|
raise PolicyError(f"invalid IPv{family} CIDR: {raw}")
|
||||||
|
parsed.append(raw)
|
||||||
|
if len(parsed) != len(set(parsed)):
|
||||||
|
raise PolicyError("CIDR list contains duplicates")
|
||||||
|
return tuple(parsed)
|
||||||
|
|
||||||
|
def generation_name(family: int, cidrs: tuple[str, ...]) -> str:
|
||||||
|
digest = hashlib.sha256(("\n".join(cidrs) + "\n").encode()).hexdigest()[:12]
|
||||||
|
return f"panama-cf{family}-{digest}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Build exactly these rich-rule strings, substituting the content-addressed generation name:
|
||||||
|
|
||||||
|
```text
|
||||||
|
rule family="ipv4" source ipset="V4_GENERATION" port port="80" protocol="tcp" accept
|
||||||
|
rule family="ipv4" source ipset="V4_GENERATION" port port="443" protocol="tcp" accept
|
||||||
|
rule family="ipv6" source ipset="V6_GENERATION" port port="80" protocol="tcp" accept
|
||||||
|
rule family="ipv6" source ipset="V6_GENERATION" port port="443" protocol="tcp" accept
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement `main(argv)` with only two accepted forms: `apply` and `--validate-only DIRECTORY`. Validation-only mode reads `DIRECTORY/cloudflare-v4.cidrs` and `DIRECTORY/cloudflare-v6.cidrs`, runs the same strict parser used by apply, prints the two generation names, and exits without consulting systemd, the network, config/state directories, sudo, or firewalld. Unknown or combined arguments exit nonzero with usage.
|
||||||
|
|
||||||
|
Query default/public and WireGuard zones before mutation. Apply new permanent ipsets/rules, exact legacy removal, `--check-config`, one `--reload`, and readback. The first task may use an in-memory snapshot; Task 3 adds durable recovery.
|
||||||
|
|
||||||
|
Replace the old port loop in `setup-server` with exact firewalld installation/state checks: `command -v firewall-cmd`, `systemctl list-unit-files firewalld.service`, one `sudo systemctl enable --now firewalld.service`, then successful `systemctl is-enabled firewalld.service` and `systemctl is-active firewalld.service`. Only after those postconditions invoke:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"$PANAMA_PATH/server/scripts/update-firewall" apply
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add the manifest entry and verify first-run behavior**
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Server firewall policy runs against stateful systemctl/firewall-cmd adapters.
|
||||||
|
hermetic tests/setup/server-firewall-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n setup/scripts/setup-server tests/setup/server-firewall-contract
|
||||||
|
python3 -m py_compile server/scripts/update-firewall
|
||||||
|
server/scripts/update-firewall --validate-only server/firewall
|
||||||
|
tests/setup/server-firewall-contract
|
||||||
|
tests/setup/contract-manifest-contract
|
||||||
|
./bin/panama test --safe server-firewall
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the required first policy**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add server/scripts/update-firewall server/firewall tests/setup/fixtures/firewall \
|
||||||
|
tests/setup/server-firewall-contract setup/scripts/setup-server \
|
||||||
|
setup/packages/server-packages tests/contracts.manifest
|
||||||
|
git commit -m "Fix: Establish scoped server firewall policy"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Reject bad ranges, unsafe zones, and foreign exposure
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `server/scripts/update-firewall`
|
||||||
|
- Modify: `tests/setup/server-firewall-contract`
|
||||||
|
- Create: `tests/setup/fixtures/firewall/refreshed-v4.cidrs`
|
||||||
|
- Create: `tests/setup/fixtures/firewall/refreshed-v6.cidrs`
|
||||||
|
- Create: `tests/setup/fixtures/firewall/invalid-*.cidrs`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `parse_cidrs`, content generations, first-run apply, the exact PATH curl adapter, and stored zone config.
|
||||||
|
- Produces: `load_zone_config`, `select_zones`, `find_exposure_conflicts`, refresh fallback, and idempotent convergence.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add exhaustive red validation and selection tables**
|
||||||
|
|
||||||
|
Fixture-invalid classes are exact:
|
||||||
|
|
||||||
|
```text
|
||||||
|
empty
|
||||||
|
blank-line
|
||||||
|
leading-space
|
||||||
|
trailing-token
|
||||||
|
comment
|
||||||
|
host-bits-set
|
||||||
|
wrong-family
|
||||||
|
slash-zero
|
||||||
|
duplicate
|
||||||
|
non-UTF8
|
||||||
|
oversized-65537-bytes
|
||||||
|
valid-v4-plus-invalid-v6
|
||||||
|
invalid-v4-plus-valid-v6
|
||||||
|
```
|
||||||
|
|
||||||
|
For each, assert byte-for-byte permanent/runtime/config/journal equality and zero mutation argv. Add zone cases for multiple `wg*` zones, public equal to WireGuard, stored missing zone, and WireGuard zone containing `wg0` plus `eth1`; all fail before mutation. Zero WireGuard candidates remains the safe success from Task 1.
|
||||||
|
|
||||||
|
Add conflicts: public `http`/`https` service, port range covering 80/443, ACCEPT zone target, and unrelated rich rule accepting 80/443. Assert a diagnostic with exact `firewall-cmd --zone=ZONE --list-all` inspection command and no deletion.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add red idempotence and two-family refresh cases**
|
||||||
|
|
||||||
|
Run the same valid policy twice. The second run must have no mutating `firewall-cmd`, check-config, or reload calls. Then provide valid refreshed v4/v6 lists and assert both generations change in one transaction; one changed plus one invalid changes neither.
|
||||||
|
|
||||||
|
Simulate curl network failure. With a valid installed policy, assert warning plus no changes. With no policy, assert the committed bootstrap pair is used.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the contract and confirm missing validation behavior**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/server-firewall-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: nonzero on malformed, conflict, refresh, and idempotence cases not yet implemented.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement strict refresh, zone config, conflict detection, and no-op convergence**
|
||||||
|
|
||||||
|
Fetch both hardcoded official endpoints without sudo using curl argv `--fail --silent --show-error --location --connect-timeout 10 --max-time 30 --max-filesize 65536 --output PART URL`. Use a private destination, verify its actual size is at most 65,536 bytes, and decode with strict ASCII before calling `parse_cidrs`. Treat the pair as one candidate and remove partial files on every exit/signal. Parse config with explicit `key=value` names and no shell execution.
|
||||||
|
|
||||||
|
Before mutation, compute normalized current and desired Panama-owned state. If identical, print `Server firewall policy already current` and return without check-config/reload. Detect foreign broad exposure but allow unrelated ports/services such as SSH and 2222. Remove only exact direct legacy ports.
|
||||||
|
|
||||||
|
Write selected-zone config to a private `.part` and rename it only after successful policy readback. Omit `wireguard_zone` when no candidate exists.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run focused checks and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m py_compile server/scripts/update-firewall
|
||||||
|
tests/setup/server-firewall-contract
|
||||||
|
./bin/panama test --safe server-firewall
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add server/scripts/update-firewall tests/setup/server-firewall-contract \
|
||||||
|
tests/setup/fixtures/firewall
|
||||||
|
git commit -m "Fix: Validate firewall ranges and ownership"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Journal and roll back interrupted firewall transactions
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `server/scripts/update-firewall`
|
||||||
|
- Modify: `tests/setup/server-firewall-contract`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: normalized current/desired state and mutation adapter from Tasks 1-2.
|
||||||
|
- Produces: `PolicySnapshot`, `write_pending`, `restore_snapshot`, `verify_snapshot`, signal-safe rollback, and pending-journal recovery.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add red failure injection and journal cases**
|
||||||
|
|
||||||
|
The firewall stub accepts `FAIL_ONCE_JSON`, an exact normalized argv array. Inject one failure at each mutation boundary: create ipset, add entry, add rich rule, add 81, remove legacy port, remove old rule, remove old ipset, check-config, and reload. Separately inject permanent/runtime readback mismatch, selected-zone config write/rename failure, and pending-journal deletion failure after reload; each is still a failed transaction and must restore the snapshot.
|
||||||
|
|
||||||
|
For every case assert updater nonzero, permanent/runtime restored byte-for-byte, rollback reload performed when needed, and journal removed only after verified restoration. Add rollback-reload failure: journal remains with mode `0600`, diagnostic names its absolute path and exact retry command.
|
||||||
|
|
||||||
|
Seed a valid `pending.json` before invocation and assert restoration happens before curl or new-policy evaluation. Seed malformed, symlinked, foreign-owned, and mode-wrong journals; assert fail closed without mutation.
|
||||||
|
|
||||||
|
Start an apply process, block one mutation, send TERM to the updater's exact PID, release the stub, and require status `143`, restored state, and no orphan process.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the contract and confirm current failure paths are red**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/server-firewall-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: nonzero because the updater has no durable journal or exact-PID signal rollback.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement checked journal and rollback ownership**
|
||||||
|
|
||||||
|
Use a frozen dataclass with JSON round-trip methods:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class PolicySnapshot:
|
||||||
|
public_zone: str
|
||||||
|
wireguard_zone: str | None
|
||||||
|
panama_ipsets: dict[str, tuple[str, ...]]
|
||||||
|
panama_rich_rules: tuple[str, ...]
|
||||||
|
legacy_ports: tuple[str, ...]
|
||||||
|
wireguard_has_81: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Acquire `${state}/firewall/lock` with `fcntl.flock(LOCK_EX | LOCK_NB)`. Reject symlinked state components. Write `pending.json.part` with mode `0600`, fsync file and directory, then rename before the first mutation.
|
||||||
|
|
||||||
|
Start each mutating sudo/firewalld subprocess in its own process group and track its leader PID. INT/TERM handlers signal that exact group, wait for it, call `restore_snapshot`, verify permanent and runtime equality after rollback reload, then exit 130/143. The contract asserts the recorded child is gone before it returns. Normal completion clears handlers before deleting the journal.
|
||||||
|
|
||||||
|
At startup, a valid pending journal is restored and verified before network/config evaluation. Invalid journal ownership/mode/shape fails closed and prints inspection instructions.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify all injected failures and signals**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m py_compile server/scripts/update-firewall
|
||||||
|
tests/setup/server-firewall-contract
|
||||||
|
./bin/panama test --safe server-firewall
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit recovery behavior**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add server/scripts/update-firewall tests/setup/server-firewall-contract
|
||||||
|
git commit -m "Fix: Roll back firewall transactions"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Document policy and run the complete firewall gate
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `README.md:56-64`
|
||||||
|
- Modify: `server/README.md:1-90`
|
||||||
|
- Modify: `.claude/skills/panama/SKILL.md`
|
||||||
|
- Modify: `skills/panama-sudo/SKILL.md` if it documents server setup
|
||||||
|
- Modify: `tests/setup/readme-contract`
|
||||||
|
- Modify: `tests/server/containers-shape-contract` only if it asserts the old global 81 policy
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: final updater behavior and its exact operator diagnostics.
|
||||||
|
- Produces: accurate exposure/refresh/rollback documentation and Package 2 firewall verification evidence.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write documentation assertions before prose**
|
||||||
|
|
||||||
|
Require README/server README to state: firewalld required, Cloudflare-only 80/443, dedicated IPv4/IPv6 ipsets, WireGuard-only 81, no automatic service ports, invalid refresh preserves last known good, one recoverable transaction, and no privileged timer. Reject the old statement that setup simply opens 80/443/81.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Confirm old documentation fails**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/readme-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update docs without live-cutover claims**
|
||||||
|
|
||||||
|
Explain that `setup-server` and server upgrades refresh the policy; first setup may use committed official ranges; missing WireGuard leaves 81 closed; conflicts require manual inspection; fixture tests model rollback but do not apply host rules.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the firewall plan gate**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n setup/scripts/setup-server tests/setup/server-firewall-contract
|
||||||
|
python3 -m py_compile server/scripts/update-firewall
|
||||||
|
server/scripts/update-firewall --validate-only server/firewall
|
||||||
|
tests/setup/server-firewall-contract
|
||||||
|
tests/setup/role-contract
|
||||||
|
tests/setup/readme-contract
|
||||||
|
tests/server/containers-shape-contract
|
||||||
|
./bin/panama test --safe
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 134 hermetic contracts pass after the SSH, provenance, and firewall contracts exist; non-hermetic skip counts remain unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit documentation and final fixture adjustments**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add README.md server/README.md .claude/skills/panama/SKILL.md \
|
||||||
|
skills/panama-sudo/SKILL.md tests/setup/readme-contract \
|
||||||
|
tests/server/containers-shape-contract
|
||||||
|
git commit -m "Docs: Explain the server firewall transaction"
|
||||||
|
```
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
# SSH hardening transaction implementation plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make `boot --server` refuse unsafe SSH hardening and atomically roll back failed validation or reloads.
|
||||||
|
|
||||||
|
**Architecture:** Keep production logic in the standalone `boot` file because it runs before the repository exists. Exercise the public `boot --server` command through a PTY, a temporary filesystem root, and PATH command adapters; never call private functions directly or touch host SSH state.
|
||||||
|
|
||||||
|
**Tech Stack:** Bash 5, Python 3 standard library for the PTY driver, OpenSSH/systemd command adapters, Panama contract runner.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md`
|
||||||
|
|
||||||
|
## Global constraints
|
||||||
|
|
||||||
|
- Preserve the public `boot --server` command and the non-root desktop bootstrap path.
|
||||||
|
- Do not reload a real SSH service, edit `/etc/ssh`, create a real account, or run a live privileged check.
|
||||||
|
- A missing or unsafe target key, missing SSH unit, declined prompt, or unsupported
|
||||||
|
pre-existing Panama drop-in keeps root/password access unchanged and continues bootstrap.
|
||||||
|
- Candidate validation or reload failure restores the prior drop-in and stops before clone/install handoff.
|
||||||
|
- Exact safe state is target ownership plus `.ssh` mode `0700` and `authorized_keys`
|
||||||
|
mode `0600`; symlinks, root UID, relative homes, blank/comment-only keys,
|
||||||
|
malformed non-comment lines, and foreign ownership are refused.
|
||||||
|
- The installed policy is `00-panama.conf` with `PermitRootLogin no`,
|
||||||
|
`PasswordAuthentication no`, and `KbdInteractiveAuthentication no`.
|
||||||
|
- Before reload, `sshd -t` and root/target `sshd -T -C` checks must prove the
|
||||||
|
desired effective policy. Earlier main-config precedence therefore fails closed.
|
||||||
|
- Root-key destination creation and writing run as the target UID. Revalidation
|
||||||
|
follows, and no same-named primary group is assumed.
|
||||||
|
- Existing regular drop-ins retain complete metadata on rollback. Symlinks,
|
||||||
|
directories, FIFOs, and other non-regular objects make hardening unavailable.
|
||||||
|
- Transaction traps are armed before the first candidate or backup artifact.
|
||||||
|
- `PANAMA_BOOT_FIXTURE_ROOT` is accepted only by a real non-root process whose stubbed `id -u` reports root. Real root plus that variable must fail closed.
|
||||||
|
- The new public contract is hermetic and must run under `panama test --safe`.
|
||||||
|
- Preserve the user's unstaged `/home/gib/.local/share/Panama/config/bash/.bashrc` change outside this worktree.
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
- `boot`: account/key preconditions, test-only filesystem adapter, SSH unit detection, atomic drop-in transaction, rollback.
|
||||||
|
- `tests/setup/root-server-bootstrap-contract`: PTY and stateful command/filesystem fixture for the real public command.
|
||||||
|
- `tests/contracts.manifest`: one `hermetic` entry for the new executable contract.
|
||||||
|
- `tests/setup/boot-contract`: retain the non-root clone/handoff behavior; adjust only if the later verified-bootstrap plan changes it.
|
||||||
|
- `tests/setup/role-contract`: retain role/stage assertions.
|
||||||
|
- `README.md`: describe key-gated transactional hardening without claiming a live reload was tested.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Gate hardening on a verified login path
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/setup/root-server-bootstrap-contract`
|
||||||
|
- Modify: `boot:52-115`
|
||||||
|
- Modify: `tests/contracts.manifest`
|
||||||
|
- Test: `tests/setup/root-server-bootstrap-contract`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: public `boot --server`, `PANAMA_BOOT_FIXTURE_ROOT`, PTY answers `gib`, password already set, and `Y` for hardening.
|
||||||
|
- Produces: `system_path ABSOLUTE_PATH`, `safe_authorized_keys USER HOME`, and `harden_server_ssh USER HOME`; later tasks extend the last function with the transaction.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Build the public fixture and write the failing precondition table**
|
||||||
|
|
||||||
|
Create a checked temporary tree with `root/etc/ssh/sshd_config.d`, `root/root/.ssh`, `root/home/gib/.ssh`, `bin`, `calls`, and a stub cloned installer. Use Python's `pty.openpty()` to start:
|
||||||
|
|
||||||
|
```python
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
"PATH": f"{stub_dir}:/usr/bin:/bin",
|
||||||
|
"PANAMA_BOOT_FIXTURE_ROOT": fixture_root,
|
||||||
|
"PANAMA_PATH": f"{fixture_root}/home/gib/.local/share/Panama",
|
||||||
|
"HOME": f"{fixture_root}/root",
|
||||||
|
}
|
||||||
|
process = subprocess.Popen(
|
||||||
|
["bash", boot, "--server"],
|
||||||
|
stdin=slave,
|
||||||
|
stdout=slave,
|
||||||
|
stderr=slave,
|
||||||
|
env=env,
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
|
os.write(master, b"gib\nY\n")
|
||||||
|
```
|
||||||
|
|
||||||
|
PATH stubs must log one shell-escaped argv vector per line. `id -u` with no username reports `0`; `id -u root` reports `0`; `id -u gib` reports `1000`; `id -nG gib` prints `gib wheel`; `passwd -S gib` prints `gib PS`; `getent passwd gib` prints the logical absolute home `/home/gib`; `runuser` materializes the clone/install handoff without changing users. `boot` resolves that logical home beneath the fixture root through `system_path`. Stub `stat` for fixture ownership metadata plus `dnf`, `git`, `sshd`, and `systemctl`; any unexpected command exits `97`.
|
||||||
|
|
||||||
|
Run one table row per unsafe state:
|
||||||
|
|
||||||
|
```text
|
||||||
|
missing
|
||||||
|
empty
|
||||||
|
comment-only
|
||||||
|
malformed-key
|
||||||
|
mixed-valid-and-malformed-key
|
||||||
|
malformed-root-key
|
||||||
|
ssh-directory-symlink
|
||||||
|
authorized-keys-symlink
|
||||||
|
directory-wrong-mode
|
||||||
|
file-wrong-mode
|
||||||
|
directory-wrong-owner
|
||||||
|
file-wrong-owner
|
||||||
|
root-target-account
|
||||||
|
relative-home
|
||||||
|
```
|
||||||
|
|
||||||
|
For every row assert status `0`, a diagnostic containing `SSH hardening unavailable`, no `sshd -t`, no `systemctl reload`, no drop-in change, and a recorded install handoff. Add safe existing-key and safe root-key-copy rows that currently reach the unsafe direct-write path and therefore fail the new expected command ordering.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the contract and confirm the red behavior**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n tests/setup/root-server-bootstrap-contract
|
||||||
|
tests/setup/root-server-bootstrap-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: nonzero findings showing current `boot` writes/reloads without the required key checks and has no fixture-root support.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the guarded filesystem adapter and key checks**
|
||||||
|
|
||||||
|
Add these shapes near the root branch, using `stat -Lc` only after rejecting symlinks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BOOT_ROOT="${PANAMA_BOOT_FIXTURE_ROOT:-}"
|
||||||
|
if [[ -n "$BOOT_ROOT" && "$EUID" -eq 0 ]]; then
|
||||||
|
echo "boot: PANAMA_BOOT_FIXTURE_ROOT is test-only" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
system_path() {
|
||||||
|
local path="$1"
|
||||||
|
[[ "$path" == /* ]] || return 2
|
||||||
|
printf '%s%s\n' "$BOOT_ROOT" "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_authorized_keys() {
|
||||||
|
local username="$1" user_home="$2" uid ssh_dir keys
|
||||||
|
uid="$(id -u "$username")" || return 1
|
||||||
|
[[ "$uid" =~ ^[0-9]+$ && "$uid" != 0 && "$user_home" == /* ]] || return 1
|
||||||
|
ssh_dir="$user_home/.ssh"
|
||||||
|
keys="$ssh_dir/authorized_keys"
|
||||||
|
[[ -d "$ssh_dir" && ! -L "$ssh_dir" && -f "$keys" && ! -L "$keys" ]] || return 1
|
||||||
|
[[ "$(stat -Lc '%u:%a' "$ssh_dir")" == "$uid:700" ]] || return 1
|
||||||
|
[[ "$(stat -Lc '%u:%a' "$keys")" == "$uid:600" ]] || return 1
|
||||||
|
valid_authorized_keys "$keys"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`valid_authorized_keys` skips blank/comment lines, requires at least one remaining
|
||||||
|
line, and runs `ssh-keygen -l` on every remaining line. Resolve the target home,
|
||||||
|
`/root/.ssh/authorized_keys`, and `/etc/ssh/sshd_config.d` through `system_path`.
|
||||||
|
A copyable root key must be a non-symlinked regular file owned by UID 0, mode
|
||||||
|
`0600`, whose non-comment lines all parse. Never overwrite an existing
|
||||||
|
`authorized_keys`. When it is absent, either create a missing `.ssh` or require
|
||||||
|
an existing `.ssh` to already be a real directory owned by the target UID with
|
||||||
|
mode `0700`. Run final directory creation and key writing as the target UID,
|
||||||
|
then revalidate ownership, modes, and key parsing. Do not chown the destination
|
||||||
|
or assume the user's primary group is named after the user. If
|
||||||
|
`safe_authorized_keys` still fails, print the unavailable message and skip the
|
||||||
|
prompt/transaction.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the hermetic manifest entry and run focused checks**
|
||||||
|
|
||||||
|
Insert the sorted manifest line with a directly preceding comment:
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Root bootstrap runs entirely against a temporary filesystem and PATH adapters.
|
||||||
|
hermetic tests/setup/root-server-bootstrap-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n boot tests/setup/root-server-bootstrap-contract
|
||||||
|
tests/setup/contract-manifest-contract
|
||||||
|
tests/setup/root-server-bootstrap-contract
|
||||||
|
tests/setup/boot-contract
|
||||||
|
tests/setup/role-contract
|
||||||
|
./bin/panama test --safe root-server-bootstrap
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pass; the public fixture proves unsafe keys do not invoke `sshd` or reload while bootstrap still hands off.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit the precondition gate**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add boot tests/setup/root-server-bootstrap-contract tests/contracts.manifest
|
||||||
|
git commit -m "Fix: Gate SSH hardening on a verified key"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Make the drop-in transaction recoverable
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `boot:42-137`
|
||||||
|
- Modify: `tests/setup/root-server-bootstrap-contract`
|
||||||
|
- Test: `tests/setup/root-server-bootstrap-contract`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `safe_authorized_keys`, `system_path`, the fixture's `sshd`/`systemctl` state files, and a detected service name.
|
||||||
|
- Produces: `detect_ssh_unit`, `restore_ssh_dropin`, and a complete `harden_server_ssh USER HOME` transaction returning 0 only after validation and reload.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing success and rollback scenarios**
|
||||||
|
|
||||||
|
Extend the command state with `SSHD_RESULTS` and `RELOAD_RESULTS`, consumed one result per call. Add exact cases:
|
||||||
|
|
||||||
|
```text
|
||||||
|
success-without-prior-dropin: validate=0 reload=0
|
||||||
|
success-replaces-prior-dropin: validate=0 reload=0
|
||||||
|
candidate-invalid: validate=1,0 reload=<none>
|
||||||
|
effective-root-policy-conflict: syntax=0 root-policy=conflict rollback-validate=0 reload=<none>
|
||||||
|
effective-target-policy-conflict: syntax=0 root-policy=safe target-policy=conflict rollback-validate=0 reload=<none>
|
||||||
|
candidate-reload-fails: validate=0,0 reload=1,0
|
||||||
|
rollback-validation-fails: validate=0,1 reload=1
|
||||||
|
rollback-reload-fails: validate=0,0 reload=1,1
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert the desired three-line content; syntax and root/target effective validation
|
||||||
|
before reload; only the detected unit; content and complete metadata restoration;
|
||||||
|
restored validation/reload ordering; nonzero status and no install handoff on every
|
||||||
|
transactional failure; no `*.tmp`/`*.backup` residue on success; and retained backup
|
||||||
|
or no-prior-file removal instructions when rollback fails. Add declined-hardening,
|
||||||
|
missing-unit, symlink/directory/FIFO drop-in, target-UID normalization, candidate and
|
||||||
|
backup preparation signals, and actual-root fixture-guard cases.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused contract and confirm it fails on current code**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/root-server-bootstrap-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: nonzero because current code writes the final path directly, never validates, guesses units through reload failure, and reports success after failed reload.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement unit detection, atomic install, and rollback**
|
||||||
|
|
||||||
|
Use explicit transaction state and traps. The core shape is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
detect_ssh_unit() {
|
||||||
|
local unit
|
||||||
|
for unit in sshd.service ssh.service; do
|
||||||
|
systemctl cat "$unit" >/dev/null 2>&1 && { printf '%s\n' "$unit"; return 0; }
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_ssh_dropin() {
|
||||||
|
if (( ssh_had_prior )); then
|
||||||
|
local restore
|
||||||
|
restore="$(mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.restore)" || return 1
|
||||||
|
cp -a -- "$ssh_backup" "$restore"
|
||||||
|
mv -f -- "$restore" "$ssh_dropin"
|
||||||
|
else
|
||||||
|
rm -f -- "$ssh_dropin"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reject an existing `00-panama.conf` unless it is a non-symlink regular file. Save
|
||||||
|
the prior `EXIT`, `INT`, and `TERM` traps and arm state-aware preparation cleanup
|
||||||
|
before creating any artifact. Create the candidate with
|
||||||
|
`umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.tmp`, write the exact
|
||||||
|
three-line desired content, and preserve an existing final file with `cp -a` in a
|
||||||
|
collision-safe same-directory `mktemp` name ending `.backup`, not `.conf`.
|
||||||
|
Atomically activate with `mv -f`. Preparation signals remove known artifacts without
|
||||||
|
touching the final path; activated signals restore, validate, and reload. Every
|
||||||
|
success or handled failure path restores the prior traps before returning.
|
||||||
|
|
||||||
|
After activation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if ! sshd -t; then
|
||||||
|
restore_ssh_dropin
|
||||||
|
sshd -t || rollback_failed=1
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! effective_ssh_policy_is_hardened "$username"; then
|
||||||
|
restore_ssh_dropin
|
||||||
|
sshd -t || rollback_failed=1
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! systemctl reload "$ssh_unit"; then
|
||||||
|
restore_ssh_dropin
|
||||||
|
sshd -t || rollback_failed=1
|
||||||
|
systemctl reload "$ssh_unit" || rollback_failed=1
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
`effective_ssh_policy_is_hardened` uses `sshd -T -C` for root and target
|
||||||
|
contexts. Root must report all three denials; target must report both authentication
|
||||||
|
denials. On clean success clear the transaction state, restore traps, and remove the
|
||||||
|
backup. On rollback failure with a prior file, keep the metadata-preserving backup
|
||||||
|
and print its absolute path plus validation/reload commands. With no prior file,
|
||||||
|
print `rm -f -- /etc/ssh/sshd_config.d/00-panama.conf`, `sshd -t`, and the detected
|
||||||
|
reload command. Do not continue to clone/install after a transactional failure.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify failure status, cleanup, and old public behavior**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n boot tests/setup/root-server-bootstrap-contract
|
||||||
|
tests/setup/root-server-bootstrap-contract
|
||||||
|
tests/setup/boot-contract
|
||||||
|
tests/setup/role-contract
|
||||||
|
./bin/panama test --safe root-server-bootstrap
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pass; every rollback scenario preserves the previous drop-in and the safe success path validates before one reload.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit the transaction**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add boot tests/setup/root-server-bootstrap-contract
|
||||||
|
git commit -m "Fix: Roll back failed SSH hardening"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Synchronize operator documentation and close the SSH plan
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `README.md:40-48`
|
||||||
|
- Modify: `.claude/skills/panama/SKILL.md`
|
||||||
|
- Modify: `skills/panama-desktop/SKILL.md` only if it describes root bootstrap
|
||||||
|
- Test: `tests/setup/readme-contract`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the landed `boot --server` behavior from Tasks 1-2.
|
||||||
|
- Produces: accurate user-facing preconditions, skip behavior, rollback behavior, and no claim of live-host proof.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the documentation assertions first**
|
||||||
|
|
||||||
|
Extend `tests/setup/readme-contract` to require nearby root-bootstrap prose containing
|
||||||
|
all of: OpenSSH-parsed target keys, `00-panama.conf`, all three denials, `sshd -t`,
|
||||||
|
root/target `sshd -T`, atomic installation, reload rollback, and hardening unavailable
|
||||||
|
without a key. Reject wording that says Panama merely writes the file or that reload
|
||||||
|
failure is ignored.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the README contract and confirm the old prose fails**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/readme-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: nonzero until README describes the transactional behavior.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update the documentation without claiming a live reload**
|
||||||
|
|
||||||
|
State plainly that Panama copies or verifies every target key line with OpenSSH,
|
||||||
|
offers hardening only with exact safe ownership/modes and a supported regular
|
||||||
|
drop-in, validates syntax and effective root/target policy, reloads the detected
|
||||||
|
unit, and restores the previous file with metadata on failure. State that fixture
|
||||||
|
contracts test these paths and no real daemon reload runs under `panama test --safe`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the plan gate**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n boot tests/setup/root-server-bootstrap-contract
|
||||||
|
tests/setup/root-server-bootstrap-contract
|
||||||
|
tests/setup/boot-contract
|
||||||
|
tests/setup/role-contract
|
||||||
|
tests/setup/readme-contract
|
||||||
|
./bin/panama test --safe
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 132 hermetic contracts pass after adding the new contract; non-hermetic skip counts remain unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit the synchronized documentation**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add README.md .claude/skills/panama/SKILL.md skills/panama-desktop/SKILL.md tests/setup/readme-contract
|
||||||
|
git commit -m "Docs: Explain transactional SSH hardening"
|
||||||
|
```
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
# Trusted installation inputs implementation plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Ensure every automatic executable input is publisher-signed or pinned by a reviewed SHA-256 before Panama executes or installs it.
|
||||||
|
|
||||||
|
**Architecture:** Add four small provenance helpers plus declarative reviewed pins. Keep vendor-specific decisions in `install-packages`, drive them through one hermetic public contract, and preserve existing installations when verification fails. Finish with a commit-pinned, digest-checked Panama bootstrap.
|
||||||
|
|
||||||
|
**Tech Stack:** Bash 5, GnuPG, rpmkeys with a temporary database, SHA-256, DNF5/Flatpak/Git command adapters, Panama contract runner.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md`
|
||||||
|
|
||||||
|
## Global constraints
|
||||||
|
|
||||||
|
- Never execute fetched shell, install an unverified RPM, or accept a moving `latest` response during automatic setup.
|
||||||
|
- Download without sudo into a private checked temporary directory. Verify before any sudo, extraction, execution, or target replacement.
|
||||||
|
- Preserve a known-good installed version on every verification/download failure.
|
||||||
|
- Use exact complete fingerprints and reviewed per-architecture SHA-256 values from the approved spec.
|
||||||
|
- Use `curl --connect-timeout 10 --max-time 600`; enforce each configured maximum byte count before verification.
|
||||||
|
- Parse `setup/provenance/installers.conf` as data. Do not `source`, `eval`, or shell-expand it.
|
||||||
|
- Claude Desktop repository setup is optional and never automatic.
|
||||||
|
- No task may mutate the host package database, repository configuration, Flatpak remotes, or live installed tools.
|
||||||
|
- The Terra command gets at most one disposable Fedora 44 container smoke test with no host mounts, credentials, services, or production state.
|
||||||
|
- Package 3 owns authenticated `panama update`; Package 5 owns mutable source-app and Neovim inputs.
|
||||||
|
- Preserve `/home/gib/.local/share/Panama/config/bash/.bashrc` outside this worktree.
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
- `setup/lib/artifact-provenance`: fingerprint, digest-download, detached-signature, and temporary-RPM-keyring helpers.
|
||||||
|
- `setup/provenance/installers.conf`: strict reviewed versions, URLs, SHA-256 values, fingerprints, and maximum sizes.
|
||||||
|
- `setup/provenance/keys/*`: reviewed ASCII-armored public keys.
|
||||||
|
- `setup/provenance/README.md`: source, retrieval date, verification command, and rotation notes.
|
||||||
|
- `tests/setup/package-provenance-contract`: real cryptographic fixture plus stateful curl/sudo/DNF/Flatpak/rpm command adapters.
|
||||||
|
- `tests/setup/fixtures/provenance/*`: test-only GPG key, tiny signed manifest, good/tampered artifacts, and trusted/untrusted repo data.
|
||||||
|
- `setup/scripts/install-packages`: vendor-specific verified repository/artifact flows.
|
||||||
|
- `install`: include provenance and installer behavior in the package-stage hash.
|
||||||
|
- `setup/scripts/link-vicinae-scripts`: use `npm ci` against the tracked lock.
|
||||||
|
- `boot`, `README.md`, `tests/setup/boot-contract`, `tests/setup/readme-contract`: verified initial Panama revision and boot digest.
|
||||||
|
- `tests/contracts.manifest`: one new hermetic contract.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Build and prove the provenance helpers
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `setup/lib/artifact-provenance`
|
||||||
|
- Create: `setup/provenance/installers.conf`
|
||||||
|
- Create: `setup/provenance/keys/terra44.asc`
|
||||||
|
- Create: `setup/provenance/keys/claude-code.asc`
|
||||||
|
- Create: `setup/provenance/keys/bun.asc`
|
||||||
|
- Create: `setup/provenance/keys/rpmfusion-free.asc`
|
||||||
|
- Create: `setup/provenance/keys/rpmfusion-nonfree.asc`
|
||||||
|
- Create: `setup/provenance/keys/hyprland-copr.asc`
|
||||||
|
- Create: `setup/provenance/keys/flathub.asc`
|
||||||
|
- Create: `setup/provenance/keys/claude-desktop.asc`
|
||||||
|
- Create: `setup/provenance/README.md`
|
||||||
|
- Create: `tests/setup/package-provenance-contract`
|
||||||
|
- Create: `tests/setup/fixtures/provenance/`
|
||||||
|
- Modify: `tests/contracts.manifest`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `curl`, `gpg`, `sha256sum`, `rpmkeys`, `stat`, and strict `NAME=value` provenance data.
|
||||||
|
- Produces: `load_installer_provenance FILE`, `key_fingerprint_matches FILE EXPECTED`, `download_sha256 URL EXPECTED MAX_BYTES DEST`, `verify_detached_signature KEY SIGNATURE CONTENT`, and `rpm_signature_matches PACKAGE KEY EXPECTED`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create cryptographic fixtures and write failing helper cases**
|
||||||
|
|
||||||
|
Generate a test-only key in a temporary `GNUPGHOME`, export its public key, sign a four-line `SHASUMS256.txt`, and commit only the public key, content, detached signature, a good tiny artifact, and a one-byte-tampered artifact. The private key must never enter the repository; the contract does not need it after fixture creation.
|
||||||
|
|
||||||
|
Write contract cases that source only `setup/lib/artifact-provenance` and assert:
|
||||||
|
|
||||||
|
```text
|
||||||
|
known fingerprint -> 0
|
||||||
|
wrong fingerprint -> nonzero
|
||||||
|
valid detached signature -> 0
|
||||||
|
wrong content or signature -> nonzero
|
||||||
|
matching digest/size -> atomic destination created
|
||||||
|
wrong digest, oversized body, interrupted curl -> destination absent or original bytes preserved
|
||||||
|
valid RPM signed by fixture key -> 0 through a temporary rpmdb
|
||||||
|
unsigned/wrong-key RPM -> nonzero and host rpmdb untouched
|
||||||
|
```
|
||||||
|
|
||||||
|
Add provenance-parser cases for unknown key, duplicate key, missing required key, whitespace around the name, shell expansion text, and unsupported architecture.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the new contract and confirm the missing-helper failure**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n tests/setup/package-provenance-contract
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: nonzero because the helper/config do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement exact helpers**
|
||||||
|
|
||||||
|
Use these signatures and behaviors:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
key_fingerprint_matches() {
|
||||||
|
local file="$1" expected="$2" actual
|
||||||
|
actual="$(gpg --batch --with-colons --import-options show-only --import "$file" 2>/dev/null \
|
||||||
|
| awk -F: '$1 == "fpr" { print $10; exit }')"
|
||||||
|
[[ "$actual" == "$expected" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_detached_signature() {
|
||||||
|
local key="$1" signature="$2" content="$3" home
|
||||||
|
home="$(mktemp -d)" || return 1
|
||||||
|
chmod 700 "$home"
|
||||||
|
GNUPGHOME="$home" gpg --batch --quiet --import "$key" >/dev/null 2>&1 \
|
||||||
|
&& GNUPGHOME="$home" gpg --batch --verify "$signature" "$content" >/dev/null 2>&1
|
||||||
|
local status=$?
|
||||||
|
rm -rf -- "$home"
|
||||||
|
return "$status"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`download_sha256` downloads to `DEST.part`, passes `--max-filesize MAX_BYTES`, verifies `stat -c %s <= MAX_BYTES`, compares a lowercase 64-hex digest, then `mv -f` atomically. Its EXIT/INT/TERM cleanup removes only the checked `.part` path.
|
||||||
|
|
||||||
|
The installer may define one private `download_bounded URL MAX_BYTES DEST` wrapper for publisher-signed RPMs whose trust assertion is the later RPM signature rather than a reviewed digest. It uses the same curl timeouts, `.part` cleanup, post-download size check, and atomic rename as `download_sha256`; it does not execute or install the result before `rpm_signature_matches` succeeds.
|
||||||
|
|
||||||
|
`rpm_signature_matches` creates a private temporary rpmdb, imports only `KEY`, verifies the complete expected primary fingerprint before import, and requires `rpmkeys --dbpath DB --checksig PACKAGE` success with an OpenPGP signature line. It never imports into the host keyring.
|
||||||
|
|
||||||
|
`load_installer_provenance` reads with `IFS='=' read -r name value`, accepts only an explicit name allowlist, rejects duplicate/empty values and any line without exactly one `=`, and exports nothing. Store values in one associative array named `INSTALLER_PROVENANCE`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add reviewed keys and config**
|
||||||
|
|
||||||
|
Fetch each key from the exact source URL in the spec to a temporary directory, verify its full fingerprint, and add its exact ASCII-armored content with `apply_patch`. Fill `installers.conf` with these reviewed values and conservative byte caps:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BUN_VERSION=1.4.0
|
||||||
|
BUN_X86_64_URL=https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-x64.zip
|
||||||
|
BUN_X86_64_SHA256=2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452
|
||||||
|
BUN_X86_64_MAX_BYTES=67108864
|
||||||
|
BUN_AARCH64_URL=https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-aarch64.zip
|
||||||
|
BUN_AARCH64_SHA256=4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e
|
||||||
|
BUN_AARCH64_MAX_BYTES=67108864
|
||||||
|
NODE_VERSION=24.20.0
|
||||||
|
NODE_X86_64_URL=https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-x64.tar.xz
|
||||||
|
NODE_X86_64_SHA256=2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2
|
||||||
|
NODE_X86_64_MAX_BYTES=67108864
|
||||||
|
NODE_AARCH64_URL=https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-arm64.tar.xz
|
||||||
|
NODE_AARCH64_SHA256=5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7
|
||||||
|
NODE_AARCH64_MAX_BYTES=67108864
|
||||||
|
CODEX_VERSION=0.150.1
|
||||||
|
CODEX_X86_64_URL=https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-x86_64-unknown-linux-musl.tar.gz
|
||||||
|
CODEX_X86_64_SHA256=00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17
|
||||||
|
CODEX_X86_64_MAX_BYTES=134217728
|
||||||
|
CODEX_AARCH64_URL=https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-aarch64-unknown-linux-musl.tar.gz
|
||||||
|
CODEX_AARCH64_SHA256=1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639
|
||||||
|
CODEX_AARCH64_MAX_BYTES=134217728
|
||||||
|
RUSTDESK_VERSION=1.4.9
|
||||||
|
RUSTDESK_X86_64_URL=https://github.com/rustdesk/rustdesk/releases/download/1.4.9/rustdesk-1.4.9-0.x86_64.rpm
|
||||||
|
RUSTDESK_X86_64_SHA256=eb1b053ac5b2f774f2271f7fbbfd2ea475899f7a55135c5e172bc54b9388f108
|
||||||
|
RUSTDESK_X86_64_MAX_BYTES=134217728
|
||||||
|
FEDORA_RELEASE=44
|
||||||
|
RPMFUSION_FREE_RELEASE_URL=https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-44.noarch.rpm
|
||||||
|
RPMFUSION_FREE_RELEASE_MAX_BYTES=4194304
|
||||||
|
RPMFUSION_NONFREE_RELEASE_URL=https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm
|
||||||
|
RPMFUSION_NONFREE_RELEASE_MAX_BYTES=4194304
|
||||||
|
TERRA_BASEURL=https://repos.fyralabs.com/terra44
|
||||||
|
HYPRLAND_COPR_BASEURL=https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/
|
||||||
|
FLATHUB_DESCRIPTOR_URL=https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
FLATHUB_DESCRIPTOR_MAX_BYTES=1048576
|
||||||
|
CLAUDE_CODE_BASEURL=https://downloads.claude.ai/claude-code/rpm/stable
|
||||||
|
CLAUDE_DESKTOP_BASEURL=https://patrickjaja.github.io/claude-desktop-extra/rpm/
|
||||||
|
TERRA_FINGERPRINT=AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
||||||
|
CLAUDE_CODE_FINGERPRINT=31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE
|
||||||
|
BUN_FINGERPRINT=F3DCC08A8572C0749B3E18888EAB4D40A7B22B59
|
||||||
|
RPMFUSION_FREE_FINGERPRINT=E9A491A3DE247814E7E067EAE06F8ECDD651FF2E
|
||||||
|
RPMFUSION_NONFREE_FINGERPRINT=79BDB88F9BBF73910FD4095B6A2AF96194843C65
|
||||||
|
HYPRLAND_COPR_FINGERPRINT=97E23476C89635135407C7D5E9BA41342C4B2995
|
||||||
|
FLATHUB_FINGERPRINT=6E5C05D979C76DAF93C081354184DD4D907A7CAE
|
||||||
|
CLAUDE_DESKTOP_FINGERPRINT=825A7D15D78BABE45646D5DF382409F597908867
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the exact hashes and fingerprints from the spec; no value may be resolved through `latest`. The provenance README must list every source URL and the command used to verify it on 2026-08-27.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add the manifest entry and run the focused gate**
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Provenance uses local signed fixtures and stubs every network/package operation.
|
||||||
|
hermetic tests/setup/package-provenance-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n setup/lib/artifact-provenance tests/setup/package-provenance-contract
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
tests/setup/contract-manifest-contract
|
||||||
|
./bin/panama test --safe package-provenance
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pass and no host GPG/RPM state changes.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the provenance foundation**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add setup/lib/artifact-provenance setup/provenance tests/setup/fixtures/provenance \
|
||||||
|
tests/setup/package-provenance-contract tests/contracts.manifest
|
||||||
|
git commit -m "Test: Add installer provenance boundary"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Verify third-party repository roots
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `setup/scripts/install-packages:218-276,339-370,396-456`
|
||||||
|
- Modify: `tests/setup/package-provenance-contract`
|
||||||
|
- Test: `tests/setup/desktop-first-contract`
|
||||||
|
- Test: `tests/setup/package-lists-contract`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `INSTALLER_PROVENANCE`, the four helper functions, vendored keys, and command adapters.
|
||||||
|
- Produces: `install_rpmfusion_repositories`, `install_terra_repository`, `configure_hyprland_repository`, `ensure_flathub_remote`, `install_claude_code`, and `install_claude_desktop_if_trusted`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add public installer cases for every repository**
|
||||||
|
|
||||||
|
Run a fixture copy of `install-packages` with temporary HOME/state and stubbed `sudo`, `dnf`, `rpm`, `rpmkeys`, `curl`, `flatpak`, and `gpg`. Assert exact command-log order and policy:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RPM Fusion: exact Fedora 44 URL -> size cap -> RPM signature -> localpkg_gpgcheck=1 install
|
||||||
|
Terra: exact F44 key -> pkg_gpgcheck=1 -> repo_gpgcheck=1 -> local gpgkey -> terra-release
|
||||||
|
COPR: exact baseurl/local key, package gpgcheck=1, explicit metadata-signature exception; no `dnf copr enable`
|
||||||
|
Flathub: decoded embedded key fingerprint and GPG-enabled remote; mismatch preserves existing remote
|
||||||
|
Claude Code: exact Anthropic key/repo checks before DNF
|
||||||
|
Claude Desktop absent/untrusted: one manual message, no download, no DNF, overall success
|
||||||
|
Claude Desktop trusted existing repo: DNF install only
|
||||||
|
```
|
||||||
|
|
||||||
|
Inject wrong keys, wrong base URLs, GPG flags off, signature failure, and DNF failure. Assert nothing downstream in the dependent transaction runs after a trust-root failure. Require `rpm -E %fedora` to equal the reviewed `FEDORA_RELEASE`; any other release fails before a third-party download or repository mutation.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the contract and confirm current unsafe paths fail**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: nonzero findings for `--nogpgcheck`, TOFU COPR, unvalidated Flathub, remote-script Claude Desktop, and unverified RPM Fusion URLs.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement signed repository setup**
|
||||||
|
|
||||||
|
Source the helper and load the config from `PANAMA_PATH` at installer start. Download RPM Fusion release RPMs, verify signatures with the matching vendored key, then call:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf install -y --setopt=localpkg_gpgcheck=1 "$free_rpm" "$nonfree_rpm"
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace Terra with `--repofrompath terra,https://repos.fyralabs.com/terra44` plus:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--setopt=terra.pkg_gpgcheck=1
|
||||||
|
--setopt=terra.repo_gpgcheck=1
|
||||||
|
--setopt=terra.gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama
|
||||||
|
```
|
||||||
|
|
||||||
|
Stage repo/key files completely before atomic sudo install. Write the COPR repo from reviewed local values rather than `dnf copr enable`, with:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[panama-hyprland]
|
||||||
|
name=Panama reviewed Hyprland COPR
|
||||||
|
baseurl=https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/
|
||||||
|
enabled=1
|
||||||
|
gpgcheck=1
|
||||||
|
repo_gpgcheck=0
|
||||||
|
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland
|
||||||
|
```
|
||||||
|
|
||||||
|
The `repo_gpgcheck=0` line is the single audited exception: the publisher returns no `repodata/repomd.xml.asc`, while its RPMs are signed by the pinned project key. The contract rejects this exception for every other repository and still requires package signatures.
|
||||||
|
|
||||||
|
Parse the Flathub descriptor as INI data, base64-decode `GPGKey`, verify its fingerprint, and reject `NoGPGVerify=true` or equivalent disabled state. Write Claude Code's stable repository with the vendored local key, `gpgcheck=1`, and `repo_gpgcheck=1`; its publisher provides signed metadata.
|
||||||
|
|
||||||
|
For Claude Desktop, inspect only an already configured repo. Trust it only when its base URL equals `https://patrickjaja.github.io/claude-desktop-extra/rpm/`, both GPG checks are `1`, and its `gpgkey` is an existing local file whose complete fingerprint matches the vendored `claude-desktop.asc`. Never curl or run the community setup script. The host's current remote-key configuration is therefore treated as untrusted until an operator configures a local reviewed key. Untrusted/absent configuration logs an optional manual instruction and returns success.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify ordering and regression contracts**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n setup/scripts/install-packages tests/setup/package-provenance-contract
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
tests/setup/desktop-first-contract
|
||||||
|
tests/setup/package-lists-contract
|
||||||
|
./bin/panama test --safe package-provenance
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit repository trust roots**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add setup/scripts/install-packages tests/setup/package-provenance-contract
|
||||||
|
git commit -m "Fix: Verify third-party package repositories"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Pin language runtimes and agent tools
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `setup/scripts/install-packages:100-177,334-394`
|
||||||
|
- Modify: `tests/setup/package-provenance-contract`
|
||||||
|
- Modify: `tests/quickshell/declared-dependencies-contract`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `download_sha256`, loaded reviewed pins, supported `uname -m` values `x86_64` and `aarch64`.
|
||||||
|
- Produces: `install_node`, `install_bun`, `install_claude_code`, `install_codex`, and `install_rustdesk` with verified staging and known-good preservation.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing per-architecture and preservation cases**
|
||||||
|
|
||||||
|
For each artifact, assert exact URL/digest selection for x86_64 and aarch64, unsupported-architecture refusal before curl, digest mismatch preserving a seeded old version, interrupted download cleanup, atomic replacement, and already-installed exact-version no-op.
|
||||||
|
|
||||||
|
Require source scans and public command logs to reject:
|
||||||
|
|
||||||
|
```text
|
||||||
|
curl ... | bash
|
||||||
|
nvm install --lts
|
||||||
|
npm install -g pnpm
|
||||||
|
npm install -g @openai/codex
|
||||||
|
releases/latest
|
||||||
|
api.github.com/.../releases/latest
|
||||||
|
```
|
||||||
|
|
||||||
|
RustDesk supports only the reviewed x86_64 RPM. aarch64 records a deliberate soft failure without downloading. pnpm must come from signed Fedora DNF; no network-script/npm fallback is allowed.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused contract and confirm it fails on moving inputs**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: nonzero findings naming each current moving or piped installer.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement verified atomic installs**
|
||||||
|
|
||||||
|
Map architecture once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64) artifact_arch=X86_64 ;;
|
||||||
|
aarch64) artifact_arch=AARCH64 ;;
|
||||||
|
*) log "Unsupported architecture: $(uname -m)"; return 1 ;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
Install Node 24.20.0 into `$NVM_DIR/versions/node/v24.20.0` from a sibling staging directory, reject archive entries outside the expected single top-level directory, require staged `bin/node --version` to print `v24.20.0`, then rename and set nvm's default alias to `24.20.0` without `nvm install`.
|
||||||
|
|
||||||
|
For Bun, reject unexpected archive paths, stage the binary at `$HOME/.bun/versions/1.4.0/bin/bun`, require `--version` to print `1.4.0`, then atomically replace a temporary symlink at `$HOME/.bun/bin/bun`. For Codex, reject absolute/parent-traversal tar members, stage the release's `codex` binary at `$HOME/.local/lib/panama/codex/0.150.1/codex`, require `--version` to identify `0.150.1`, then atomically replace `$HOME/.local/bin/codex` through a temporary symlink. Existing version directories must match the reviewed binary/version or cause a soft failure; never delete and recreate an unverified collision.
|
||||||
|
|
||||||
|
Install Claude Code through the signed stable repository from Task 2. Download RustDesk's versioned RPM, verify SHA-256, then pass only that local path to DNF. Install pnpm through the signed Fedora package transaction and record a soft failure if unavailable. Every archive extracts into a checked private directory first; no archive writes directly into its final prefix.
|
||||||
|
|
||||||
|
Every helper failure appends the same component name to `softly_failed`; `report_soft_failures` keeps the package hash unstamped.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run focused and dependency checks**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n setup/scripts/install-packages tests/setup/package-provenance-contract
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
tests/quickshell/declared-dependencies-contract
|
||||||
|
tests/setup/desktop-first-contract
|
||||||
|
./bin/panama test --safe package-provenance
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit pinned user tools**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add setup/scripts/install-packages tests/setup/package-provenance-contract \
|
||||||
|
tests/quickshell/declared-dependencies-contract
|
||||||
|
git commit -m "Fix: Pin runtime and agent artifacts"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Invalidate stale installer state and lock npm installs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `install:67-106`
|
||||||
|
- Modify: `setup/scripts/link-vicinae-scripts:79-97`
|
||||||
|
- Modify: `tests/setup/launcher-search-contract`
|
||||||
|
- Modify: `tests/setup/update-command-contract`
|
||||||
|
- Modify: `tests/setup/package-provenance-contract`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: tracked package lists, installer, provenance helper/config/keys, and extension lockfile.
|
||||||
|
- Produces: `hash_packages` covering every installer trust input and Vicinae `npm ci` behavior.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing hash and lockfile assertions**
|
||||||
|
|
||||||
|
Run `hash_packages` from a disposable installer copy and assert the digest changes independently when each of these changes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
setup/packages/core-packages
|
||||||
|
setup/scripts/install-packages
|
||||||
|
setup/lib/artifact-provenance
|
||||||
|
setup/provenance/installers.conf
|
||||||
|
one setup/provenance/keys file
|
||||||
|
```
|
||||||
|
|
||||||
|
In the Vicinae fixture, stub npm and require argv `ci`, not `install`. Seed a lock mismatch and assert nonzero extension-build status with the lockfile byte-for-byte unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Confirm current hash and npm behavior fail**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
tests/setup/launcher-search-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: current hash ignores installer/provenance changes and extension setup invokes `npm install`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Hash exact inputs and switch to `npm ci`**
|
||||||
|
|
||||||
|
Replace the current `find ... -maxdepth 1` stream with a sorted NUL-safe list containing top-level package files, `setup/scripts/install-packages`, `setup/lib/artifact-provenance`, and every regular file under `setup/provenance`. Hash file paths plus contents so renames change the digest.
|
||||||
|
|
||||||
|
Change only the extension dependency command to `npm ci`; do not add lockfile repair or update behavior.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify upgrade and extension behavior**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n install setup/scripts/link-vicinae-scripts
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
tests/setup/launcher-search-contract
|
||||||
|
tests/setup/update-command-contract
|
||||||
|
./bin/panama test --safe package-provenance
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit state invalidation and npm locking**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add install setup/scripts/link-vicinae-scripts tests/setup/launcher-search-contract \
|
||||||
|
tests/setup/update-command-contract tests/setup/package-provenance-contract
|
||||||
|
git commit -m "Fix: Re-run verified installer inputs"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Verify the initial Panama revision before handoff
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `boot:20-166`
|
||||||
|
- Modify: `tests/setup/boot-contract`
|
||||||
|
- Modify: `README.md:1-48`
|
||||||
|
- Modify: `tests/setup/readme-contract`
|
||||||
|
- Modify: `.claude/skills/panama/SKILL.md`
|
||||||
|
- Modify: `skills/panama-desktop/SKILL.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `PANAMA_BOOT_REVISION` as a full lowercase 40-hex commit and `PANAMA_BOOT_SHA256` as a lowercase 64-hex digest.
|
||||||
|
- Produces: verified fresh clone at that revision, fast-forward-only clean existing checkout, and documentation pinned to the implementation commit immediately preceding its documentation commit.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace old permissive boot-contract expectations with red trust cases**
|
||||||
|
|
||||||
|
The public fixture must assert:
|
||||||
|
|
||||||
|
```text
|
||||||
|
missing/malformed revision -> no git clone/fetch and no install
|
||||||
|
fresh clone -> fetch exact revision, resolve HEAD^{commit}, equality, handoff
|
||||||
|
HEAD mismatch -> nonzero, no install
|
||||||
|
existing clean ancestor -> fast-forward to exact revision, then install
|
||||||
|
existing dirty or divergent checkout -> nonzero, no reset, no install
|
||||||
|
fetch failure -> nonzero, no install
|
||||||
|
```
|
||||||
|
|
||||||
|
Delete the old assertion that a failed pull proceeds with the checkout as-is. Add README assertions rejecting `bash <(curl .../main/boot)` and requiring a commit URL, 40-hex revision, 64-hex SHA-256, `sha256sum -c`, 10-second connect timeout, 30-second total timeout, and 256 KiB maximum.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run boot and README contracts to prove they fail**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/boot-contract
|
||||||
|
tests/setup/readme-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both fail on the mutable branch bootstrap and permissive pull fallback.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement exact-revision clone/handoff and commit it**
|
||||||
|
|
||||||
|
Validate inputs before Git:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[[ "${PANAMA_BOOT_REVISION:-}" =~ ^[0-9a-f]{40}$ ]] || exit 1
|
||||||
|
[[ "${PANAMA_BOOT_SHA256:-}" =~ ^[0-9a-f]{64}$ ]] || exit 1
|
||||||
|
actual_boot_sha="$(sha256sum "${BASH_SOURCE[0]}" | cut -d' ' -f1)"
|
||||||
|
[[ "$actual_boot_sha" == "$PANAMA_BOOT_SHA256" ]] || exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
For a fresh destination, initialize/fetch the exact commit, verify `git rev-parse HEAD^{commit}` equality, create local `main` at that commit, and set `branch.main.remote=origin` plus `branch.main.merge=refs/heads/main`. For an existing checkout, require empty `git status --porcelain`, fetch the exact commit, require `git merge-base --is-ancestor HEAD REVISION`, and fast-forward only. Never use reset or execute after mismatch/failure.
|
||||||
|
|
||||||
|
Run focused tests, then commit only implementation and contract changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n boot tests/setup/boot-contract
|
||||||
|
tests/setup/boot-contract
|
||||||
|
tests/setup/root-server-bootstrap-contract
|
||||||
|
git add boot tests/setup/boot-contract tests/setup/root-server-bootstrap-contract
|
||||||
|
git commit -m "Fix: Verify the initial Panama revision"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Compute the committed boot pin and write the documented command**
|
||||||
|
|
||||||
|
Use the implementation commit just created:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bootstrap_commit="$(git rev-parse HEAD)"
|
||||||
|
bootstrap_sha="$(git show "$bootstrap_commit:boot" | sha256sum | cut -d' ' -f1)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Write README commands that download
|
||||||
|
`https://git.gbrown.org/gib/Panama/raw/commit/$bootstrap_commit/boot` to a checked temporary file with `curl --connect-timeout 10 --max-time 30 --max-filesize 262144`, compare `$bootstrap_sha` through `sha256sum -c`, then invoke with both environment values. Use the same verified command for desktop and `--server`; never pipe the response to Bash.
|
||||||
|
|
||||||
|
The README contract must parse those literal values, run `git cat-file -e COMMIT^{commit}`, and require:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test "$(git show "$commit:boot" | sha256sum | cut -d' ' -f1)" = "$documented_sha"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Update operator skills and run the complete installer plan gate**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n boot install setup/scripts/install-packages setup/scripts/link-vicinae-scripts \
|
||||||
|
setup/lib/artifact-provenance tests/setup/package-provenance-contract \
|
||||||
|
tests/setup/boot-contract
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
tests/setup/boot-contract
|
||||||
|
tests/setup/root-server-bootstrap-contract
|
||||||
|
tests/setup/readme-contract
|
||||||
|
tests/setup/package-lists-contract
|
||||||
|
tests/setup/desktop-first-contract
|
||||||
|
tests/setup/launcher-search-contract
|
||||||
|
tests/setup/update-command-contract
|
||||||
|
./bin/panama test --safe
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 133 hermetic contracts pass after the SSH and provenance contracts exist; non-hermetic skip counts remain unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the pinned documentation**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add README.md tests/setup/readme-contract .claude/skills/panama/SKILL.md \
|
||||||
|
skills/panama-desktop/SKILL.md setup/provenance/README.md
|
||||||
|
git commit -m "Docs: Pin the verified Panama bootstrap"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Smoke-test signed Terra bootstrap in a disposable Fedora 44 container
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `setup/provenance/README.md`
|
||||||
|
- Modify: `tests/setup/package-provenance-contract` only if the smoke test exposes a fixture gap
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: exact Terra key/repo command landed in Task 2.
|
||||||
|
- Produces: recorded disposable proof or a fail-closed Terra-unavailable implementation; never host installation.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Preflight the disposable target**
|
||||||
|
|
||||||
|
Require rootless Podman, no bind mounts, a fresh `registry.fedoraproject.org/fedora:44` container, and no forwarded credentials or host sockets. The command may download repository metadata and the `terra-release` package only inside the disposable container.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the exact key and DNF verification path**
|
||||||
|
|
||||||
|
Copy only the vendored Terra key into the container, verify its full fingerprint, then run the exact `--repofrompath` and three `terra.*` GPG settings from Task 2. Query the resulting repo file and package signature settings. Remove the container on exit.
|
||||||
|
|
||||||
|
Expected: DNF installs `terra-release` with both package and metadata verification enabled and without `--nogpgcheck`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Apply the fail-closed result**
|
||||||
|
|
||||||
|
If the exact command fails, do not weaken GPG settings. Change the installer to print Terra unavailable and exit before initial/desktop/Hyprland transactions; update the fixture expectation to that branch. If it succeeds, make no production change.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Record proof and rerun the hermetic gate**
|
||||||
|
|
||||||
|
Document the container image, date, exact command, exit status, key fingerprint, and inspected repo settings in `setup/provenance/README.md`. Do not claim host installation.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
./bin/panama test --safe
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit the provenance proof**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add setup/provenance/README.md setup/scripts/install-packages \
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
git commit -m "Docs: Record signed Terra bootstrap proof"
|
||||||
|
```
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
# Panama repository audit remediation
|
||||||
|
|
||||||
|
Approved direction (2026-08-26): repair the complete repository audit in staged,
|
||||||
|
reviewable commits on one branch. Safety and test trust come first. No repair may
|
||||||
|
touch a real server, change a live firewall, or run a desktop-takeover check without
|
||||||
|
Gabriel explicitly approving that runtime step.
|
||||||
|
|
||||||
|
## Why this is one program
|
||||||
|
|
||||||
|
The audit found defects in four systems that depend on each other:
|
||||||
|
|
||||||
|
- `boot`, `install`, and `panama update` decide which code runs with root access.
|
||||||
|
- The server catalog and updater decide which network services run unattended.
|
||||||
|
- Quickshell owns capture, dictation, privacy state, and other long-lived desktop work.
|
||||||
|
- The contract runner is the proof for all three, but its safe classification and two
|
||||||
|
contracts are currently wrong.
|
||||||
|
|
||||||
|
Fixing a product defect while the gate is false-green or incorrectly labelled safe
|
||||||
|
would replace one uncertainty with another. This design therefore restores the gate
|
||||||
|
first, then fixes privileged and unattended paths, then the desktop.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
| Decision | Choice |
|
||||||
|
|---|---|
|
||||||
|
| Delivery | One remediation branch with a small commit per behavior |
|
||||||
|
| Order | Test trust, bootstrap, updates and server lifecycle, desktop, remaining hardening |
|
||||||
|
| Test style | Behavioral fixtures at public command and QML interfaces |
|
||||||
|
| Live desktop | Excluded from the default safe gate; explicit capability and approval required |
|
||||||
|
| Live server | Never used during implementation; Podman, systemd, SSH, and firewall are stubbed |
|
||||||
|
| Production | Out of scope; repository changes only |
|
||||||
|
| Visual design | Unchanged; no mocks are needed because no layout or copy redesign is planned |
|
||||||
|
| Compatibility | Existing command names and normal successful flows remain intact |
|
||||||
|
| Failure policy | Privileged or destructive uncertainty fails closed with a recovery instruction |
|
||||||
|
|
||||||
|
## Program structure
|
||||||
|
|
||||||
|
The work is five repair packages. Each package has its own tests, implementation,
|
||||||
|
review, and commit sequence. A package may use a helper introduced by an earlier one,
|
||||||
|
but it may not reach forward into unfinished work.
|
||||||
|
|
||||||
|
### Package 1: make the verification gate trustworthy
|
||||||
|
|
||||||
|
#### Explicit contract capabilities
|
||||||
|
|
||||||
|
`tests/contracts.manifest` becomes the source of truth for contract execution.
|
||||||
|
Every discovered executable contract has exactly one entry. Its format is
|
||||||
|
`<capability>[,<capability>...] <repo-relative-path>`, with `#` for whole-line
|
||||||
|
comments:
|
||||||
|
|
||||||
|
```text
|
||||||
|
hermetic tests/setup/boot-contract
|
||||||
|
live-host tests/quickshell/updates-contract
|
||||||
|
live-compositor tests/hypr/keybind-categories-contract
|
||||||
|
live-desktop tests/quickshell/settings-pages-contract
|
||||||
|
network tests/quickshell/home-assistant-helper-contract
|
||||||
|
privileged tests/setup/root-server-bootstrap-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
The vocabulary is deliberately small:
|
||||||
|
|
||||||
|
- `hermetic`: temporary state and stubbed system commands only.
|
||||||
|
- `live-host`: reads the real machine or session without changing it or contacting a non-fixture endpoint.
|
||||||
|
- `live-compositor`: reads or reloads the running compositor, without moving windows.
|
||||||
|
- `live-desktop`: maps surfaces, moves focus or windows, or changes desktop state.
|
||||||
|
- `network`: contacts a non-fixture network endpoint.
|
||||||
|
- `privileged`: needs root or changes system configuration.
|
||||||
|
|
||||||
|
Capabilities may be combined. `panama test --safe` runs only `hermetic` contracts.
|
||||||
|
`hermetic` is exclusive and cannot be combined with another capability. Plain
|
||||||
|
`panama test` keeps its current all-contract meaning, but prints the capabilities
|
||||||
|
before each non-hermetic contract and requires a TTY confirmation. Automation must
|
||||||
|
grant each needed capability explicitly with repeatable flags such as
|
||||||
|
`--allow live-host --allow live-compositor`; there is no grant that means "anything."
|
||||||
|
Pattern selection does not bypass this rule.
|
||||||
|
|
||||||
|
The existing `tests/desktop-hijacking` file is retired after its explanations move
|
||||||
|
beside the matching manifest entries as comments. A contract fails the manifest gate
|
||||||
|
when a discovered path is missing, a manifest path is stale, a path is duplicated, or
|
||||||
|
an unknown capability appears. The runner fails closed rather than inferring safety
|
||||||
|
from source patterns.
|
||||||
|
|
||||||
|
#### Runner behavior
|
||||||
|
|
||||||
|
Each contract gets a configurable outer timeout. The default is 180 seconds and
|
||||||
|
`PANAMA_TEST_TIMEOUT_SECONDS` may change it for local diagnosis. The runner captures
|
||||||
|
stdout and stderr separately. On failure it prints both. On success it prints a
|
||||||
|
warning and the captured stderr rather than discarding it.
|
||||||
|
|
||||||
|
The runner creates its capture directory with `mktemp -d`, removes it on exit, and
|
||||||
|
reports a timeout as a normal failed contract. One stuck QML or IPC process cannot
|
||||||
|
block the remaining suite forever.
|
||||||
|
|
||||||
|
#### Current red and false-green contracts
|
||||||
|
|
||||||
|
- `agent-usage-contract` generates its session directory and timestamps from the
|
||||||
|
current local day. Production gains no test-only clock interface.
|
||||||
|
- `compose-secrets-contract` parses YAML and dotenv-shaped files as structured data.
|
||||||
|
Markdown is scanned only for private-key and known token signatures, not prose that
|
||||||
|
resembles an assignment.
|
||||||
|
- `update-command-contract` runs the real `panama update` command against a temporary
|
||||||
|
local bare remote, clone, temporary state, and stubbed installer/system commands.
|
||||||
|
Parser failures and fixture command failures must propagate. Successful stderr stays
|
||||||
|
visible through the runner.
|
||||||
|
- Contracts that currently write a probe into tracked or live-linked paths move the
|
||||||
|
probe into a complete temporary copy. Any unavoidable temporary mutation restores
|
||||||
|
from the EXIT trap, including interruption.
|
||||||
|
|
||||||
|
#### Test interfaces
|
||||||
|
|
||||||
|
- Contract discovery and execution: `panama test [--safe] [pattern]`.
|
||||||
|
- Update behavior: the real `panama update` command in disposable Git repositories.
|
||||||
|
- Secret policy: parsed Compose/env data plus explicit token fixtures.
|
||||||
|
|
||||||
|
Tests do not assert that a source file contains a command when they can execute the
|
||||||
|
public command against stubs and inspect the outcome.
|
||||||
|
|
||||||
|
### Package 2: secure bootstrap and privileged installation
|
||||||
|
|
||||||
|
#### SSH hardening transaction
|
||||||
|
|
||||||
|
Root server bootstrap may harden SSH only when all of these are true:
|
||||||
|
|
||||||
|
1. The target account has a nonempty regular `authorized_keys` file.
|
||||||
|
2. The `.ssh` directory and file are owned by the target user and have safe modes.
|
||||||
|
3. A candidate Panama drop-in passes `sshd -t` as part of the complete active config.
|
||||||
|
4. Reloading the detected SSH unit succeeds.
|
||||||
|
|
||||||
|
The drop-in is written to a same-directory temporary file and installed atomically.
|
||||||
|
If validation or reload fails, Panama restores the previous drop-in, validates the
|
||||||
|
restored configuration, and leaves root/password access unchanged. If the user has no
|
||||||
|
verified key, hardening is unavailable rather than merely defaulting to no.
|
||||||
|
|
||||||
|
The public interface stays `boot --server`. A new root-bootstrap contract supplies
|
||||||
|
stubbed `id`, `passwd`, `sshd`, `systemctl`, account data, and filesystem state. It
|
||||||
|
proves the no-key refusal, successful transaction, invalid-config rollback, and failed-
|
||||||
|
reload rollback.
|
||||||
|
|
||||||
|
#### Trusted installation inputs
|
||||||
|
|
||||||
|
No network response may be executed as root unless Panama verifies an immutable digest
|
||||||
|
or package signature first.
|
||||||
|
|
||||||
|
- Terra installation no longer uses `--nogpgcheck`. Before implementation, the plan
|
||||||
|
records the current official repository instructions, signing-key fingerprint, and
|
||||||
|
verification command in a provenance fixture. The contract pins that fingerprint.
|
||||||
|
If no verifiable path exists, the installer reports Terra unavailable and stops
|
||||||
|
before the desktop package transaction that depends on it.
|
||||||
|
- Claude Desktop's unpinned GitHub Pages installer is removed from the automatic path.
|
||||||
|
Panama may install the package when a trusted repository is already configured. It
|
||||||
|
otherwise reports a manual optional step and continues without Claude Desktop.
|
||||||
|
- Existing remote installers for Bun, Claude Code, and similar tools are audited under
|
||||||
|
the same rule. A tool that cannot offer a pinned or signed installation path moves to
|
||||||
|
an explicit optional action rather than remaining in the unattended base install.
|
||||||
|
|
||||||
|
Package contracts test artifact provenance and failure behavior with local fixtures.
|
||||||
|
They never contact the real repositories.
|
||||||
|
|
||||||
|
#### Server firewall transaction
|
||||||
|
|
||||||
|
Server setup establishes the policy it documents:
|
||||||
|
|
||||||
|
- firewalld must be installed, enabled, and active, or server setup fails.
|
||||||
|
- Ports 80 and 443 accept public traffic only from validated Cloudflare IPv4 and IPv6
|
||||||
|
ranges held in dedicated firewalld ipsets.
|
||||||
|
- Port 81 is assigned only to the WireGuard interface or zone.
|
||||||
|
- No service-specific port is opened automatically.
|
||||||
|
- Updating Cloudflare ranges validates every CIDR before replacing the last known good
|
||||||
|
ipsets. An empty or malformed download changes nothing.
|
||||||
|
- The complete permanent configuration is applied and reloaded as one recoverable
|
||||||
|
transaction. A failure restores the previous Panama-owned rules.
|
||||||
|
|
||||||
|
The repository carries the policy and updater, not a claim that an external cloud
|
||||||
|
firewall happens to compensate. Fixture tests cover inactive firewalld, zone selection,
|
||||||
|
bad CIDRs, rollback, and repeated setup.
|
||||||
|
|
||||||
|
### Package 3: make updates, migrations, and server lifecycle recoverable
|
||||||
|
|
||||||
|
#### Clean-revision machine updates
|
||||||
|
|
||||||
|
`panama update` keeps local work stashed until the pulled checkout has completed
|
||||||
|
`install --upgrade` and migrations. It records the exact stash object it created and
|
||||||
|
does not assume `stash@{0}` still names it.
|
||||||
|
|
||||||
|
An EXIT/INT/TERM recovery handler restores that stash only when the pulled update has
|
||||||
|
finished and the worktree is clean. If restoration conflicts or the tree is not clean,
|
||||||
|
the handler leaves the exact stash untouched and prints its object ID and recovery
|
||||||
|
command. A next-run check also reports an unfinished Panama update, which covers SIGKILL
|
||||||
|
and power loss that no trap can catch.
|
||||||
|
|
||||||
|
Installer edits restored after the trusted update are not executed during that run.
|
||||||
|
The update contract proves dirty dotfiles, dirty installer code, pull failure, installer
|
||||||
|
failure, interruption, successful restore, and conflicting restore.
|
||||||
|
|
||||||
|
A failed or unavailable pull remains visible in the final status and makes `panama
|
||||||
|
update` return nonzero after safe local repairs finish. Interactive updates preflight
|
||||||
|
administrator authentication once. A noninteractive update uses `sudo -n` and fails
|
||||||
|
immediately with a clear instruction instead of waiting on a hidden prompt. Documentation
|
||||||
|
states that routine updates ask no Panama questions but may require administrator
|
||||||
|
authentication.
|
||||||
|
|
||||||
|
#### Exact installer state
|
||||||
|
|
||||||
|
Before disabling idle behavior, `install` records whether each relevant gsettings key
|
||||||
|
exists and its exact serialized value. The EXIT handler restores only keys it changed,
|
||||||
|
using those exact values. A missing schema remains untouched.
|
||||||
|
|
||||||
|
Every declared stage is required. A missing or non-executable stage is recorded as a
|
||||||
|
failed stage and makes the final result nonzero.
|
||||||
|
|
||||||
|
Automatic migration baselining is removed. Shipped migrations are self-guarding and run
|
||||||
|
on both fresh and existing machines. `--baseline` remains an explicit development/admin
|
||||||
|
command but no install path infers freshness from an absent state directory.
|
||||||
|
|
||||||
|
The secret relocation migration exits nonzero while both the old and new secret files
|
||||||
|
exist. It tightens the old file to mode `0600`, prints the two exact paths and required
|
||||||
|
manual reconciliation, and retries on the next migration run. It never marks unresolved
|
||||||
|
secret state complete.
|
||||||
|
|
||||||
|
#### Collision-proof displacement
|
||||||
|
|
||||||
|
A shared shell module owns backup displacement for installer, user-content, skills, and
|
||||||
|
server-definition paths. Its small interface accepts a source path and a backup category.
|
||||||
|
It guarantees:
|
||||||
|
|
||||||
|
- Panama-owned symlinks are replaced without backup.
|
||||||
|
- Foreign symlinks are moved, never discarded.
|
||||||
|
- Existing backups are never overwritten.
|
||||||
|
- The destination is unique without relying on second-resolution timestamps alone.
|
||||||
|
- A failed move leaves the original object in place and returns nonzero.
|
||||||
|
|
||||||
|
Callers no longer invent `.bak` or `.pre-panama` names independently. Behavioral tests
|
||||||
|
exercise repeated runs, foreign symlinks, collisions, and failure paths through the
|
||||||
|
public stage commands.
|
||||||
|
|
||||||
|
#### Server catalog containment and portability
|
||||||
|
|
||||||
|
`panama server` accepts only an exact service name returned by the catalog. Separators,
|
||||||
|
dot components, and aliases are rejected before any target path is created.
|
||||||
|
|
||||||
|
Compose definitions use `${HOME}` and `${XDG_RUNTIME_DIR}` rather than `/home/gib` and
|
||||||
|
`/run/user/1000`. `RequiresMountsFor` is removed where unused and uses `%h` only for a
|
||||||
|
service that truly needs the media mount. Existing `.env` files must be regular,
|
||||||
|
non-symlink files and are corrected to mode `0600` on every enable.
|
||||||
|
|
||||||
|
The nightly update unit executes a stable command linked under `~/.local/bin`, not a
|
||||||
|
hard-coded checkout path. Setup creates that link from the resolved `PANAMA_PATH`, so a
|
||||||
|
supported custom checkout receives the same timer behavior as the default location.
|
||||||
|
|
||||||
|
`disable` stops the unit, verifies the compose containers are stopped, and only then
|
||||||
|
unlinks the unit. Failure leaves the management link installed and reports the commands
|
||||||
|
needed for diagnosis.
|
||||||
|
|
||||||
|
Definition tracking separates `seen` from `applied` hashes. Relinking may update `seen`,
|
||||||
|
but only a verified successful restart updates `applied`, so an unapplied definition
|
||||||
|
keeps warning.
|
||||||
|
|
||||||
|
#### Safe unattended container updates
|
||||||
|
|
||||||
|
The updater takes a nonblocking `flock` for the complete run. It gets resolved images
|
||||||
|
from `podman compose config --images`; dotenv files are never sourced or evaluated as
|
||||||
|
shell code.
|
||||||
|
|
||||||
|
Before pulling, the updater records every running container's image ID and tags enough
|
||||||
|
rollback references to recreate the old project. After `compose up -d`, it waits with a
|
||||||
|
bounded timeout until every expected container is running and each declared healthcheck
|
||||||
|
is healthy. A service without healthchecks still has to remain running for the complete
|
||||||
|
stability window.
|
||||||
|
|
||||||
|
On failure, it restores the old image references, recreates the project, verifies the
|
||||||
|
rollback, records the service as failed, and keeps the rollback images. Image pruning
|
||||||
|
runs only after every changed service passed verification. A rollback failure is called
|
||||||
|
out separately and keeps all images.
|
||||||
|
|
||||||
|
The updater's public interface and systemd timer stay the same. A fixture contract stubs
|
||||||
|
`podman`, `podman compose`, and `systemctl` to prove pull failure, unhealthy startup,
|
||||||
|
crash after startup, successful update, successful rollback, failed rollback, frozen and
|
||||||
|
stopped services, concurrency, and hostile dotenv values.
|
||||||
|
|
||||||
|
#### Container privilege and secret policy
|
||||||
|
|
||||||
|
Socket consumers are split by what they need:
|
||||||
|
|
||||||
|
- Read-only monitoring uses an allowlisted socket proxy. A read-only bind mount is not
|
||||||
|
treated as an API permission boundary.
|
||||||
|
- Portainer retains administrative control only when pinned to an immutable image,
|
||||||
|
bound to WireGuard, excluded from automatic updates, and explicitly enabled as a
|
||||||
|
trusted administrator.
|
||||||
|
- Spoon's job worker runs under a dedicated Unix account and rootless Podman runtime so
|
||||||
|
a job cannot mount the main server account's SSH keys, service data, or `.env` files.
|
||||||
|
- Services that do not require the Podman API lose the socket mount.
|
||||||
|
|
||||||
|
Every service receives only the environment variables it needs. Redis, MinIO, and other
|
||||||
|
sidecars do not inherit a stack-wide `.env`. The catalog contract inventories socket
|
||||||
|
mounts, floating tags, `label:disable`, `seccomp:unconfined`, published ports, and broad
|
||||||
|
`env_file` use. Intentional exceptions name their risk and update policy in a machine-
|
||||||
|
checked manifest.
|
||||||
|
|
||||||
|
Required SMTP and mail settings live in each service's `.env.example`; Compose carries
|
||||||
|
only `${VAR}` references. `CHANGE_ME` never appears inline where `panama server enable`
|
||||||
|
cannot detect it.
|
||||||
|
|
||||||
|
Publicly sourced images use immutable versions or digests. A service that deliberately
|
||||||
|
tracks a locally controlled `latest` tag is excluded from unattended pulls unless its
|
||||||
|
deployment pipeline supplies and records an immutable digest.
|
||||||
|
|
||||||
|
### Package 4: fix desktop races and permanent work
|
||||||
|
|
||||||
|
#### One monitor per capture transaction
|
||||||
|
|
||||||
|
Capture snapshots the focused output name when a transaction opens and never reads live
|
||||||
|
focus again for that transaction. `shell.qml` creates one capture overlay per
|
||||||
|
`Quickshell.screens` entry. Only the overlay whose screen matches the snapshotted output
|
||||||
|
maps and accepts coordinates. Freeze, selection, capture, and commit all use the same
|
||||||
|
output name.
|
||||||
|
|
||||||
|
A two-screen QML fixture proves that focus changes after open do not move the transaction
|
||||||
|
and that overlay-local coordinates resolve against the frozen output.
|
||||||
|
|
||||||
|
Screenshot and recording destinations include millisecond precision plus an exclusive
|
||||||
|
collision suffix. The large frozen frame loads asynchronously while its overlay remains
|
||||||
|
unmapped; the overlay maps only after the image is ready or the fallback state is known.
|
||||||
|
|
||||||
|
#### Owned dictation recorder
|
||||||
|
|
||||||
|
The dictation helper serializes start, stop, cancel, and recovery with a lock. State is
|
||||||
|
written to a mode-`0600` temporary file and atomically replaced only after `pw-record`
|
||||||
|
starts.
|
||||||
|
|
||||||
|
State carries PID, `/proc` start time, expected executable identity, recording path, and
|
||||||
|
creation time. Stop and cancel signal a process only after all identity fields match.
|
||||||
|
Stale state is removed without signalling an unknown PID. Concurrent start returns the
|
||||||
|
existing active recording instead of launching a second recorder.
|
||||||
|
|
||||||
|
Behavioral tests use real short-lived fixture processes to prove concurrent start, stale
|
||||||
|
PID, PID mismatch, normal stop, cancel, and interrupted state write.
|
||||||
|
|
||||||
|
#### Request-owned Screen Intelligence work
|
||||||
|
|
||||||
|
Every analysis receives a monotonically increasing generation. Capture, OCR, and model
|
||||||
|
processes record that generation when started. Stream and exit callbacks ignore output
|
||||||
|
whose generation no longer matches the active request. `close()` invalidates the
|
||||||
|
generation before stopping any process.
|
||||||
|
|
||||||
|
The public QML interface does not change. A fixture overlaps requests and closes during
|
||||||
|
OCR to prove an old callback cannot set `ready`, `error`, or replace the new result.
|
||||||
|
|
||||||
|
#### Event-driven privacy state
|
||||||
|
|
||||||
|
Privacy monitoring uses Quickshell's PipeWire registry, node, link, and property change
|
||||||
|
signals. It does not fork `pw-dump` on an idle timer. Construction may perform at most
|
||||||
|
one initial registry read; every later update reacts to graph changes.
|
||||||
|
|
||||||
|
The existing curated-event and privacy interfaces remain intact. A contract runs an
|
||||||
|
isolated registry fixture long enough to prove no periodic process launches occur and
|
||||||
|
that camera, microphone, and screen-capture transitions still publish the same state.
|
||||||
|
|
||||||
|
#### Owned wallpaper processes and atomic writes
|
||||||
|
|
||||||
|
Video wallpaper cleanup signals only processes Panama launched. It tracks process IDs
|
||||||
|
and start times or uses a dedicated user-service cgroup. Global `pkill -x mpvpaper` and
|
||||||
|
global `pgrep` are removed.
|
||||||
|
|
||||||
|
Helpers that rewrite Compose, settings, or generated configuration write a same-directory
|
||||||
|
temporary file, preserve ownership and mode, flush and fsync, validate the temporary
|
||||||
|
content, then atomically replace the destination. A failed validation or write leaves the
|
||||||
|
original byte-for-byte intact.
|
||||||
|
|
||||||
|
#### Pinned speech assets
|
||||||
|
|
||||||
|
The Whisper container uses an immutable digest. The model has a committed URL, expected
|
||||||
|
size, and SHA-256. Downloads go to a temporary file and become active only after all
|
||||||
|
checks pass. A mismatch removes the temporary file and preserves the installed model.
|
||||||
|
|
||||||
|
### Package 5: remaining hardening and documentation
|
||||||
|
|
||||||
|
#### Desktop entries and media outputs
|
||||||
|
|
||||||
|
`panama-webapp` rejects NUL, newline, carriage return, and unsupported URL schemes.
|
||||||
|
It parses the URL, requires one logical argument, and escapes each Desktop Entry field
|
||||||
|
according to the specification. Browser switches cannot be introduced through the URL.
|
||||||
|
|
||||||
|
`panama-transcode` reserves a unique temporary output in the destination directory,
|
||||||
|
writes only to that path, and atomically renames after success. Failure cleanup removes
|
||||||
|
only the inode it created.
|
||||||
|
|
||||||
|
#### Documentation and policy alignment
|
||||||
|
|
||||||
|
- README role behavior, contract count, safe-test meaning, and sudo requirements match
|
||||||
|
the implemented commands.
|
||||||
|
- The manual uses the current Settings route names and hidden-leaf taxonomy.
|
||||||
|
- Migration guidance describes the actual interactive and noninteractive privilege
|
||||||
|
policy. Tests validate each command invocation, not file-wide word presence.
|
||||||
|
- Server documentation distinguishes policy Panama enforces from external firewall or
|
||||||
|
hosting assumptions.
|
||||||
|
- Generated settings documentation remains `--check` clean.
|
||||||
|
|
||||||
|
## Verification strategy
|
||||||
|
|
||||||
|
Every behavior follows one red-green cycle through an agreed interface:
|
||||||
|
|
||||||
|
| Repair | Interface under test | System adapter |
|
||||||
|
|---|---|---|
|
||||||
|
| Contract safety | `panama test` | temporary manifest and fixture contracts |
|
||||||
|
| Update safety | `panama update` | local Git remote and stub installer |
|
||||||
|
| Root bootstrap | `boot --server` | stub accounts, sshd, systemd, filesystem |
|
||||||
|
| Firewall | `setup-server` | stub firewall-cmd and systemctl |
|
||||||
|
| Installer state | `install` | temporary HOME/state and stub gsettings/stages |
|
||||||
|
| Server management | `panama server` | temporary catalog/HOME and stub systemctl/compose |
|
||||||
|
| Image updater | updater command | stateful Podman/systemd fixture |
|
||||||
|
| Capture | Capture QML interface | two-screen semantic harness |
|
||||||
|
| Dictation | helper CLI | fixture recorder processes and temporary runtime dir |
|
||||||
|
| Screen Intelligence | QML interface | delayed fixture processes |
|
||||||
|
| Privacy | privacy QML state | isolated PipeWire fixture |
|
||||||
|
|
||||||
|
After each package:
|
||||||
|
|
||||||
|
1. Run the focused contracts changed by that package.
|
||||||
|
2. Run `panama test --safe` under the new manifest.
|
||||||
|
3. Run syntax checks for every touched shell, Python, Lua, QML, YAML, and systemd file
|
||||||
|
using the tools available on the host.
|
||||||
|
4. Request independent code review and resolve Critical and Important findings.
|
||||||
|
5. Commit only that package.
|
||||||
|
|
||||||
|
Final repository verification includes the complete hermetic suite, Hyprland config
|
||||||
|
validation, generated-document checks, Compose rendering with fixture env files, secret
|
||||||
|
scanning, and `git diff --check`.
|
||||||
|
|
||||||
|
Non-hermetic checks are separate, explicitly granted gates after all hermetic work is
|
||||||
|
green. `live-host` remains read-only; compositor, desktop, and network checks receive
|
||||||
|
only their named grants. Server cutover, service restarts, SSH reload, firewall mutation,
|
||||||
|
and production deployment are not part of this implementation branch.
|
||||||
|
|
||||||
|
## Audit finding ledger
|
||||||
|
|
||||||
|
This table prevents a smaller issue from disappearing behind the larger repairs.
|
||||||
|
|
||||||
|
| Finding | Package |
|
||||||
|
|---|---|
|
||||||
|
| SSH hardening lockout | 2 |
|
||||||
|
| Unverified root installation inputs | 2 |
|
||||||
|
| Missing documented firewall policy | 2 |
|
||||||
|
| Update executes restored work-in-progress | 3 |
|
||||||
|
| Interrupted update recovery | 3 |
|
||||||
|
| Pull failure followed by success status | 3 |
|
||||||
|
| Hidden sudo prompt on noninteractive update | 3 |
|
||||||
|
| Hard-coded idle restoration | 3 |
|
||||||
|
| Automatic migration baseline inference | 3 |
|
||||||
|
| Unresolved secret migration marked complete | 3 |
|
||||||
|
| Missing required stage skipped | 3 |
|
||||||
|
| Backup collisions and foreign symlink loss | 3 |
|
||||||
|
| Dotenv sourced and evaluated as shell | 3 |
|
||||||
|
| Invalid container health gate and premature prune | 3 |
|
||||||
|
| Server disable false success | 3 |
|
||||||
|
| Hard-coded home and runtime UID | 3 |
|
||||||
|
| Service-name path components | 3 |
|
||||||
|
| Definition warning acknowledged before apply | 3 |
|
||||||
|
| Existing `.env` mode and symlink trust | 3 |
|
||||||
|
| Concurrent updater runs | 3 |
|
||||||
|
| Custom `PANAMA_PATH` update unit | 3 |
|
||||||
|
| Floating images with Podman socket access | 3 |
|
||||||
|
| Sidecar secret over-distribution | 3 |
|
||||||
|
| Inline SMTP placeholders outside `.env` | 3 |
|
||||||
|
| Multi-monitor capture mismatch | 4 |
|
||||||
|
| Dictation concurrent start and reusable PID | 4 |
|
||||||
|
| Stale Screen Intelligence callbacks | 4 |
|
||||||
|
| Permanent `pw-dump` polling | 4 |
|
||||||
|
| Global `mpvpaper` process cleanup | 4 |
|
||||||
|
| Non-atomic source/config rewrites | 4 |
|
||||||
|
| Mutable speech image and model | 4 |
|
||||||
|
| Capture filename collisions and synchronous frame load | 4 |
|
||||||
|
| Date-expiring agent-usage contract | 1 |
|
||||||
|
| Secret-scan prose false positive | 1 |
|
||||||
|
| False-green update contract | 1 |
|
||||||
|
| Incomplete safe-test classification | 1 |
|
||||||
|
| No runner timeout and hidden stderr | 1 |
|
||||||
|
| Contracts mutating tracked/live-linked paths | 1 |
|
||||||
|
| Contracts depending on real installed settings | 1 |
|
||||||
|
| Weak migration privilege policy test | 5 |
|
||||||
|
| Web-app Desktop Entry injection | 5 |
|
||||||
|
| Transcode output deletion race | 5 |
|
||||||
|
| Documentation drift | 5 |
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Running the bootstrap on a real VPS.
|
||||||
|
- Applying firewalld rules to a real host.
|
||||||
|
- Restarting or migrating any existing server service.
|
||||||
|
- Activating a new Hyprland session or changing monitor state.
|
||||||
|
- Visual redesign of Settings, capture, notifications, or other shell surfaces.
|
||||||
|
- Replacing Podman, Quickshell, Hyprland, or the existing command vocabulary.
|
||||||
|
- Broad refactoring unrelated to an audited failure path.
|
||||||
|
|
||||||
|
## Completion criteria
|
||||||
|
|
||||||
|
The remediation is complete when every ledger row has a verified implementation. A row
|
||||||
|
may leave the ledger only when a failing behavioral test disproves the audit finding and
|
||||||
|
Gabriel approves that removal. The hermetic suite must be green with no hidden stderr;
|
||||||
|
no contract classified safe may read live host/compositor state or touch live desktop,
|
||||||
|
network, or privileged state;
|
||||||
|
generated documentation and configuration validators must pass; and an independent
|
||||||
|
final review must find no unresolved Critical or Important issue.
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
# Secure bootstrap and privileged installation design
|
||||||
|
|
||||||
|
**Status:** Approved in chat on 2026-08-27
|
||||||
|
|
||||||
|
**Parent program:** `docs/superpowers/specs/2026-08-26-repository-audit-remediation-design.md`
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document refines Package 2 after repository-wide reconnaissance found that the
|
||||||
|
same trust boundary extends beyond the three examples named in the parent design.
|
||||||
|
Panama must not turn mutable network content into root-capable code. That includes
|
||||||
|
the initial root bootstrap, package-manager repository bootstraps, downloaded
|
||||||
|
installers and artifacts, and user-level processes that run while the installer
|
||||||
|
keeps a live sudo credential.
|
||||||
|
|
||||||
|
The package still has three independently testable parts:
|
||||||
|
|
||||||
|
1. transactional SSH hardening in `boot --server`;
|
||||||
|
2. verified bootstrap and installer inputs;
|
||||||
|
3. transactional server firewall policy.
|
||||||
|
|
||||||
|
All three are fixture-tested. This branch does not reload a real SSH daemon, mutate a
|
||||||
|
real firewall, install a real package, start a service, or apply changes to a server.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Keep automatic installation when an official signature or a reviewed immutable
|
||||||
|
digest is available. Otherwise make the component explicit and optional.
|
||||||
|
- Keep the public `boot --server` interface, but remove the mutable
|
||||||
|
`bash <(curl .../main/boot)` installation path from the documentation.
|
||||||
|
- A missing or unsafe target SSH key makes hardening unavailable but does not stop
|
||||||
|
bootstrap. Validation or reload failure rolls back and stops bootstrap.
|
||||||
|
- Safe target SSH state means a non-root account, an absolute home, a real `.ssh`
|
||||||
|
directory owned by the target UID with mode `0700`, and a nonempty regular
|
||||||
|
`authorized_keys` file owned by the target UID with mode `0600`. Symlinks are
|
||||||
|
refused. Every nonblank, non-comment key line must parse with OpenSSH tooling.
|
||||||
|
Panama may create and normalize files it copied from root, but final destination
|
||||||
|
creation and writing run as the target UID. It does not take ownership of an
|
||||||
|
unsafe pre-existing target path or assume that the user's primary group has the
|
||||||
|
same name as the user.
|
||||||
|
- Failed SSH reload rollback includes restored-config validation and a reload of the
|
||||||
|
restored configuration, because a command can apply state and still return
|
||||||
|
nonzero.
|
||||||
|
- Firewalld is required. Setup attempts to enable and start it, then verifies both
|
||||||
|
states before any rule mutation.
|
||||||
|
- Ports 80 and 443 are public only through Cloudflare source ipsets. Port 81 is open
|
||||||
|
only in a unique WireGuard-only zone. With no WireGuard zone it stays closed and
|
||||||
|
setup reports that fact; ambiguity or an unsafe mixed-interface zone fails before
|
||||||
|
mutation.
|
||||||
|
- Panama removes only its own rules and the exact legacy direct `80/tcp`, `443/tcp`,
|
||||||
|
and `81/tcp` rules created by the old server installer. Other broad rules are
|
||||||
|
reported as conflicts, not silently deleted.
|
||||||
|
- There is no privileged firewall timer. Server installation and upgrades are the
|
||||||
|
refresh entry points.
|
||||||
|
- Use narrow verification helpers, not a generalized installer framework.
|
||||||
|
|
||||||
|
## Shared trust model
|
||||||
|
|
||||||
|
`install` obtains one sudo credential and refreshes it until the run ends. Therefore
|
||||||
|
code executed as the target user during that window is root-capable in practice.
|
||||||
|
Panama treats fetched shell, binaries, RPM scriptlets, npm lifecycle code, container
|
||||||
|
entrypoints, and sourced repository build definitions as executable inputs.
|
||||||
|
|
||||||
|
Fetched JSON, repository descriptors, keys, checksums, HTML, icons, and CIDR lists are
|
||||||
|
data only while Panama parses them without executing them. They still require strict
|
||||||
|
shape, size, signer, and destination checks before they can authorize an executable
|
||||||
|
transaction.
|
||||||
|
|
||||||
|
Every download follows this order:
|
||||||
|
|
||||||
|
1. create a checked private temporary directory;
|
||||||
|
2. download without sudo with redirects, a 10-second connection timeout, a
|
||||||
|
600-second total timeout, and the reviewed per-artifact byte limit;
|
||||||
|
3. verify the expected fingerprint, signature, or reviewed SHA-256;
|
||||||
|
4. stage the complete result on the destination filesystem;
|
||||||
|
5. use sudo only for the narrow final package/repository operation that needs it;
|
||||||
|
6. atomically replace user-owned installed artifacts;
|
||||||
|
7. remove temporary material on success, failure, INT, and TERM.
|
||||||
|
|
||||||
|
Verification failure never falls back to the fetched artifact and never removes a
|
||||||
|
known-good installed version.
|
||||||
|
|
||||||
|
## SSH hardening transaction
|
||||||
|
|
||||||
|
### Public flow
|
||||||
|
|
||||||
|
The root `boot --server` branch keeps account creation, password setup, key copy,
|
||||||
|
clone, and target-user handoff in the standalone `boot` file. It adds one private
|
||||||
|
function, `harden_server_ssh USER HOME`, because no repository helper exists before
|
||||||
|
the clone.
|
||||||
|
|
||||||
|
Before offering hardening, Panama verifies:
|
||||||
|
|
||||||
|
- the target account exists, has a numeric UID other than 0, and its `getent` home is
|
||||||
|
absolute and nonempty;
|
||||||
|
- neither the home-relative `.ssh` path nor `authorized_keys` is a symlink;
|
||||||
|
- `.ssh` and `authorized_keys` have the exact ownership and modes in Decisions;
|
||||||
|
- `authorized_keys` contains at least one nonblank, non-comment line, and OpenSSH
|
||||||
|
parses every such line as a public key;
|
||||||
|
- an installed SSH unit is detected, preferring `sshd.service` and falling back to
|
||||||
|
`ssh.service` only when the first unit is absent.
|
||||||
|
|
||||||
|
If the target has no key and root has a safe regular key file, Panama copies only that
|
||||||
|
file. The target UID creates or normalizes `.ssh` at `0700` and writes
|
||||||
|
`authorized_keys` at `0600` through an already-open root-key input. Panama then
|
||||||
|
revalidates exact UID ownership, modes, and OpenSSH key parsing. It does not chown a
|
||||||
|
target-controlled path or assume a same-named primary group.
|
||||||
|
|
||||||
|
If the key preconditions fail, no SSH unit is installed, or the Panama drop-in path
|
||||||
|
already names a symlink or non-regular object, Panama prints why hardening is
|
||||||
|
unavailable, keeps root/password authentication unchanged, and continues the
|
||||||
|
clone/install handoff. Declining the prompt has the same unchanged-state outcome.
|
||||||
|
|
||||||
|
### Transaction
|
||||||
|
|
||||||
|
The desired drop-in is exactly:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PermitRootLogin no
|
||||||
|
PasswordAuthentication no
|
||||||
|
KbdInteractiveAuthentication no
|
||||||
|
```
|
||||||
|
|
||||||
|
Panama creates the candidate with `umask 077` and `mktemp` in
|
||||||
|
`/etc/ssh/sshd_config.d`. Its temporary name does not end in `.conf`, so the normal
|
||||||
|
include glob cannot activate it early. It refuses a pre-existing Panama path unless
|
||||||
|
it is a non-symlink regular file. It preserves an existing `00-panama.conf`, including
|
||||||
|
its ownership, mode, timestamps, ACLs, and extended attributes, in the same directory.
|
||||||
|
State-aware EXIT/INT/TERM cleanup is armed before the first candidate or backup
|
||||||
|
artifact, and the candidate is atomically renamed over the final path.
|
||||||
|
|
||||||
|
It then runs `sshd -t` against the complete active configuration. Before reload,
|
||||||
|
`sshd -T -C` must report `permitrootlogin no`, `passwordauthentication no`, and
|
||||||
|
`kbdinteractiveauthentication no` for the root context. The target-user context must
|
||||||
|
report both authentication directives as `no`. This fails closed when an earlier
|
||||||
|
main-config directive wins despite the precedence-safe filename. Panama reloads only
|
||||||
|
the detected unit after every check passes. Success disarms rollback and removes the
|
||||||
|
backup.
|
||||||
|
|
||||||
|
On validation failure, Panama restores or removes the new drop-in, validates the
|
||||||
|
restored configuration, and returns nonzero without reloading the rejected candidate.
|
||||||
|
On reload failure, Panama restores the previous drop-in, validates it, reloads the
|
||||||
|
restored unit, and returns nonzero. A rollback validation/reload failure preserves the
|
||||||
|
backup and prints its path plus exact recovery commands. When no prior file existed,
|
||||||
|
recovery instead instructs the operator to remove `00-panama.conf`, run `sshd -t`,
|
||||||
|
and reload the detected unit.
|
||||||
|
|
||||||
|
Existing drop-ins go through the same desired-content, validation, and reload path;
|
||||||
|
mere existence is not treated as proof of hardening.
|
||||||
|
|
||||||
|
### Hermetic adapter
|
||||||
|
|
||||||
|
`PANAMA_BOOT_FIXTURE_ROOT` exists only for the contract. `boot` accepts it only when
|
||||||
|
the real effective UID is nonzero while the stubbed `id -u` reports root; an actual
|
||||||
|
root process that sets it exits before mutation. Absolute system paths are resolved
|
||||||
|
beneath that fixture root. Command adapters remain ordinary PATH stubs.
|
||||||
|
|
||||||
|
The public test seam is the real `boot --server` command under a PTY. The fixture
|
||||||
|
stubs `id`, `passwd`, `getent`, `useradd`, `usermod`, `runuser`, `git`, `dnf`, `sshd`,
|
||||||
|
and `systemctl`, and provides temporary account and filesystem state.
|
||||||
|
|
||||||
|
Required cases are:
|
||||||
|
|
||||||
|
- missing, empty, comment-only, malformed, mixed valid/malformed, symlinked,
|
||||||
|
wrong-owner, and wrong-mode target keys;
|
||||||
|
- safe root-key copy and safe existing target key;
|
||||||
|
- target-UID copy normalization with a primary group whose name differs from the user;
|
||||||
|
- declined hardening and no installed SSH unit;
|
||||||
|
- pre-existing symlink, directory, and FIFO Panama drop-ins;
|
||||||
|
- successful initial install and replacement of an existing drop-in;
|
||||||
|
- invalid syntax and conflicting effective-policy rollback;
|
||||||
|
- failed reload rollback, including restored validation and reload;
|
||||||
|
- rollback failure retaining its recovery artifact or printing no-prior-file removal;
|
||||||
|
- INT/TERM during candidate and backup preparation as well as after activation;
|
||||||
|
- actual-root rejection of `PANAMA_BOOT_FIXTURE_ROOT` in a user namespace;
|
||||||
|
- exact command ordering and no install handoff after a transactional failure.
|
||||||
|
|
||||||
|
## Verified bootstrap and installer inputs
|
||||||
|
|
||||||
|
### Initial Panama bootstrap
|
||||||
|
|
||||||
|
README installation instructions use a two-file trust assertion: a full 40-character
|
||||||
|
Git commit and the SHA-256 of `boot` at that commit. The command downloads the raw file
|
||||||
|
from the commit URL with a 10-second connection timeout, 30-second total timeout, and
|
||||||
|
256 KiB maximum, checks SHA-256, and only then executes it.
|
||||||
|
|
||||||
|
The verified revision is passed as `PANAMA_BOOT_REVISION`. `boot` requires a full
|
||||||
|
lowercase hexadecimal commit, fetches that exact object, verifies
|
||||||
|
`git rev-parse HEAD^{commit}` equality, and only then executes `install`. A fresh
|
||||||
|
clone creates local `main` at that commit with `origin/main` as its upstream. An
|
||||||
|
existing checkout must be clean and may only fast-forward to the verified commit;
|
||||||
|
Panama never resets or overwrites local work. A mismatch, dirty checkout, divergent
|
||||||
|
history, or fetch failure stops before handoff.
|
||||||
|
|
||||||
|
This uses two commits when refreshing the documented bootstrap: the implementation is
|
||||||
|
committed first; a following documentation commit records the preceding full commit
|
||||||
|
and `git show COMMIT:boot | sha256sum`. A hermetic contract recomputes the documented
|
||||||
|
digest from that committed object. A digest copied from the same hosting origin is not
|
||||||
|
a substitute for a long-lived publisher signing key, so Package 3 owns the stronger
|
||||||
|
authenticated-update/release-manifest design.
|
||||||
|
|
||||||
|
### Verification helpers and provenance data
|
||||||
|
|
||||||
|
Create `setup/lib/artifact-provenance` with only these public shell functions:
|
||||||
|
|
||||||
|
```text
|
||||||
|
key_fingerprint_matches FILE EXPECTED_FINGERPRINT
|
||||||
|
download_sha256 URL EXPECTED_SHA256 MAX_BYTES DESTINATION
|
||||||
|
verify_detached_signature KEY_FILE SIGNATURE_FILE CONTENT_FILE
|
||||||
|
rpm_signature_matches PACKAGE_FILE KEY_FILE EXPECTED_FINGERPRINT
|
||||||
|
```
|
||||||
|
|
||||||
|
`rpm_signature_matches` imports only the expected key into a temporary RPM database
|
||||||
|
and requires `rpmkeys --dbpath ... --checksig` to report a valid package signature;
|
||||||
|
it does not trust or modify the host RPM keyring.
|
||||||
|
|
||||||
|
Create `setup/provenance/installers.conf` as non-executable data. It uses one
|
||||||
|
`NAME=value` assignment per line, rejects unknown/duplicate keys, and is parsed without
|
||||||
|
`source` or `eval`. Per-architecture URLs, SHA-256 values, and maximum byte counts are
|
||||||
|
explicit.
|
||||||
|
|
||||||
|
Vendored public keys live under `setup/provenance/keys/`. The accompanying README
|
||||||
|
records the official source URL, retrieval date, complete primary fingerprint,
|
||||||
|
verification command, and rotation policy. Runtime verification compares the complete
|
||||||
|
primary fingerprint, not a short key ID.
|
||||||
|
|
||||||
|
As reviewed on 2026-08-27, the trust anchors are:
|
||||||
|
|
||||||
|
| Source | Primary fingerprint |
|
||||||
|
|---|---|
|
||||||
|
| Terra 44 | `AE09157A4DE88B497EA1D5D300CDAB43DE226D6F` |
|
||||||
|
| Anthropic Claude Code | `31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE` |
|
||||||
|
| Bun releases | `F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` |
|
||||||
|
| RPM Fusion free | `E9A491A3DE247814E7E067EAE06F8ECDD651FF2E` |
|
||||||
|
| RPM Fusion nonfree | `79BDB88F9BBF73910FD4095B6A2AF96194843C65` |
|
||||||
|
| lionheartp/Hyprland COPR | `97E23476C89635135407C7D5E9BA41342C4B2995` |
|
||||||
|
| Flathub | `6E5C05D979C76DAF93C081354184DD4D907A7CAE` |
|
||||||
|
| Claude Desktop Extra | `825A7D15D78BABE45646D5DF382409F597908867` |
|
||||||
|
|
||||||
|
The initial reviewed artifact pins are:
|
||||||
|
|
||||||
|
| Artifact | Architecture | SHA-256 |
|
||||||
|
|---|---|---|
|
||||||
|
| Bun 1.4.0 | x86_64 | `2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452` |
|
||||||
|
| Bun 1.4.0 | aarch64 | `4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e` |
|
||||||
|
| Node 24.20.0 | x86_64 | `2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2` |
|
||||||
|
| Node 24.20.0 | aarch64 | `5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7` |
|
||||||
|
| Codex 0.150.1 package | x86_64 musl | `00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17` |
|
||||||
|
| Codex 0.150.1 package | aarch64 musl | `1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639` |
|
||||||
|
| RustDesk 1.4.9 RPM | x86_64 | `eb1b053ac5b2f774f2271f7fbbfd2ea475899f7a55135c5e172bc54b9388f108` |
|
||||||
|
|
||||||
|
Unsupported architectures fail before download. Updating any version, URL, digest, or
|
||||||
|
key is an explicit reviewed repository change.
|
||||||
|
|
||||||
|
The provenance README cites these publisher-controlled records:
|
||||||
|
|
||||||
|
- Terra package instructions, key, and bootstrap limitation:
|
||||||
|
`https://github.com/terrapkg/packages/blob/frawhide/README.md`,
|
||||||
|
`https://repos.fyralabs.com/terra44/key.asc`, and
|
||||||
|
`https://github.com/terrapkg/packages/discussions/7736`;
|
||||||
|
- Anthropic setup and signing key: `https://code.claude.com/docs/en/setup` and
|
||||||
|
`https://downloads.claude.ai/keys/claude-code.asc`;
|
||||||
|
- Bun release and release-key usage:
|
||||||
|
`https://github.com/oven-sh/bun/releases/tag/bun-v1.4.0` and
|
||||||
|
`https://github.com/oven-sh/bun/blob/main/dockerhub/distroless/Dockerfile`;
|
||||||
|
- RPM Fusion keys:
|
||||||
|
`https://download1.rpmfusion.org/free/fedora/RPM-GPG-KEY-rpmfusion-free-fedora-2020`
|
||||||
|
and
|
||||||
|
`https://download1.rpmfusion.org/nonfree/fedora/RPM-GPG-KEY-rpmfusion-nonfree-fedora-2020`;
|
||||||
|
- Hyprland COPR key:
|
||||||
|
`https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/pubkey.gpg`;
|
||||||
|
- Flathub descriptor: `https://flathub.org/repo/flathub.flatpakrepo`;
|
||||||
|
- Claude Desktop Extra key:
|
||||||
|
`https://patrickjaja.github.io/claude-desktop-extra/gpg-key.asc`;
|
||||||
|
- Node release and verification instructions:
|
||||||
|
`https://github.com/nodejs/node/releases/tag/v24.20.0` and
|
||||||
|
`https://github.com/nodejs/node/blob/main/README.md`;
|
||||||
|
- Codex release and signing workflow:
|
||||||
|
`https://github.com/openai/codex/releases/tag/rust-v0.150.1` and
|
||||||
|
`https://github.com/openai/codex/blob/main/.github/workflows/rust-release.yml`;
|
||||||
|
- RustDesk release: `https://github.com/rustdesk/rustdesk/releases/tag/1.4.9`.
|
||||||
|
|
||||||
|
### Repository roots
|
||||||
|
|
||||||
|
- RPM Fusion release RPMs are downloaded first, verified against the vendored free
|
||||||
|
and nonfree keys, then installed as local files with
|
||||||
|
`--setopt=localpkg_gpgcheck=1`.
|
||||||
|
- Terra installs no package with `--nogpgcheck`. Panama verifies the Terra 44 key,
|
||||||
|
installs it atomically, and uses `terra.pkg_gpgcheck=1`,
|
||||||
|
`terra.repo_gpgcheck=1`, and the local pinned key with the official repository.
|
||||||
|
Failure stops before initial, desktop, or Hyprland package transactions.
|
||||||
|
- Hyprland COPR configuration is written from reviewed local data with the exact
|
||||||
|
base URL, `gpgcheck=1`, and the pinned project key. The publisher does not provide
|
||||||
|
`repomd.xml.asc`, so this repository has an explicit `repo_gpgcheck=0` exception;
|
||||||
|
package signatures remain mandatory. Panama does not use interactive/TOFU
|
||||||
|
`dnf copr enable -y`.
|
||||||
|
- Flathub's descriptor is downloaded as data. Panama decodes and verifies its embedded
|
||||||
|
primary key and requires GPG verification before adding or retaining the remote.
|
||||||
|
A mismatch preserves an existing remote and skips Flathub transactions.
|
||||||
|
|
||||||
|
The exact Terra DNF command receives a disposable Fedora 44 container smoke test. It
|
||||||
|
is never tried on the daily-driver host. If the signed bootstrap cannot be made to
|
||||||
|
work, Terra is reported unavailable and desktop installation stops before packages
|
||||||
|
that depend on it.
|
||||||
|
|
||||||
|
### User tools
|
||||||
|
|
||||||
|
- Node 24.20.0 is installed into the nvm version layout from the reviewed per-arch
|
||||||
|
archive and selected as the default. `nvm install --lts` is removed.
|
||||||
|
- pnpm comes from Fedora's signed `pnpm` package. If unavailable, Panama reports a
|
||||||
|
soft failure; it does not run npm or a remote installer as fallback.
|
||||||
|
- Bun is installed from the reviewed archive after SHA-256 verification; the remote
|
||||||
|
install script is removed.
|
||||||
|
- Claude Code uses Anthropic's signed RPM repository after exact key and repository
|
||||||
|
validation; `claude.ai/install.sh` is removed.
|
||||||
|
- Codex uses the reviewed per-arch package archive and an atomic user-owned install;
|
||||||
|
unversioned `npm install -g` is removed.
|
||||||
|
- RustDesk uses the reviewed versioned RPM URL and SHA-256 before the narrow sudo DNF
|
||||||
|
install. It never resolves `latest` at runtime.
|
||||||
|
- Claude Desktop repository setup is never downloaded or executed. If an existing
|
||||||
|
repository has the expected base URL, vendored-key fingerprint, `gpgcheck=1`, and
|
||||||
|
`repo_gpgcheck=1`, Panama may install from it. Otherwise it prints one optional
|
||||||
|
manual step and continues successfully.
|
||||||
|
- Vicinae extensions use `npm ci`; lock mismatch fails the extension build without
|
||||||
|
modifying the tracked lockfile.
|
||||||
|
|
||||||
|
Changing installer code, provenance data, keys, or package lists invalidates the
|
||||||
|
installer-stage hash so the next upgrade re-runs verification.
|
||||||
|
|
||||||
|
### Hermetic contract
|
||||||
|
|
||||||
|
`tests/setup/package-provenance-contract` runs real `gpg`/`sha256sum` against a test
|
||||||
|
key and signed tiny fixture manifest. It stubs curl, sudo, DNF, rpmkeys, Git, and
|
||||||
|
Flatpak. It proves:
|
||||||
|
|
||||||
|
- correct signature/digest success and verify-before-install ordering;
|
||||||
|
- wrong fingerprint, wrong signer, bad signature, absent checksum, truncation,
|
||||||
|
tampering, unsupported architecture, and interrupted download refusal;
|
||||||
|
- known-good target preservation and temporary cleanup;
|
||||||
|
- exact RPM Fusion, Terra, COPR, Flathub, Claude Code, and Claude Desktop policies;
|
||||||
|
- no `curl | shell`, unpinned npm global install, moving `latest` lookup, or
|
||||||
|
unverified root package input remains in the base installer;
|
||||||
|
- both x86_64 and aarch64 select only their reviewed URL and digest.
|
||||||
|
|
||||||
|
The existing `boot`, README, package-list, desktop-first, apps, launcher-search, and
|
||||||
|
update-command contracts are updated where their old expectations contradict the new
|
||||||
|
trust boundary.
|
||||||
|
|
||||||
|
## Server firewall transaction
|
||||||
|
|
||||||
|
### Files and policy
|
||||||
|
|
||||||
|
Create `server/scripts/update-firewall` as a standard-library Python executable and
|
||||||
|
invoke it from the real `setup/scripts/setup-server` stage. It is server-only and does
|
||||||
|
not reuse the desktop `panama-firewall` helper.
|
||||||
|
|
||||||
|
The repository carries LF-terminated canonical CIDR files:
|
||||||
|
|
||||||
|
- `server/firewall/cloudflare-v4.cidrs`
|
||||||
|
- `server/firewall/cloudflare-v6.cidrs`
|
||||||
|
|
||||||
|
They begin with the current official ranges published at
|
||||||
|
`https://www.cloudflare.com/ips-v4` and `https://www.cloudflare.com/ips-v6`.
|
||||||
|
|
||||||
|
Each refresh uses a 10-second connection timeout and 30-second total timeout, and
|
||||||
|
downloads at most 64 KiB per response without sudo. It requires:
|
||||||
|
|
||||||
|
- valid UTF-8/ASCII with one value per line;
|
||||||
|
- no blanks, whitespace, comments, or trailing fields;
|
||||||
|
- `ipaddress.ip_network(value, strict=True)` success;
|
||||||
|
- matching file family, canonical string form, no `/0`, no duplicate, and nonempty
|
||||||
|
IPv4 and IPv6 sets.
|
||||||
|
|
||||||
|
The pair is indivisible. One invalid response changes nothing. With an existing valid
|
||||||
|
Panama policy, refresh failure warns and preserves it. On first setup, the validated
|
||||||
|
committed pair is the fallback.
|
||||||
|
|
||||||
|
### Desired state
|
||||||
|
|
||||||
|
Content-addressed ipsets are named:
|
||||||
|
|
||||||
|
```text
|
||||||
|
panama-cf4-<first 12 SHA-256 hex>
|
||||||
|
panama-cf6-<first 12 SHA-256 hex>
|
||||||
|
```
|
||||||
|
|
||||||
|
The selected public zone receives exactly four Panama-owned rich rules: IPv4 and IPv6
|
||||||
|
sources for each of `80/tcp` and `443/tcp`. The selected WireGuard zone receives only
|
||||||
|
`81/tcp`. Panama adds no service-specific port.
|
||||||
|
|
||||||
|
Zone selection is stored without shell evaluation in
|
||||||
|
`${XDG_CONFIG_HOME:-$HOME/.config}/panama/server-firewall.conf`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
public_zone=public
|
||||||
|
wireguard_zone=wireguard
|
||||||
|
```
|
||||||
|
|
||||||
|
On first run, the public zone is firewalld's default zone. The WireGuard zone is the
|
||||||
|
unique active zone whose interfaces are all named `wg*`. Zero matches leaves port 81
|
||||||
|
closed and omits the setting. More than one match, identical public/WireGuard zones,
|
||||||
|
or a candidate containing a non-WireGuard interface fails before mutation. Explicit
|
||||||
|
stored zones must still exist and satisfy those invariants.
|
||||||
|
|
||||||
|
Before mutation, Panama refuses unrelated services, port ranges, ACCEPT zone targets,
|
||||||
|
or rich rules that broadly admit 80/443. It prints exact inspection commands. It
|
||||||
|
removes the exact direct 80/443/81 legacy rules only during the documented migration.
|
||||||
|
|
||||||
|
### Recoverable transaction
|
||||||
|
|
||||||
|
The updater acquires a nonblocking user-state lock and writes a checked journal to
|
||||||
|
`${XDG_STATE_HOME:-$HOME/.local/state}/panama/firewall/pending.json` containing only
|
||||||
|
Panama-owned rules/ipsets, the legacy direct ports, the WireGuard 81 rule, and selected
|
||||||
|
zones.
|
||||||
|
|
||||||
|
It then:
|
||||||
|
|
||||||
|
1. creates and fills the new permanent generation ipsets;
|
||||||
|
2. adds the four new public rules;
|
||||||
|
3. adds WireGuard 81 when a valid zone exists;
|
||||||
|
4. removes old Panama rules, exact legacy ports, and unreferenced Panama ipsets;
|
||||||
|
5. runs `firewall-cmd --check-config`;
|
||||||
|
6. performs one reload;
|
||||||
|
7. reads permanent and runtime state back;
|
||||||
|
8. writes selected-zone configuration and removes the journal only after equivalence.
|
||||||
|
|
||||||
|
Old live rules remain active until the single reload. A partial permanent transaction
|
||||||
|
therefore does not create an outage. Any error restores the snapshot and reloads.
|
||||||
|
Rollback success still returns nonzero. Rollback failure retains the journal and
|
||||||
|
prints its path plus the exact retry command. A later run restores a pending journal
|
||||||
|
before considering new inputs.
|
||||||
|
|
||||||
|
`setup-server` attempts `sudo systemctl enable --now firewalld`, then requires both
|
||||||
|
`is-enabled` and `is-active`. Missing commands or failed postconditions stop before
|
||||||
|
the updater. The updater itself is never run through sudo; only its narrow systemctl
|
||||||
|
and firewall-cmd mutations use sudo.
|
||||||
|
|
||||||
|
### Stateful contract
|
||||||
|
|
||||||
|
`tests/setup/server-firewall-contract` invokes the real `setup-server`. Its stateful
|
||||||
|
stubs keep separate permanent/runtime JSON and exact argv logs. `firewall-cmd` supports
|
||||||
|
only the queried zone, service, port, rich-rule, ipset, check-config, and reload
|
||||||
|
operations. Reload copies permanent to runtime. Exact one-shot failure selectors leave
|
||||||
|
state unchanged.
|
||||||
|
|
||||||
|
Required cases are:
|
||||||
|
|
||||||
|
- missing/inactive firewalld that cannot become enabled and active;
|
||||||
|
- first setup with legacy rules, exact desired ipsets/rules, WireGuard-only 81,
|
||||||
|
unrelated SSH/manual rules preserved, and one reload;
|
||||||
|
- every malformed CIDR class and indivisible-pair behavior;
|
||||||
|
- no WireGuard zone, ambiguous zones, identical zones, and mixed interfaces;
|
||||||
|
- unrelated broad exposure conflict refusal;
|
||||||
|
- reload failure with successful rollback and rollback-reload failure with journal;
|
||||||
|
- identical second run with no mutation/reload and valid two-family refresh;
|
||||||
|
- interrupted journal recovery before new evaluation.
|
||||||
|
|
||||||
|
## Scope ledger amendments
|
||||||
|
|
||||||
|
Reconnaissance added findings that the parent ledger did not name. Ownership is:
|
||||||
|
|
||||||
|
| Finding | Package |
|
||||||
|
|---|---|
|
||||||
|
| Mutable root bootstrap and mutable initial checkout | 2 |
|
||||||
|
| Unverified RPM Fusion/COPR/Flathub trust roots | 2 |
|
||||||
|
| Moving Node/pnpm/Bun/Claude/Codex/RustDesk inputs | 2 |
|
||||||
|
| Mutable npm install during Vicinae extension build | 2 |
|
||||||
|
| Unauthenticated fetched revision used by `panama update` | 3 |
|
||||||
|
| Mutable source-app build inputs | 5 |
|
||||||
|
| Mutable Neovim bootstrap/plugin graph | 5 |
|
||||||
|
|
||||||
|
Package 3 must define an authenticated approved revision or signed release manifest
|
||||||
|
before fetched Panama code reaches `install --upgrade`. Package 5 must pin or refuse
|
||||||
|
the ChatGPT source application and Neovim bootstrap/plugin graph. Package 3 retains
|
||||||
|
container image trust, and Package 4 retains the Whisper image/model pins already
|
||||||
|
assigned by the parent design.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Package 2 updates:
|
||||||
|
|
||||||
|
- README bootstrap commands and root-server narrative;
|
||||||
|
- README/server README firewall policy and port exposure;
|
||||||
|
- installer comments that currently defend moving or unsigned inputs;
|
||||||
|
- package/provenance documentation and key-rotation procedure;
|
||||||
|
- Panama development/operator skills when command behavior changes.
|
||||||
|
|
||||||
|
Documentation never claims a real host was cut over or a live reload succeeded.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Each implementation plan uses red-green cycles through public interfaces. The package
|
||||||
|
gate includes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n boot install setup/scripts/install-packages setup/scripts/setup-server \
|
||||||
|
setup/scripts/link-vicinae-scripts setup/lib/artifact-provenance \
|
||||||
|
tests/setup/root-server-bootstrap-contract \
|
||||||
|
tests/setup/package-provenance-contract \
|
||||||
|
tests/setup/server-firewall-contract
|
||||||
|
|
||||||
|
python3 -m py_compile server/scripts/update-firewall
|
||||||
|
|
||||||
|
tests/setup/root-server-bootstrap-contract
|
||||||
|
tests/setup/package-provenance-contract
|
||||||
|
tests/setup/server-firewall-contract
|
||||||
|
tests/setup/boot-contract
|
||||||
|
tests/setup/role-contract
|
||||||
|
tests/setup/readme-contract
|
||||||
|
tests/setup/package-lists-contract
|
||||||
|
tests/setup/desktop-first-contract
|
||||||
|
tests/setup/launcher-search-contract
|
||||||
|
tests/setup/update-command-contract
|
||||||
|
|
||||||
|
./bin/panama test --safe
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
If available, ShellCheck covers every touched shell file. The signed Terra command
|
||||||
|
also receives one disposable Fedora 44 container smoke test. The container has no host
|
||||||
|
mounts, host package database, system bus, SSH daemon, firewall access, or production
|
||||||
|
credentials.
|
||||||
|
|
||||||
|
## Completion criteria
|
||||||
|
|
||||||
|
Package 2 is complete when:
|
||||||
|
|
||||||
|
- no unverified mutable network response is executed by root or while relying on the
|
||||||
|
installer's sudo keepalive;
|
||||||
|
- the initial Panama revision and boot digest are checked before handoff;
|
||||||
|
- every automatic third-party executable input is signature-verified or pinned by a
|
||||||
|
reviewed immutable SHA-256;
|
||||||
|
- untrusted Claude Desktop setup is optional and never automatic;
|
||||||
|
- SSH hardening cannot remove the available login path and rolls back every tested
|
||||||
|
validation/reload failure;
|
||||||
|
- server firewall setup establishes the documented Cloudflare/WireGuard policy or
|
||||||
|
returns nonzero without losing the last known good policy;
|
||||||
|
- the full hermetic gate passes without live capability grants;
|
||||||
|
- independent package review has no unresolved Critical or Important finding.
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
# * setup-identity and install-hardware are dropped. They exist only to
|
# * setup-identity and install-hardware are dropped. They exist only to
|
||||||
# consume interview answers -- git identity, NVIDIA, Secure Boot, firmware
|
# consume interview answers -- git identity, NVIDIA, Secure Boot, firmware
|
||||||
# -- and every one of those is a first-run decision.
|
# -- and every one of those is a first-run decision.
|
||||||
# * install-packages runs only when the package lists actually changed.
|
# * install-packages runs only when its tracked installation inputs changed.
|
||||||
# * Migrations always run rather than baseline. See the migrations block.
|
# * Migrations always run rather than baseline. See the migrations block.
|
||||||
#
|
#
|
||||||
# Everything else is shared on purpose: the sudo keepalive, the per-stage
|
# Everything else is shared on purpose: the sudo keepalive, the per-stage
|
||||||
@@ -64,12 +64,15 @@ done
|
|||||||
|
|
||||||
source "$PANAMA_PATH/bin/ascii"
|
source "$PANAMA_PATH/bin/ascii"
|
||||||
|
|
||||||
# ── Have the package lists changed? ──────────────────────────────────────────
|
# ── Have the installation inputs changed? ───────────────────────────────────
|
||||||
#
|
#
|
||||||
# install-packages is the slow stage -- a dnf metadata refresh, a Flathub
|
# install-packages is the slow stage -- a dnf metadata refresh, a Flathub
|
||||||
# round-trip, and a transaction that resolves to "nothing to do" almost every
|
# round-trip, and a transaction that resolves to "nothing to do" almost every
|
||||||
# time. On an upgrade it is worth running only when the lists it reads actually
|
# time. On an upgrade it is worth running only when its package lists or
|
||||||
# changed, so this hashes them and remembers the result.
|
# reviewed installer trust inputs changed, so this hashes them and remembers
|
||||||
|
# the result. The framed, sorted stream includes top-level package files, the
|
||||||
|
# package-stage adapter, the provenance helper, and regular provenance files;
|
||||||
|
# both relative paths and bytes are part of the state.
|
||||||
#
|
#
|
||||||
# A content hash rather than a git range, because Panama is developed in place:
|
# A content hash rather than a git range, because Panama is developed in place:
|
||||||
# a package added to a list and not yet committed must still install. A range
|
# a package added to a list and not yet committed must still install. A range
|
||||||
@@ -86,25 +89,75 @@ STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
|
|||||||
PACKAGES_HASH="$STATE_DIR/packages-hash"
|
PACKAGES_HASH="$STATE_DIR/packages-hash"
|
||||||
|
|
||||||
hash_packages() {
|
hash_packages() {
|
||||||
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -exec sha256sum {} + \
|
local file relative size fixed_input digest
|
||||||
| sort | sha256sum | cut -d' ' -f1
|
|
||||||
|
for fixed_input in \
|
||||||
|
"$PANAMA_PATH/setup/scripts/install-packages" \
|
||||||
|
"$PANAMA_PATH/setup/lib/artifact-provenance"; do
|
||||||
|
[[ -f "$fixed_input" && ! -L "$fixed_input" && -r "$fixed_input" ]] || return 1
|
||||||
|
done
|
||||||
|
|
||||||
|
digest="$(
|
||||||
|
{
|
||||||
|
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -print0 || exit 1
|
||||||
|
printf '%s\0' \
|
||||||
|
"$PANAMA_PATH/setup/scripts/install-packages" \
|
||||||
|
"$PANAMA_PATH/setup/lib/artifact-provenance" || exit 1
|
||||||
|
find "$PANAMA_PATH/setup/provenance" -type f -print0 || exit 1
|
||||||
|
} | LC_ALL=C sort -z | while IFS= read -r -d '' file; do
|
||||||
|
relative="${file#"$PANAMA_PATH"/}"
|
||||||
|
size="$(wc -c <"$file")" || exit 1
|
||||||
|
printf '%s\0%s\0' "$relative" "$size" || exit 1
|
||||||
|
cat -- "$file" || exit 1
|
||||||
|
printf '\0' || exit 1
|
||||||
|
done | sha256sum | cut -d' ' -f1
|
||||||
|
)" || return 1
|
||||||
|
printf '%s\n' "$digest"
|
||||||
}
|
}
|
||||||
|
|
||||||
packages_needed() {
|
packages_needed() {
|
||||||
|
local current_hash recorded_hash
|
||||||
|
|
||||||
(( FORCE_PACKAGES )) && return 0
|
(( FORCE_PACKAGES )) && return 0
|
||||||
(( UPGRADE )) || return 0
|
(( UPGRADE )) || return 0
|
||||||
[[ -r "$PACKAGES_HASH" ]] || return 0
|
[[ -r "$PACKAGES_HASH" ]] || return 0
|
||||||
[[ "$(hash_packages)" != "$(cat "$PACKAGES_HASH")" ]]
|
current_hash="$(hash_packages)" || return 2
|
||||||
|
recorded_hash="$(cat "$PACKAGES_HASH")" || return 2
|
||||||
|
[[ "$current_hash" != "$recorded_hash" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Written only after the stage succeeds, mirroring the rule panama-migrate
|
# Written only after the stage succeeds, mirroring the rule panama-migrate
|
||||||
# documents for its markers: a step that did not complete has not happened, and
|
# documents for its markers: a step that did not complete has not happened, and
|
||||||
# recording it as done hides it forever.
|
# recording it as done hides it forever.
|
||||||
record_packages_hash() {
|
record_packages_hash() {
|
||||||
|
local temporary_hash
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
hash_packages >"$PACKAGES_HASH"
|
temporary_hash="$(mktemp "$STATE_DIR/.packages-hash.XXXXXX")" || return 1
|
||||||
|
if hash_packages >"$temporary_hash"; then
|
||||||
|
mv -f -- "$temporary_hash" "$PACKAGES_HASH"
|
||||||
|
else
|
||||||
|
rm -f -- "$temporary_hash"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Repository trust is checked before the installer can reach its bootstrap DNF.
|
||||||
|
# Status 78 is reserved for a trust-root failure and is propagated unchanged so
|
||||||
|
# no later stage, especially install-hardware, can invoke DNF with that repo.
|
||||||
|
TERRA_TRUST_FAILURE_STATUS=78
|
||||||
|
trust_preflight="$PANAMA_PATH/setup/scripts/install-packages"
|
||||||
|
if [[ ! -x "$trust_preflight" ]]; then
|
||||||
|
printf 'install: package repository trust preflight is unavailable\n' >&2
|
||||||
|
exit "$TERRA_TRUST_FAILURE_STATUS"
|
||||||
|
fi
|
||||||
|
if "$trust_preflight" --trust-preflight; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
trust_status=$?
|
||||||
|
printf 'install: package repository trust preflight failed\n' >&2
|
||||||
|
exit "$trust_status"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── The interview ────────────────────────────────────────────────────────────
|
# ── The interview ────────────────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# Everything Panama needs to be told is asked here, before a single package is
|
# Everything Panama needs to be told is asked here, before a single package is
|
||||||
@@ -289,16 +342,34 @@ for stage in "${STAGES[@]}"; do
|
|||||||
script="$PANAMA_PATH/setup/scripts/$stage"
|
script="$PANAMA_PATH/setup/scripts/$stage"
|
||||||
[[ -x "$script" ]] || continue
|
[[ -x "$script" ]] || continue
|
||||||
printf '\n=== %s ===\n' "$stage"
|
printf '\n=== %s ===\n' "$stage"
|
||||||
if [[ "$stage" == install-packages ]] && ! packages_needed; then
|
if [[ "$stage" == install-packages ]]; then
|
||||||
echo "The package lists have not changed since the last run; skipping."
|
package_state_status=0
|
||||||
echo "Run with --packages to install them anyway."
|
packages_needed || package_state_status=$?
|
||||||
continue
|
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
|
fi
|
||||||
if ! "$script"; then
|
if "$script"; then
|
||||||
|
if [[ "$stage" == install-packages ]]; then
|
||||||
|
if ! record_packages_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")
|
failed+=("$stage")
|
||||||
printf '!!! %s failed\n' "$stage" >&2
|
printf '!!! %s failed\n' "$stage" >&2
|
||||||
elif [[ "$stage" == install-packages ]]; then
|
|
||||||
record_packages_hash
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# replace the community ChatGPT Desktop build with the official OpenAI package
|
||||||
|
#
|
||||||
|
# `panama app chatgpt-desktop` used to build a community wrapper (codex-desktop)
|
||||||
|
# from the upstream macOS disk image, complete with a local rebuild daemon.
|
||||||
|
# OpenAI ships an official Linux RPM now, and the installer takes that instead;
|
||||||
|
# this repairs machines still carrying the community build. The official
|
||||||
|
# package goes on before the community one comes off, so a failure part-way
|
||||||
|
# leaves the machine with an app, never without one.
|
||||||
|
#
|
||||||
|
# Rules, because the runner cannot enforce them:
|
||||||
|
#
|
||||||
|
# * Safe to run twice. The marker records success, not intent.
|
||||||
|
# * Tolerant of the repair already being correct -- the user may have fixed
|
||||||
|
# it by hand, or a later ./install may have put it back.
|
||||||
|
# * Root work goes through `panama-sudo --reason "..."`, never bare sudo,
|
||||||
|
# so the password prompt names the repair.
|
||||||
|
# * Exit non-zero to be retried at the next login. Exit zero only when the
|
||||||
|
# machine is genuinely in the state this describes.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
|
|
||||||
|
# Machines that never had the community build are already correct. The official
|
||||||
|
# app is the installer's job, not this one's.
|
||||||
|
rpm -q codex-desktop >/dev/null 2>&1 || exit 0
|
||||||
|
|
||||||
|
# The same verified repository the installer establishes: the pinned signing
|
||||||
|
# key, then a repository that names it, so dnf checks OpenAI's signature before
|
||||||
|
# root installs anything. See setup/lib/chatgpt-package.
|
||||||
|
# shellcheck source=../setup/lib/chatgpt-package
|
||||||
|
source "$PANAMA_PATH/setup/lib/chatgpt-package"
|
||||||
|
|
||||||
|
sudo_cmd=(sudo)
|
||||||
|
if [[ -t 0 && -x "$PANAMA_PATH/bin/panama-sudo" ]]; then
|
||||||
|
sudo_cmd=(
|
||||||
|
"$PANAMA_PATH/bin/panama-sudo" --reason
|
||||||
|
"Replacing the community-built ChatGPT Desktop (codex-desktop) with the official OpenAI package"
|
||||||
|
--
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The official package first, so the machine is never left without one.
|
||||||
|
if ! rpm -q chatgpt >/dev/null 2>&1; then
|
||||||
|
chatgpt_install_repository "${sudo_cmd[@]}"
|
||||||
|
"${sudo_cmd[@]}" dnf install -y chatgpt
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The community package's updater is a user unit; stop it before dnf removes
|
||||||
|
# the unit file out from under it. Removal also takes the app in /opt, both
|
||||||
|
# binaries, and the polkit policy the local rebuilds needed.
|
||||||
|
systemctl --user disable --now codex-update-manager.service 2>/dev/null || true
|
||||||
|
"${sudo_cmd[@]}" dnf remove -y codex-desktop
|
||||||
|
systemctl --user daemon-reload 2>/dev/null || true
|
||||||
|
|
||||||
|
# The rebuild state the updater kept; the official package needs none of it.
|
||||||
|
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/codex-update-manager" \
|
||||||
|
"${XDG_CACHE_HOME:-$HOME/.cache}/codex-runtimes" \
|
||||||
|
"${XDG_CONFIG_HOME:-$HOME/.config}/codex-update-manager" \
|
||||||
|
"${XDG_STATE_HOME:-$HOME/.local/state}/codex-update-manager"
|
||||||
|
|
||||||
|
echo "Replaced the community codex-desktop build with the official chatgpt package."
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# ChatGPT Desktop.
|
|
||||||
#
|
|
||||||
# OpenAI ships macOS and Windows only. This is a community wrapper that converts
|
|
||||||
# the upstream macOS disk image into a Linux Electron app and packages it as an
|
|
||||||
# RPM, so the installed result is again something dnf owns.
|
|
||||||
#
|
|
||||||
# Same exception, same reason: there is no packaged form to prefer. Nothing is
|
|
||||||
# pinned; `bootstrap-native` fetches the current upstream image each time and
|
|
||||||
# fails loudly when it cannot.
|
|
||||||
|
|
||||||
description="ChatGPT Desktop, built into a Fedora RPM"
|
|
||||||
repo="https://github.com/ilysenko/codex-desktop-linux.git"
|
|
||||||
|
|
||||||
# bootstrap-native installs build dependencies, builds, packages, and installs
|
|
||||||
# the newest artifact -- so unlike the Claude build there is no separate install
|
|
||||||
# step to do here.
|
|
||||||
build() {
|
|
||||||
make bootstrap-native
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Pinned signing keys
|
||||||
|
|
||||||
|
A key lands here when a publisher signs what Panama installs but does not
|
||||||
|
publish the key, or its fingerprint, anywhere an install could fetch and check
|
||||||
|
them first. Pinning the key is what lets `dnf` verify a download before root
|
||||||
|
ever sees it.
|
||||||
|
|
||||||
|
Nothing here is a secret. These are public keys, and the reason to track them
|
||||||
|
is that a *changed* one should be a merge request somebody reads, not a silent
|
||||||
|
change of who is trusted.
|
||||||
|
|
||||||
|
## `RPM-GPG-KEY-chatgpt`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| Fingerprint | `3BFA0E4AE8B8CC16A2D9BA684A3B4A566C4660E4` |
|
||||||
|
| User ID | `Codex Linux Repository` |
|
||||||
|
| Signs | the `chatgpt` package and the repository metadata at `https://persistent.oaistatic.com/codex-app-prod/linux/rpm/$basearch` |
|
||||||
|
| Used by | `setup/lib/chatgpt-package` |
|
||||||
|
|
||||||
|
Captured on 2026-08-27 from a machine where the official package had been
|
||||||
|
installed, at `/etc/pki/rpm-gpg/RPM-GPG-KEY-chatgpt`, where the package's own
|
||||||
|
root scriptlet writes it. It is the key that signed both the installed
|
||||||
|
`chatgpt` package and the live `repodata/repomd.xml.asc`.
|
||||||
|
|
||||||
|
Be honest about what that is worth: OpenAI's documented instructions
|
||||||
|
(<https://learn.chatgpt.com/docs/linux/linux-app>) are to download an RPM and
|
||||||
|
install it, and they publish no key URL and no fingerprint to compare against.
|
||||||
|
So this is trust established on first use and then held, not trust verified
|
||||||
|
against the publisher. Held is the part that matters -- from here every machine
|
||||||
|
checks the same fingerprint, and a swapped download fails instead of installing.
|
||||||
|
|
||||||
|
To re-derive the fingerprint from the file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gpg --show-keys --with-colons setup/keys/RPM-GPG-KEY-chatgpt \
|
||||||
|
| awk -F: '$1 == "fpr" { print $10; exit }'
|
||||||
|
```
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQINBGpypFUBEACi1Vvzq9pIpA6lj7chbqELuxJtVuzUzxrasa6ZU0yF4yhq7jf8
|
||||||
|
3YkJRHwbezBKeQyzJ5lkX0EhXS8aXxUhMAm3PFpAlwcInfKzmV7atJwvaxIw6Rmd
|
||||||
|
GYe9fBWKjTN/SmPIjtyxrTznZY97+TfD1AeGZpLaJ8fsnhrC+HkiN2TACiTocgpe
|
||||||
|
hFiP0OWK7mWZeTWnY2scpIYXP1Ro7nQv4KacmY4JacTQ7m/HM0Qej/3olhuEv2Cw
|
||||||
|
lMVWw57/oHhmTllfLDQOogFQyIVqaaR98y/Eu6cAabSfcsqAAZ2A8vfHYD27z28J
|
||||||
|
vLO2PZEJd5ThlnX4Zqv0eIpZdBj//8Sl/MSqTshFZ1NDsRoqwdqw284X5MpnOJ4k
|
||||||
|
4Sc2Se8tJxt/nCeibH3dJ504Fb1X/mnOqhCAQ6pVJz4RB5HRlFPSkxVPyag1v1m/
|
||||||
|
7T4vie+OR4eqFQNz6mudrOoMmeVIfyL5fbe4cOr4fk/FyvEE2xMgkFatPqXn7vM9
|
||||||
|
og+zremPCfwRAFpBPyX74VowFY7llcdaj/w8K5T8PzM14Hb3E4ZKizMluKmTvTq9
|
||||||
|
WE1/eSQJLLQqXD5VmtmdUaC/VyE/1ZlIxcA1LWqvEQ327UXREvX/nHsrkKrl956W
|
||||||
|
jzkiHFUTsD1NJ0dMfs+csOt8Furb5jZj+HsMmCm9jLdfz5b/4WKLPbvxIwARAQAB
|
||||||
|
tBZDb2RleCBMaW51eCBSZXBvc2l0b3J5iQJRBBMBCgA7FiEEO/oOSui4zBai2bpo
|
||||||
|
SjtKVmxGYOQFAmpypFUCGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQ
|
||||||
|
SjtKVmxGYORlCQ/9FyikZo8HQcJBP9E/oXVPds/fQnIFB2qJR2z3DrfYEonNt/ev
|
||||||
|
SAySkPPq4/mEOjaI0pFlDDGSaps+FTcJFgoVRTasBIF7JJivvjW9ap8iWEbhhVLe
|
||||||
|
IrFLbMLpUcTRntUx7R4fVMJ/1/cGn+NWZmNwS9ORorzSyCH0IAgCw1Xc3ZrjuMbF
|
||||||
|
VjdToMC1TiXXCEmlYpQakmQ3Ay1cH0FHC2BBNn1MNVkJdPhpZIZCdhaMPHfYFpyo
|
||||||
|
pg8wFvZ5iIcvlbMgyuy8CPJVRWUcYy2dOhEOGnYJnXRPkE3E1hf8YOHNzRlduH89
|
||||||
|
6lT9qcEK2+fpLfrVGoc4zscLZ+Ey+Ko6iQRdVE1j67+wNR3hX8ukue574v1N/xxu
|
||||||
|
i575jumSE19lEj1sH4+P4gFHOtTbF0JhKKzLctbga0IAwTPKhnt3qzj1U5Yj/MZS
|
||||||
|
uEVjrLhdRauOuFBXUclgyVf2w/lE85UUOdlcollsYA6Huq7xDamqf8SslZQGre3E
|
||||||
|
I+lhpqJR1cOwDMUzzcl40uTyhrxXXd/bk4QSlhZbwHR25Pnt+ZMtWavlQWS0eDEV
|
||||||
|
8djuXAURCmx5WOqAFB/TJe1mn5EvyWg4VFzrY/NVNOpzgY5+Xp7J28z7f637r712
|
||||||
|
Eu9j4imVcdPigwS+jf/0f81i2o9b82Y26TN8+EtDLCY841MJ1lrjDrX/dno=
|
||||||
|
=Y+3h
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
Executable
+133
@@ -0,0 +1,133 @@
|
|||||||
|
#!/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
|
||||||
|
home="$(mktemp -d)" || exit 1
|
||||||
|
chmod 700 "$home"
|
||||||
|
trap 'rm -rf -- "$home"' EXIT
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
GNUPGHOME="$home" gpg --batch --with-colons --import-options show-only --import "$1" 2>/dev/null \
|
||||||
|
| awk -F: '$1 == "pub" { primary = 1; next } primary && $1 == "fpr" { print $10; primary = 0 }'
|
||||||
|
)
|
||||||
|
|
||||||
|
key_fingerprint_matches() {
|
||||||
|
local file="$1" expected="$2"
|
||||||
|
local -a primary_fingerprints=()
|
||||||
|
mapfile -t primary_fingerprints < <(_primary_key_fingerprints "$file")
|
||||||
|
[[ ${#primary_fingerprints[@]} -eq 1 && "${primary_fingerprints[0]}" == "$expected" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
_key_has_one_primary() {
|
||||||
|
local -a primary_fingerprints=()
|
||||||
|
mapfile -t primary_fingerprints < <(_primary_key_fingerprints "$1")
|
||||||
|
[[ ${#primary_fingerprints[@]} -eq 1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_detached_signature() {
|
||||||
|
local key="$1" signature="$2" content="$3" home
|
||||||
|
_key_has_one_primary "$key" || return 1
|
||||||
|
(
|
||||||
|
home="$(mktemp -d)" || exit 1
|
||||||
|
chmod 700 "$home"
|
||||||
|
trap 'rm -rf -- "$home"' EXIT
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
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 "$directory/.${filename}.part.XXXXXX")" || exit 1
|
||||||
|
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 status
|
||||||
|
|
||||||
|
key_fingerprint_matches "$key" "$expected" || return 1
|
||||||
|
home="$(mktemp -d)" || return 1
|
||||||
|
chmod 700 "$home"
|
||||||
|
db="$home/rpmdb"
|
||||||
|
mkdir -m 700 "$db" || {
|
||||||
|
rm -rf -- "$home"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
rpmkeys --dbpath "$db" --import "$key" >/dev/null 2>&1 \
|
||||||
|
&& output="$(rpmkeys --dbpath "$db" --checksig --verbose "$package" 2>&1)"
|
||||||
|
status=$?
|
||||||
|
rm -rf -- "$home"
|
||||||
|
(( status == 0 )) || return 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,97 @@
|
|||||||
|
# Installing OpenAI's ChatGPT Desktop without trusting the download. Sourced,
|
||||||
|
# not run.
|
||||||
|
#
|
||||||
|
# OpenAI signs both its packages and its repository metadata, with one key, and
|
||||||
|
# publishes neither that key nor its fingerprint anywhere a first install could
|
||||||
|
# fetch them. The documented instructions are "download this RPM and install
|
||||||
|
# it" -- and the RPM's own root scriptlet is what writes the repository file and
|
||||||
|
# drops the key into /etc/pki/rpm-gpg. Following them means handing an
|
||||||
|
# unverified download to root and letting it decide afterwards what to trust,
|
||||||
|
# which is the one thing this repository will not do with a network response.
|
||||||
|
#
|
||||||
|
# So the key is pinned here instead. setup/keys/ carries a copy and records
|
||||||
|
# where it came from; this verifies that copy's fingerprint, installs it, and
|
||||||
|
# writes the repository itself with gpgcheck on. dnf then checks the metadata
|
||||||
|
# signature and the package signature against that key before anything runs as
|
||||||
|
# root, and every later upgrade goes through the same repository and the same
|
||||||
|
# key.
|
||||||
|
#
|
||||||
|
# Two callers, which is why this is a library: install-packages, for a machine
|
||||||
|
# being built, and the migration that replaces the community codex-desktop
|
||||||
|
# build on machines that predate the official package.
|
||||||
|
|
||||||
|
# The key that signs the packages and the repository metadata. Pinned, so a
|
||||||
|
# substituted key is a failure here rather than a silent change of publisher.
|
||||||
|
CHATGPT_KEY_FINGERPRINT="3BFA0E4AE8B8CC16A2D9BA684A3B4A566C4660E4"
|
||||||
|
|
||||||
|
# `$basearch` stays literal: dnf expands it, and this is the same base URL the
|
||||||
|
# package's own scriptlet configures.
|
||||||
|
CHATGPT_REPO_BASEURL="https://persistent.oaistatic.com/codex-app-prod/linux/rpm/\$basearch"
|
||||||
|
CHATGPT_REPO_FILE="/etc/yum.repos.d/chatgpt.repo"
|
||||||
|
CHATGPT_KEY_FILE="/etc/pki/rpm-gpg/RPM-GPG-KEY-chatgpt"
|
||||||
|
|
||||||
|
chatgpt_pinned_key() {
|
||||||
|
printf '%s/setup/keys/RPM-GPG-KEY-chatgpt' "${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The fingerprint of the pinned copy. Nonzero when it cannot be read at all,
|
||||||
|
# which the caller reports differently from a key that reads but is the wrong
|
||||||
|
# one.
|
||||||
|
chatgpt_pinned_fingerprint() {
|
||||||
|
local key
|
||||||
|
key="$(chatgpt_pinned_key)"
|
||||||
|
[[ -r "$key" ]] || return 1
|
||||||
|
gpg --show-keys --with-colons "$key" 2>/dev/null \
|
||||||
|
| awk -F: '$1 == "fpr" { print $10; exit }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fails without touching anything when the pinned key is missing, unreadable,
|
||||||
|
# or not the key this repository says it is. Everything below assumes it passed.
|
||||||
|
chatgpt_verify_pinned_key() {
|
||||||
|
local found
|
||||||
|
if ! command -v gpg >/dev/null 2>&1; then
|
||||||
|
printf 'gpg is missing, so the pinned ChatGPT signing key cannot be verified.\n' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! found="$(chatgpt_pinned_fingerprint)"; then
|
||||||
|
printf 'The pinned ChatGPT signing key is missing: %s\n' "$(chatgpt_pinned_key)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$found" != "$CHATGPT_KEY_FINGERPRINT" ]]; then
|
||||||
|
printf 'The pinned ChatGPT signing key is %s, not the expected %s.\n' \
|
||||||
|
"${found:-unreadable}" "$CHATGPT_KEY_FINGERPRINT" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installs the verified key and the repository that names it, so the install
|
||||||
|
# after this one is a signature check rather than an act of faith.
|
||||||
|
#
|
||||||
|
# Takes the command that gets root, because the two callers ask for it
|
||||||
|
# differently: plain `sudo` from the installer, which authenticated once at the
|
||||||
|
# top of the run, and `panama-sudo --reason ...` from a migration, whose prompt
|
||||||
|
# has to say which repair it is for.
|
||||||
|
chatgpt_install_repository() {
|
||||||
|
local -a sudo_cmd=("$@")
|
||||||
|
(( ${#sudo_cmd[@]} > 0 )) || sudo_cmd=(sudo)
|
||||||
|
|
||||||
|
chatgpt_verify_pinned_key || return 1
|
||||||
|
|
||||||
|
"${sudo_cmd[@]}" install -D -m 0644 "$(chatgpt_pinned_key)" "$CHATGPT_KEY_FILE" || return 1
|
||||||
|
"${sudo_cmd[@]}" rpmkeys --import "$CHATGPT_KEY_FILE" || return 1
|
||||||
|
|
||||||
|
# Written here rather than left to the package's scriptlet, because the
|
||||||
|
# point of it is to exist -- with gpgcheck on and this key named -- before
|
||||||
|
# the first install rather than after it. Same base URL and same key the
|
||||||
|
# scriptlet writes, so it finds nothing to change later.
|
||||||
|
printf '%s\n' \
|
||||||
|
'[openai-chatgpt]' \
|
||||||
|
'name=ChatGPT' \
|
||||||
|
"baseurl=$CHATGPT_REPO_BASEURL" \
|
||||||
|
'enabled=1' \
|
||||||
|
'type=rpm-md' \
|
||||||
|
'gpgcheck=1' \
|
||||||
|
'repo_gpgcheck=1' \
|
||||||
|
"gpgkey=file://$CHATGPT_KEY_FILE" \
|
||||||
|
| "${sudo_cmd[@]}" tee "$CHATGPT_REPO_FILE" >/dev/null || return 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
| Key | Source URL | Expected primary fingerprint | Verification command |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Terra 44 | `https://repos.fyralabs.com/terra44/key.asc` | `AE09157A4DE88B497EA1D5D300CDAB43DE226D6F` | `gpg --batch --with-colons --import-options show-only --import terra44.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||||
|
| Anthropic Claude Code | `https://downloads.claude.ai/keys/claude-code.asc` | `31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE` | `gpg --batch --with-colons --import-options show-only --import claude-code.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||||
|
| Bun releases | `https://keys.openpgp.org/vks/v1/by-fingerprint/F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` | `F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` | `gpg --batch --with-colons --import-options show-only --import bun.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||||
|
| RPM Fusion free | `https://download1.rpmfusion.org/free/fedora/RPM-GPG-KEY-rpmfusion-free-fedora-2020` | `E9A491A3DE247814E7E067EAE06F8ECDD651FF2E` | `gpg --batch --with-colons --import-options show-only --import rpmfusion-free.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||||
|
| RPM Fusion nonfree | `https://download1.rpmfusion.org/nonfree/fedora/RPM-GPG-KEY-rpmfusion-nonfree-fedora-2020` | `79BDB88F9BBF73910FD4095B6A2AF96194843C65` | `gpg --batch --with-colons --import-options show-only --import rpmfusion-nonfree.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||||
|
| lionheartp/Hyprland COPR | `https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/pubkey.gpg` | `97E23476C89635135407C7D5E9BA41342C4B2995` | `gpg --batch --with-colons --import-options show-only --import hyprland-copr.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||||
|
| Flathub | `https://flathub.org/repo/flathub.flatpakrepo` | `6E5C05D979C76DAF93C081354184DD4D907A7CAE` | `awk -F= '/^GPGKey=/{print $2}' flathub.flatpakrepo \| base64 --decode \| gpg --batch --with-colons --import-options show-only --import \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||||
|
| Claude Desktop Extra | `https://patrickjaja.github.io/claude-desktop-extra/gpg-key.asc` | `825A7D15D78BABE45646D5DF382409F597908867` | `gpg --batch --with-colons --import-options show-only --import claude-desktop.asc \| awk -F: '$1 == "fpr" { print $10; exit }'` |
|
||||||
|
|
||||||
|
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, update the vendored key and
|
||||||
|
`installers.conf` together, refresh this retrieval record, and add a focused
|
||||||
|
contract case if the verification behavior changes. Until that review lands,
|
||||||
|
verification fails closed and preserves any known-good destination.
|
||||||
|
|
||||||
|
## Container-only Terra 44 signed-bootstrap proof
|
||||||
|
|
||||||
|
On 2026-08-27, a single disposable rootless Podman container proved the Terra
|
||||||
|
bootstrap path without changing the host package database, host keyring, or
|
||||||
|
host repository files. 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 exact host check reported the complete primary
|
||||||
|
fingerprint `AE09157A4DE88B497EA1D5D300CDAB43DE226D6F`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gpg --batch --with-colons --import-options show-only --import setup/provenance/keys/terra44.asc \
|
||||||
|
| awk -F: '$1 == "fpr" { print $10; exit }'
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the container the copied and installed key both had the recorded
|
||||||
|
SHA-256 before and after installation. `terra-release-44-9.noarch` was
|
||||||
|
installed. Its effective `terra` configuration reported `gpgcheck = 1`,
|
||||||
|
`pkg_gpgcheck = 1`, and `repo_gpgcheck = 1`; no GPG-bypass option was used.
|
||||||
|
The package's own `/etc/yum.repos.d/terra.repo` uses its Terra metalink and
|
||||||
|
`RPM-GPG-KEY-terra44`. That differs from Panama's deliberately staged local
|
||||||
|
key/base-URL file in `install-packages`, which replaces the release-generated
|
||||||
|
file only after this verified bootstrap step.
|
||||||
|
|
||||||
|
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`. The
|
||||||
|
same-container post-check independently confirmed the installed package and
|
||||||
|
effective signature settings above; no retry or second container was used.
|
||||||
@@ -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-----
|
||||||
+1085
-140
File diff suppressed because it is too large
Load Diff
+25
-23
@@ -11,12 +11,11 @@
|
|||||||
# this repository, not anybody's personal content, so a stranger who clones
|
# this repository, not anybody's personal content, so a stranger who clones
|
||||||
# Panama wants it for exactly the same reason its author does.
|
# Panama wants it for exactly the same reason its author does.
|
||||||
#
|
#
|
||||||
# ~/.claude/skills was a single symlink into user/agents/skills until now, and
|
# ~/.agents/skills and ~/.claude/skills may each start as a single symlink into
|
||||||
# a directory cannot be two things at once. So the destination becomes a real
|
# user/agents/skills, but a directory cannot point at personal and shipped
|
||||||
# directory and every skill -- shipped here, personal from user/ -- is linked
|
# skills at once. Both destinations become real directories with one link per
|
||||||
# into it one at a time. link-user runs after this stage on purpose: it links
|
# skill. link-user runs after this stage on purpose, so a personal skill named
|
||||||
# last, so a personal skill named like a shipped one wins, which is the
|
# like a shipped one wins in every agent runtime.
|
||||||
# precedence Claude Code itself uses.
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
|||||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
SKILLS_DIR="$PANAMA_PATH/skills"
|
SKILLS_DIR="$PANAMA_PATH/skills"
|
||||||
PANAMA_OLD="$PANAMA_PATH/config/old"
|
PANAMA_OLD="$PANAMA_PATH/config/old"
|
||||||
DESTINATION="$HOME/.claude/skills"
|
DESTINATIONS=("$HOME/.agents/skills" "$HOME/.claude/skills")
|
||||||
|
|
||||||
[[ -d "$SKILLS_DIR" ]] || { log "No skills/ in this checkout; nothing to link."; exit 0; }
|
[[ -d "$SKILLS_DIR" ]] || { log "No skills/ in this checkout; nothing to link."; exit 0; }
|
||||||
|
|
||||||
@@ -51,27 +50,30 @@ displace() {
|
|||||||
log "Moved existing $destination to $backup"
|
log "Moved existing $destination to $backup"
|
||||||
}
|
}
|
||||||
|
|
||||||
# The destination itself has to be a real directory before anything can be
|
# Each destination has to be a real directory before anything can be linked
|
||||||
# linked into it. An old whole-directory symlink is removed; a regular file
|
# into it. An old whole-directory symlink is removed; a regular file somebody
|
||||||
# somebody left at this path is kept, in config/old/.
|
# left at the path is kept in config/old/.
|
||||||
mkdir -p "$(dirname "$DESTINATION")"
|
for destination in "${DESTINATIONS[@]}"; do
|
||||||
if [[ -L "$DESTINATION" ]]; then
|
mkdir -p "$(dirname "$destination")"
|
||||||
rm -f "$DESTINATION"
|
if [[ -L "$destination" ]]; then
|
||||||
log "Removed the old $DESTINATION symlink; skills are linked one by one now"
|
rm -f "$destination"
|
||||||
elif [[ -e "$DESTINATION" && ! -d "$DESTINATION" ]]; then
|
log "Removed the old $destination symlink; skills are linked one by one now"
|
||||||
displace "$DESTINATION"
|
elif [[ -e "$destination" && ! -d "$destination" ]]; then
|
||||||
fi
|
displace "$destination"
|
||||||
mkdir -p "$DESTINATION"
|
fi
|
||||||
|
mkdir -p "$destination"
|
||||||
|
done
|
||||||
|
|
||||||
linked=0
|
linked=0
|
||||||
for skill in "$SKILLS_DIR"/*; do
|
for skill in "$SKILLS_DIR"/*; do
|
||||||
[[ -e "$skill" ]] || continue
|
[[ -e "$skill" ]] || continue
|
||||||
name="$(basename "$skill")"
|
name="$(basename "$skill")"
|
||||||
target="$DESTINATION/$name"
|
for destination in "${DESTINATIONS[@]}"; do
|
||||||
|
target="$destination/$name"
|
||||||
displace "$target"
|
displace "$target"
|
||||||
ln -s "$skill" "$target"
|
ln -s "$skill" "$target"
|
||||||
log "Linked skills/$name → $target"
|
log "Linked skills/$name → $target"
|
||||||
|
done
|
||||||
linked=$(( linked + 1 ))
|
linked=$(( linked + 1 ))
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ if [[ -d "$extensions_source" ]] && command -v npm >/dev/null 2>&1; then
|
|||||||
[[ -f "$extension/package.json" ]] || continue
|
[[ -f "$extension/package.json" ]] || continue
|
||||||
name="$(basename "$extension")"
|
name="$(basename "$extension")"
|
||||||
|
|
||||||
# Skip a build that would produce what is already there. `npm install`
|
# Skip a build that would produce what is already there. `npm ci`
|
||||||
# alone takes long enough to be worth not repeating on every re-run of
|
# alone takes long enough to be worth not repeating on every re-run of
|
||||||
# a stage that is otherwise nearly instant.
|
# a stage that is otherwise nearly instant.
|
||||||
built="$vicinae_data_dir/extensions/$name"
|
built="$vicinae_data_dir/extensions/$name"
|
||||||
@@ -91,7 +91,7 @@ if [[ -d "$extensions_source" ]] && command -v npm >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
printf 'Building Vicinae extension %s\n' "$name"
|
printf 'Building Vicinae extension %s\n' "$name"
|
||||||
if ! (cd "$extension" && npm install --silent >/dev/null 2>&1 && npm run build >/dev/null 2>&1); then
|
if ! (cd "$extension" && npm ci --silent >/dev/null 2>&1 && npm run build >/dev/null 2>&1); then
|
||||||
printf 'Vicinae extension %s did not build; skipping\n' "$name" >&2
|
printf 'Vicinae extension %s did not build; skipping\n' "$name" >&2
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -40,7 +40,21 @@ Other tools that beat raw commands:
|
|||||||
- `panama-launch --class '<regex>' -- cmd…` — focus the window if it exists, launch otherwise
|
- `panama-launch --class '<regex>' -- cmd…` — focus the window if it exists, launch otherwise
|
||||||
- `panama-sudo --reason "why" -- cmd…` — root work; load the `panama-sudo` skill first
|
- `panama-sudo --reason "why" -- cmd…` — root work; load the `panama-sudo` skill first
|
||||||
- `panama update` — bring the machine current (pull, repairs, unattended stages); asks nothing
|
- `panama update` — bring the machine current (pull, repairs, unattended stages); asks nothing
|
||||||
- `panama test --safe` — verify the desktop's contracts without hijacking the session
|
- `panama test --safe` — run only contracts classified as hermetic in
|
||||||
|
`tests/contracts.manifest`
|
||||||
|
|
||||||
|
For a new machine, use the complete verified bootstrap command in `README.md`.
|
||||||
|
Desktop and server installs use the same commit-addressed `boot` file and
|
||||||
|
SHA-256; server adds only `--server`. Do not replace that URL with a branch or
|
||||||
|
pipe its response into Bash. `boot` refuses missing or malformed pins, a boot
|
||||||
|
digest mismatch, and any dirty, divergent, fetch-failed, or mismatched existing
|
||||||
|
checkout. Use `panama update` after installation.
|
||||||
|
|
||||||
|
A plain `panama test` prompts in a terminal before it starts non-hermetic contracts. Automation
|
||||||
|
must grant each required capability with a repeatable `--allow`, such as
|
||||||
|
`panama test --allow live-host updates`. Each contract has a 180-second outer timeout by default,
|
||||||
|
which a positive `PANAMA_TEST_TIMEOUT_SECONDS` value overrides. Failures print captured stdout
|
||||||
|
and stderr. Successful stdout stays quiet. Successful stderr is surfaced as a warning.
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
# 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/chatgpt-package-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
|
STUB
|
||||||
chmod +x "$stub/codex"
|
chmod +x "$stub/codex"
|
||||||
|
|
||||||
mkdir -p "$work/codex/sessions/2026/08/25"
|
session_day="$(date +%Y/%m/%d)"
|
||||||
cat >"$work/codex/sessions/2026/08/25/session.jsonl" <<'JSONL'
|
session_date="$(date +%Y-%m-%d)"
|
||||||
{"type":"turn_context","payload":{"model":"gpt-5.6-sol"}}
|
session_offset="$(date +%:z)"
|
||||||
{"type":"token_count","timestamp":"2026-08-25T10:00:00Z","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":999999,"output_tokens":999999},"last_token_usage":{"input_tokens":1200,"cached_input_tokens":1000,"output_tokens":300}}}}
|
mkdir -p "$work/codex/sessions/$session_day"
|
||||||
{"type":"token_count","timestamp":"2026-08-25T10:05:00Z","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":1999999,"output_tokens":1999999},"last_token_usage":{"input_tokens":1200,"cached_input_tokens":1000,"output_tokens":300}}}}
|
{
|
||||||
JSONL
|
printf '%s\n' '{"type":"turn_context","payload":{"model":"gpt-5.6-sol"}}'
|
||||||
touch "$work/codex/sessions/2026/08/25/session.jsonl"
|
jq -nc --arg timestamp "${session_date}T10:00:00${session_offset}" \
|
||||||
|
'{type:"token_count",timestamp:$timestamp,payload:{type:"token_count",info:{total_token_usage:{input_tokens:999999,output_tokens:999999},last_token_usage:{input_tokens:1200,cached_input_tokens:1000,output_tokens:300}}}}'
|
||||||
|
jq -nc --arg timestamp "${session_date}T10:05:00${session_offset}" \
|
||||||
|
'{type:"token_count",timestamp:$timestamp,payload:{type:"token_count",info:{total_token_usage:{input_tokens:1999999,output_tokens:1999999},last_token_usage:{input_tokens:1200,cached_input_tokens:1000,output_tokens:300}}}}'
|
||||||
|
} >"$work/codex/sessions/$session_day/session.jsonl"
|
||||||
|
|
||||||
codex_record="$(
|
codex_record="$(
|
||||||
PATH="$stub:$PATH" \
|
PATH="$stub:$PATH" \
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ SHELL_WORDS='^(if|then|else|elif|fi|for|while|until|do|done|case|esac|in|functio
|
|||||||
# authselect is on the list for the same reason: it manages Fedora's PAM and
|
# authselect is on the list for the same reason: it manages Fedora's PAM and
|
||||||
# nsswitch profiles and arrives with fprintd-pam, realmd and nss-mdns, so the
|
# nsswitch profiles and arrives with fprintd-pam, realmd and nss-mdns, so the
|
||||||
# fingerprint aliases in config/bash can rely on it without declaring it.
|
# fingerprint aliases in config/bash can rely on it without declaring it.
|
||||||
BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|find|xargs|basename|dirname|mkdir|rm|cp|mv|ln|chmod|chown|stat|df|du|date|sleep|env|id|tee|touch|mktemp|readlink|realpath|seq|comm|join|paste|od|file|nl|fold|column|tput|timeout|flock|install|sha256sum|md5sum|base64|nproc|uptime|free|uname|hostname|whoami|ps|pgrep|pkill|kill|killall|lsblk|mount|umount|sudo|su|rpm|dnf|flatpak|git|python3|ss|ip|ls|rfkill|lsof|authselect|setsid|nohup|grub2-mkconfig|sysctl)$'
|
BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|find|xargs|basename|dirname|mkdir|rm|cp|mv|ln|chmod|chown|stat|df|du|date|sleep|env|id|tee|touch|mktemp|readlink|realpath|seq|comm|join|paste|od|file|nl|fold|column|tput|timeout|flock|install|tar|sha256sum|md5sum|base64|nproc|uptime|free|uname|hostname|whoami|ps|pgrep|pkill|kill|killall|lsblk|mount|umount|sudo|su|rpm|dnf|flatpak|git|python3|ss|ip|ls|rfkill|lsof|authselect|setsid|nohup|grub2-mkconfig|sysctl)$'
|
||||||
|
|
||||||
# bootctl and coredumpctl ship in systemd-udev, which every Fedora install
|
# bootctl and coredumpctl ship in systemd-udev, which every Fedora install
|
||||||
# carries -- it is the udev half of systemd, not an optional tool. Declaring
|
# carries -- it is the udev half of systemd, not an optional tool. Declaring
|
||||||
@@ -45,10 +45,10 @@ BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|fi
|
|||||||
SESSION='^(systemctl|busctl|journalctl|loginctl|hostnamectl|localectl|systemd-inhibit|systemd-run|udevadm|bootctl|coredumpctl|gsettings|dconf|dbus-send|dbus-monitor|hyprctl|qs|quickshell|gnf|panama|wl-copy|wl-paste)$'
|
SESSION='^(systemctl|busctl|journalctl|loginctl|hostnamectl|localectl|systemd-inhibit|systemd-run|udevadm|bootctl|coredumpctl|gsettings|dconf|dbus-send|dbus-monitor|hyprctl|qs|quickshell|gnf|panama|wl-copy|wl-paste)$'
|
||||||
|
|
||||||
# Installed by install-packages itself rather than by a package list. Two
|
# Installed by install-packages itself rather than by a package list. Two
|
||||||
# reasons, both deliberate: bun, claude and codex have no RPM or flatpak at
|
# reasons, both deliberate: bun and codex use reviewed release archives,
|
||||||
# all (codex comes through npm), and node, npm and pnpm come from nvm on
|
# Claude Code and pnpm use signed DNF repositories, and node/npm use a reviewed
|
||||||
# purpose -- a dnf nodejs earlier on PATH would win every per-project
|
# Node archive inside nvm's version directory. A system nodejs earlier on PATH
|
||||||
# `nvm use`, which is the whole point of having nvm.
|
# would win every per-project `nvm use`, which is the whole point of having nvm.
|
||||||
# Anything added here needs a matching install block and a stated reason.
|
# Anything added here needs a matching install block and a stated reason.
|
||||||
SELF_INSTALLED='^(bun|claude|codex|node|npm|pnpm)$'
|
SELF_INSTALLED='^(bun|claude|codex|node|npm|pnpm)$'
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,7 @@
|
|||||||
# slot from a real icon rather than assuming one -- a DockItem is taller
|
# slot from a real icon rather than assuming one -- a DockItem is taller
|
||||||
# than it is wide, so a constant is wrong on one of the two orientations.
|
# than it is wide, so a constant is wrong on one of the two orientations.
|
||||||
#
|
#
|
||||||
# The geometry checks launch isolated shells against a temporary config. The
|
# The geometry checks launch isolated shells against a temporary config.
|
||||||
# real settings are read to build them and never written.
|
|
||||||
|
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
@@ -47,8 +46,138 @@ for path in "$dock" "$body" "$strip"; do
|
|||||||
[[ -r "$path" ]] || fail "missing $path"
|
[[ -r "$path" ]] || fail "missing $path"
|
||||||
done
|
done
|
||||||
|
|
||||||
work="$(mktemp -d)"
|
# Keep this contract honest: it must never add a probe to the linked source
|
||||||
trap 'rm -rf "$work" "$shell_dir/dock-position-probe.qml"' EXIT
|
# tree, and every exit path must leave that tree byte-for-byte as it began.
|
||||||
|
source_probe="$shell_dir/dock-position-probe.qml"
|
||||||
|
source_probe_existed=0
|
||||||
|
source_probe_checksum=""
|
||||||
|
if [[ -e "$source_probe" ]]; then
|
||||||
|
source_probe_existed=1
|
||||||
|
source_probe_checksum="$(sha256sum -- "$source_probe")"
|
||||||
|
fi
|
||||||
|
source_tree_checksum() {
|
||||||
|
find "$shell_dir" -type f -print0 \
|
||||||
|
| LC_ALL=C sort -z \
|
||||||
|
| xargs -0 sha256sum -- \
|
||||||
|
| sha256sum
|
||||||
|
}
|
||||||
|
source_tree_initial_checksum="$(source_tree_checksum)"
|
||||||
|
|
||||||
|
assert_source_tree_unchanged() {
|
||||||
|
[[ "$(source_tree_checksum)" == "$source_tree_initial_checksum" ]] \
|
||||||
|
|| { printf 'dock position contract: the Quickshell source tree changed\n' >&2; return 1; }
|
||||||
|
|
||||||
|
if (( source_probe_existed )); then
|
||||||
|
[[ -e "$source_probe" && "$(sha256sum -- "$source_probe")" == "$source_probe_checksum" ]] \
|
||||||
|
|| { printf 'dock position contract: the source probe changed\n' >&2; return 1; }
|
||||||
|
else
|
||||||
|
[[ ! -e "$source_probe" ]] \
|
||||||
|
|| { printf 'dock position contract: the contract created a source probe\n' >&2; return 1; }
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture_pid=""
|
||||||
|
stop_fixture() {
|
||||||
|
[[ -n "$fixture_pid" ]] || return 0
|
||||||
|
if kill -0 "$fixture_pid" 2>/dev/null; then
|
||||||
|
kill -TERM "$fixture_pid" 2>/dev/null || true
|
||||||
|
wait "$fixture_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fixture_pid=""
|
||||||
|
}
|
||||||
|
cleanup() {
|
||||||
|
local status=$?
|
||||||
|
local fixture_cleanup='pass'
|
||||||
|
local source_probe_assertions='pass'
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
stop_fixture
|
||||||
|
if ! rm -rf -- "$work"; then
|
||||||
|
printf 'dock position contract: could not remove temporary fixture\n' >&2
|
||||||
|
fixture_cleanup='fail'
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
if ! assert_source_tree_unchanged; then
|
||||||
|
source_probe_assertions='fail'
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
if [[ -n "${DOCK_POSITION_CONTRACT_CLEANUP_PROOF:-}" ]]; then
|
||||||
|
printf 'fixture-cleanup=%s source-and-probe-assertions=%s\n' \
|
||||||
|
"$fixture_cleanup" "$source_probe_assertions" \
|
||||||
|
>"$DOCK_POSITION_CONTRACT_CLEANUP_PROOF" || status=1
|
||||||
|
fi
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! work="$(mktemp -d)"; then
|
||||||
|
fail 'could not create a temporary fixture'
|
||||||
|
fi
|
||||||
|
[[ -d "$work" ]] || fail 'could not create a temporary fixture'
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
fixture_shell="$work/quickshell"
|
||||||
|
|
||||||
|
# These checks are deliberately against this harness. A live settings read or
|
||||||
|
# source-tree probe means the contract itself is unsafe before it maps a panel.
|
||||||
|
probe_name='dock-position-probe.qml'
|
||||||
|
source_write_marker="$(printf 'cat >\"$shell_dir/%s\"' "$probe_name")"
|
||||||
|
home_dollar="$(printf '\044')"
|
||||||
|
home_tilde="$(printf '\176')"
|
||||||
|
home_marker="${home_dollar}HOME"
|
||||||
|
settings_store_spellings=(
|
||||||
|
"${home_dollar}HOME/.config/panama/settings.json"
|
||||||
|
"${home_dollar}{HOME}/.config/panama/settings.json"
|
||||||
|
"${home_tilde}/.config/panama/settings.json"
|
||||||
|
"${home_dollar}{XDG_CONFIG_HOME:-${home_dollar}HOME/.config}/panama/settings.json"
|
||||||
|
)
|
||||||
|
assert_no_real_settings_reference() {
|
||||||
|
local marker
|
||||||
|
for marker in "${settings_store_spellings[@]}"; do
|
||||||
|
grep -Fq "$marker" "$1" && return 1
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
run_settings_path_probes() {
|
||||||
|
local spelling
|
||||||
|
local number=0
|
||||||
|
for spelling in "${settings_store_spellings[@]}"; do
|
||||||
|
local fixture="$work/settings-path-$number"
|
||||||
|
printf 'settings=%s\n' "$spelling" >"$fixture"
|
||||||
|
assert_no_real_settings_reference "$fixture" \
|
||||||
|
&& fail "the real-settings guard accepted $spelling"
|
||||||
|
((number += 1))
|
||||||
|
done
|
||||||
|
local safe_fixture="$work/settings-path-safe"
|
||||||
|
printf 'XDG_CONFIG_HOME=%s/config\n' "$work" >"$safe_fixture"
|
||||||
|
assert_no_real_settings_reference "$safe_fixture" \
|
||||||
|
|| fail 'the real-settings guard rejected an isolated configuration'
|
||||||
|
}
|
||||||
|
run_pre_copy_cleanup_probe() {
|
||||||
|
[[ "${DOCK_POSITION_CONTRACT_SKIP_PRECOPY_PROBE:-}" == 1 ]] && return
|
||||||
|
|
||||||
|
local proof="$work/pre-copy-cleanup-proof"
|
||||||
|
if DOCK_POSITION_CONTRACT_INJECT_PRECOPY_FAILURE=1 \
|
||||||
|
DOCK_POSITION_CONTRACT_SKIP_PRECOPY_PROBE=1 \
|
||||||
|
DOCK_POSITION_CONTRACT_CLEANUP_PROOF="$proof" \
|
||||||
|
"$0" >"$work/pre-copy-output" 2>&1; then
|
||||||
|
fail 'the injected pre-copy failure did not fail'
|
||||||
|
fi
|
||||||
|
[[ -r "$proof" ]] \
|
||||||
|
|| fail 'the injected pre-copy failure did not leave cleanup proof'
|
||||||
|
grep -Fxq 'fixture-cleanup=pass source-and-probe-assertions=pass' "$proof" \
|
||||||
|
|| fail 'the injected pre-copy failure skipped cleanup or source/probe assertions'
|
||||||
|
}
|
||||||
|
unsafe_harness=()
|
||||||
|
grep -Fq "$source_write_marker" "$0" && unsafe_harness+=('writes its probe in the Quickshell source tree')
|
||||||
|
assert_no_real_settings_reference "$0" || unsafe_harness+=('reads the user configuration directory')
|
||||||
|
(( ${#unsafe_harness[@]} == 0 )) \
|
||||||
|
|| fail "unsafe harness: ${unsafe_harness[*]}"
|
||||||
|
run_settings_path_probes
|
||||||
|
|
||||||
|
[[ "${DOCK_POSITION_CONTRACT_INJECT_PRECOPY_FAILURE:-}" != 1 ]] \
|
||||||
|
|| fail 'injected pre-copy failure'
|
||||||
|
run_pre_copy_cleanup_probe
|
||||||
|
|
||||||
|
cp -a "$shell_dir/." "$fixture_shell/" \
|
||||||
|
|| fail 'could not copy the Quickshell fixture'
|
||||||
|
|
||||||
# ── 3, 4, 6. What can be read ───────────────────────────────────────────────
|
# ── 3, 4, 6. What can be read ───────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -111,7 +240,8 @@ PY
|
|||||||
|
|
||||||
# ── 1, 2, 7. Geometry, measured ────────────────────────────────────────────────
|
# ── 1, 2, 7. Geometry, measured ────────────────────────────────────────────────
|
||||||
|
|
||||||
cat >"$shell_dir/dock-position-probe.qml" <<'QML'
|
probe="$fixture_shell/$probe_name"
|
||||||
|
cat >"$probe" <<'QML'
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.modules.dock
|
import qs.modules.dock
|
||||||
@@ -164,28 +294,50 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
QML
|
QML
|
||||||
|
|
||||||
settings_source="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json"
|
|
||||||
[[ -r "$settings_source" ]] || fail 'no settings to build a probe configuration from'
|
|
||||||
|
|
||||||
measure() {
|
measure() {
|
||||||
local position="$1"
|
local position="$1"
|
||||||
python3 - "$settings_source" "$work/panama/settings.json" "$position" <<'PY'
|
python3 - "$work/config/panama/settings.json" "$position" <<'PY'
|
||||||
import json, pathlib, sys
|
import json, pathlib, sys
|
||||||
data = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
out = pathlib.Path(sys.argv[1])
|
||||||
data["dockPosition"] = sys.argv[3]
|
data = {
|
||||||
out = pathlib.Path(sys.argv[2])
|
"schemaVersion": 1,
|
||||||
|
"dockAutohide": True,
|
||||||
|
"dockPosition": sys.argv[2],
|
||||||
|
"dockScreens": [],
|
||||||
|
"dockIconSize": 48,
|
||||||
|
"dockRevealDelayMs": 0,
|
||||||
|
"dockHideDelayMs": 250,
|
||||||
|
"dockPinned": [],
|
||||||
|
}
|
||||||
out.parent.mkdir(parents=True, exist_ok=True)
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
out.write_text(json.dumps(data))
|
out.write_text(json.dumps(data) + "\n")
|
||||||
PY
|
PY
|
||||||
( cd "$shell_dir" && XDG_CONFIG_HOME="$work" timeout 40 qs -p ./dock-position-probe.qml 2>&1 ) \
|
local output="$work/$position.output"
|
||||||
| grep -oE '(DOCKGEOM|MASKGEOM) .*'
|
(
|
||||||
|
cd "$fixture_shell" \
|
||||||
|
&& XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" \
|
||||||
|
exec timeout --foreground --kill-after=5s 40s qs -p "./$probe_name"
|
||||||
|
) >"$output" 2>&1 &
|
||||||
|
fixture_pid=$!
|
||||||
|
if ! wait "$fixture_pid"; then
|
||||||
|
fixture_pid=""
|
||||||
|
cat "$output" >&2
|
||||||
|
fail "the $position fixture did not exit cleanly"
|
||||||
|
fi
|
||||||
|
fixture_pid=""
|
||||||
|
geometry="$(grep -oE '(DOCKGEOM|MASKGEOM) .*' "$output")" \
|
||||||
|
|| fail "the $position fixture produced no geometry"
|
||||||
}
|
}
|
||||||
|
|
||||||
bottom="$(measure bottom)"
|
geometry=""
|
||||||
|
measure bottom
|
||||||
|
bottom="$geometry"
|
||||||
[[ -n "$bottom" ]] || fail 'the bottom dock produced no geometry at all'
|
[[ -n "$bottom" ]] || fail 'the bottom dock produced no geometry at all'
|
||||||
left="$(measure left)"
|
measure left
|
||||||
|
left="$geometry"
|
||||||
[[ -n "$left" ]] || fail 'the left dock produced no geometry at all'
|
[[ -n "$left" ]] || fail 'the left dock produced no geometry at all'
|
||||||
right="$(measure right)"
|
measure right
|
||||||
|
right="$geometry"
|
||||||
[[ -n "$right" ]] || fail 'the right dock produced no geometry at all'
|
[[ -n "$right" ]] || fail 'the right dock produced no geometry at all'
|
||||||
|
|
||||||
# The span checks read the window; the region checks read every line.
|
# The span checks read the window; the region checks read every line.
|
||||||
|
|||||||
@@ -25,7 +25,34 @@ fail() {
|
|||||||
|
|
||||||
[[ -x "$generator" ]] || fail 'the generator is missing or not executable'
|
[[ -x "$generator" ]] || fail 'the generator is missing or not executable'
|
||||||
|
|
||||||
|
scratch="$(mktemp -d /tmp/panama-docs.XXXXXX)"
|
||||||
|
trap 'rm -rf "$scratch"' EXIT
|
||||||
|
git -C "$repo_dir" diff -- docs/settings.md >"$scratch/docs.diff.before" \
|
||||||
|
|| fail 'could not capture the initial docs/settings.md state'
|
||||||
|
|
||||||
|
assert_doc_unchanged() {
|
||||||
|
git -C "$repo_dir" diff -- docs/settings.md >"$scratch/docs.diff.after" \
|
||||||
|
&& cmp -s "$scratch/docs.diff.before" "$scratch/docs.diff.after"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
status=$?
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
if ! assert_doc_unchanged; then
|
||||||
|
printf 'settings docs contract: the contract changed tracked docs/settings.md\n' >&2
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
rm -rf "$scratch"
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap 'exit 129' HUP
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
|
||||||
"$generator" --check || fail 'docs/settings.md is stale -- run quickshell/scripts/panama-settings-docs and commit the result'
|
"$generator" --check || fail 'docs/settings.md is stale -- run quickshell/scripts/panama-settings-docs and commit the result'
|
||||||
|
assert_doc_unchanged || fail 'the default --check changed tracked docs/settings.md'
|
||||||
|
|
||||||
# A generator that silently emitted nothing would also pass --check against an
|
# A generator that silently emitted nothing would also pass --check against an
|
||||||
# equally empty file, so the output is checked for substance too.
|
# equally empty file, so the output is checked for substance too.
|
||||||
@@ -47,14 +74,21 @@ grep -q 'cursor:zoom_factor' "$doc" \
|
|||||||
|
|
||||||
# A stale copy must be detectable, not merely regenerable. Prove the check
|
# A stale copy must be detectable, not merely regenerable. Prove the check
|
||||||
# actually compares content rather than always returning success.
|
# actually compares content rather than always returning success.
|
||||||
scratch="$(mktemp -d /tmp/panama-docs.XXXXXX)"
|
|
||||||
trap 'rm -rf "$scratch"' EXIT
|
|
||||||
cp "$doc" "$scratch/settings.md"
|
cp "$doc" "$scratch/settings.md"
|
||||||
printf '\n<!-- drift -->\n' >>"$doc"
|
printf '\n<!-- drift -->\n' >>"$scratch/settings.md"
|
||||||
if "$generator" --check >/dev/null 2>&1; then
|
if "$generator" --check --output "$scratch/settings.md" >/dev/null 2>&1; then
|
||||||
cp "$scratch/settings.md" "$doc"
|
|
||||||
fail '--check reported success on a modified file, so staleness would never be caught'
|
fail '--check reported success on a modified file, so staleness would never be caught'
|
||||||
fi
|
fi
|
||||||
cp "$scratch/settings.md" "$doc"
|
assert_doc_unchanged || fail '--check --output changed tracked docs/settings.md'
|
||||||
|
|
||||||
|
# An explicit relative destination belongs to the caller's working directory,
|
||||||
|
# not the repository root.
|
||||||
|
(
|
||||||
|
cd "$scratch" || exit 1
|
||||||
|
"$generator" --output generated.md >/dev/null
|
||||||
|
) || fail 'a relative --output path did not resolve from the current directory'
|
||||||
|
assert_doc_unchanged || fail '--output changed tracked docs/settings.md'
|
||||||
|
cmp -s "$doc" "$scratch/generated.md" \
|
||||||
|
|| fail 'generation through a relative --output path produced different documentation'
|
||||||
|
|
||||||
printf 'settings docs contract: PASS (%d settings documented)\n' "$settings"
|
printf 'settings docs contract: PASS (%d settings documented)\n' "$settings"
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
# instead of being handed whole to a Text element and to anybody reading
|
# instead of being handed whole to a Text element and to anybody reading
|
||||||
# over a shoulder.
|
# over a shoulder.
|
||||||
#
|
#
|
||||||
# Runs entirely against a temporary config home. The real settings store is read
|
# Runs entirely against temporary config homes: one explicit source fixture and
|
||||||
# for the export and never written.
|
# one isolated import destination. The real settings store is never consulted.
|
||||||
|
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
@@ -52,6 +52,29 @@ trap 'rm -rf "$work"' EXIT
|
|||||||
bundle="$work/export.json"
|
bundle="$work/export.json"
|
||||||
field() { python3 -c "import json,sys; print(json.load(sys.stdin)$1)"; }
|
field() { python3 -c "import json,sys; print(json.load(sys.stdin)$1)"; }
|
||||||
|
|
||||||
|
export XDG_CONFIG_HOME="$work/source-config"
|
||||||
|
source_settings="$XDG_CONFIG_HOME/panama/settings.json"
|
||||||
|
mkdir -p "$(dirname "$source_settings")"
|
||||||
|
python3 - "$source_settings" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
|
||||||
|
# A representative source store: every value that should travel is valid for
|
||||||
|
# its schema type, while the machine-only and unknown values prove the allow
|
||||||
|
# list does not export whatever else happens to be present.
|
||||||
|
fixture = {
|
||||||
|
"blurEnabled": False, # boolean
|
||||||
|
"gapsIn": 7, # integer
|
||||||
|
"vrrPolicy": 2, # numeric enum
|
||||||
|
"weatherLocation": "Fixture Harbor", # string
|
||||||
|
"displays": {"DP-9": "source-only"},
|
||||||
|
"lastPage": "appearance",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"someFutureToken": "Bearer fixture-secret",
|
||||||
|
}
|
||||||
|
with open(sys.argv[1], "w", encoding="utf-8") as target:
|
||||||
|
json.dump(fixture, target)
|
||||||
|
PY
|
||||||
|
|
||||||
# ── 1 & 2. Export carries taste, not hardware ───────────────────────────────
|
# ── 1 & 2. Export carries taste, not hardware ───────────────────────────────
|
||||||
|
|
||||||
"$helper" export "$bundle" >"$work/export-result.json" || fail 'export failed'
|
"$helper" export "$bundle" >"$work/export-result.json" || fail 'export failed'
|
||||||
@@ -61,7 +84,7 @@ reason="$(field "['error']" <"$work/export-result.json")"
|
|||||||
[[ "$(stat -c '%a' "$bundle")" == "600" ]] \
|
[[ "$(stat -c '%a' "$bundle")" == "600" ]] \
|
||||||
|| fail 'the export is readable by other accounts'
|
|| fail 'the export is readable by other accounts'
|
||||||
|
|
||||||
python3 - "$bundle" "$schema" "${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" <<'PY' || fail 'the export carried the wrong things, in one direction or the other'
|
python3 - "$bundle" "$schema" "$source_settings" <<'PY' || fail 'the export carried the wrong things, in one direction or the other'
|
||||||
import json, re, sys
|
import json, re, sys
|
||||||
bundle = json.load(open(sys.argv[1]))
|
bundle = json.load(open(sys.argv[1]))
|
||||||
schema = open(sys.argv[2]).read()
|
schema = open(sys.argv[2]).read()
|
||||||
@@ -105,6 +128,16 @@ PY
|
|||||||
# ── 3, 4. Arrival is validated per key, and the types are all covered ───────
|
# ── 3, 4. Arrival is validated per key, and the types are all covered ───────
|
||||||
|
|
||||||
export XDG_CONFIG_HOME="$work/config"
|
export XDG_CONFIG_HOME="$work/config"
|
||||||
|
destination_settings="$XDG_CONFIG_HOME/panama/settings.json"
|
||||||
|
mkdir -p "$(dirname "$destination_settings")"
|
||||||
|
python3 - "$destination_settings" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
|
||||||
|
# This valid preference is deliberately absent from the source fixture and
|
||||||
|
# therefore from the bundle. Import must merge around it rather than replace it.
|
||||||
|
with open(sys.argv[1], "w", encoding="utf-8") as target:
|
||||||
|
json.dump({"borderSize": 4}, target)
|
||||||
|
PY
|
||||||
|
|
||||||
python3 - "$bundle" "$work/tampered.json" <<'PY'
|
python3 - "$bundle" "$work/tampered.json" <<'PY'
|
||||||
import json, sys
|
import json, sys
|
||||||
@@ -208,18 +241,15 @@ PY
|
|||||||
|
|
||||||
# ── 5. Import merges rather than replaces ───────────────────────────────────
|
# ── 5. Import merges rather than replaces ───────────────────────────────────
|
||||||
|
|
||||||
python3 - "$work/config/panama/settings.json" <<'PY'
|
|
||||||
import json, sys
|
|
||||||
store = json.load(open(sys.argv[1]))
|
|
||||||
store["aSettingTheBundleNeverMentions"] = "kept"
|
|
||||||
json.dump(store, open(sys.argv[1], "w"))
|
|
||||||
PY
|
|
||||||
"$helper" import "$bundle" >/dev/null || fail 'second import failed'
|
"$helper" import "$bundle" >/dev/null || fail 'second import failed'
|
||||||
python3 - "$work/config/panama/settings.json" <<'PY' || fail 'import replaced the store instead of merging into it'
|
python3 - "$bundle" "$destination_settings" <<'PY' || fail 'import replaced the store instead of merging into it'
|
||||||
import json, sys
|
import json, sys
|
||||||
store = json.load(open(sys.argv[1]))
|
bundle = json.load(open(sys.argv[1]))["settings"]
|
||||||
if store.get("aSettingTheBundleNeverMentions") != "kept":
|
store = json.load(open(sys.argv[2]))
|
||||||
raise SystemExit('a setting the bundle did not mention was removed')
|
if "borderSize" in bundle:
|
||||||
|
raise SystemExit('the merge sentinel unexpectedly appeared in the import bundle')
|
||||||
|
if store.get("borderSize") != 4:
|
||||||
|
raise SystemExit('a schema-valid setting the bundle did not mention was removed')
|
||||||
PY
|
PY
|
||||||
|
|
||||||
applied="$("$helper" import "$bundle" | field "['applied']")"
|
applied="$("$helper" import "$bundle" | field "['applied']")"
|
||||||
|
|||||||
@@ -24,36 +24,47 @@ note() { findings+=("$1"); }
|
|||||||
|
|
||||||
[[ -d "$server_dir" ]] || { printf 'compose secrets contract: no server/ directory\n' >&2; exit 1; }
|
[[ -d "$server_dir" ]] || { printf 'compose secrets contract: no server/ directory\n' >&2; exit 1; }
|
||||||
|
|
||||||
# ── 1. Tracked content is clean ──────────────────────────────────────────────
|
# ── 1. Scanner fixtures and tracked content ──────────────────────────────────
|
||||||
#
|
|
||||||
# Only tracked files: the live .env a cutover briefly leaves in a service
|
|
||||||
# directory is exactly what the gitignore exists for, and flagging it here
|
|
||||||
# would punish the ignore for working.
|
|
||||||
|
|
||||||
while IFS= read -r file; do
|
scanner="$repo_dir/tests/server/scan-tracked-secrets.py"
|
||||||
path="$repo_dir/$file"
|
fixtures_dir="$repo_dir/tests/server/fixtures/secrets"
|
||||||
[[ -f "$path" ]] || continue
|
|
||||||
|
|
||||||
# A secret-bearing key with a literal value. ${VAR} interpolations, empty
|
if ! python3 "$scanner" "$fixtures_dir/clean" \
|
||||||
# values, the CHANGE_ME placeholder, and booleans (ALLOW_EMPTY_PASSWORD=yes
|
compose.yml .env.example README.md signature-near-misses.txt; then
|
||||||
# is a switch, not a credential) are the allowed shapes; anything else
|
note 'the clean secret-scanning fixture was rejected'
|
||||||
# after PASSWORD/SECRET/TOKEN/KEY is treated as a leak. Keys that merely
|
fi
|
||||||
# configure where a secret lives (a *_FILE path, a key NAME) are not
|
|
||||||
# values.
|
expect_leak() {
|
||||||
|
local fixture="$1"
|
||||||
|
local expected="$2"
|
||||||
|
local output
|
||||||
|
local status
|
||||||
|
|
||||||
|
output="$(python3 "$scanner" "$fixtures_dir/leaked" "$fixture" 2>&1)"
|
||||||
|
status=$?
|
||||||
|
if (( status != 1 )); then
|
||||||
|
note "the leaked $fixture fixture exited $status instead of 1"
|
||||||
|
elif [[ "$output" != "$expected" ]]; then
|
||||||
|
note "the leaked $fixture fixture reported '$output' instead of '$expected'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_leak compose.yml 'compose.yml:4: POSTGRES_PASSWORD'
|
||||||
|
expect_leak .env.example '.env.example:1: API_TOKEN'
|
||||||
|
expect_leak plain-list.yml 'plain-list.yml:4: API_TOKEN'
|
||||||
|
expect_leak quoted-mapping.yml 'quoted-mapping.yml:4: API_TOKEN'
|
||||||
|
expect_leak quoted-list.yml 'quoted-list.yml:4: API_TOKEN'
|
||||||
|
expect_leak pem-private-key.txt 'pem-private-key.txt:1: private key'
|
||||||
|
expect_leak anthropic-token.txt 'anthropic-token.txt:1: provider token'
|
||||||
|
expect_leak github-token.txt 'github-token.txt:1: provider token'
|
||||||
|
expect_leak slack-token.txt 'slack-token.txt:1: provider token'
|
||||||
|
|
||||||
|
mapfile -t tracked_server_files < <(git -C "$repo_dir" ls-files 'server/**' 'server/*')
|
||||||
|
if ! output="$(python3 "$scanner" "$repo_dir" "${tracked_server_files[@]}" 2>&1)"; then
|
||||||
while IFS= read -r hit; do
|
while IFS= read -r hit; do
|
||||||
note "$file looks like it carries a secret: ${hit%%[=:]*}"
|
[[ -n "$hit" ]] && note "$hit"
|
||||||
done < <(grep -inE '(password|secret|token|api_key|private_key|access_key)[a-z0-9_]*[[:space:]]*[:=]' "$path" 2>/dev/null \
|
done <<< "$output"
|
||||||
| grep -vE '[:=][[:space:]]*["'"'"']?(\$\{|CHANGE_ME|(true|false|yes|no|[01])["'"'"']?[[:space:]]*$|["'"'"']?[[:space:]]*$)' \
|
fi
|
||||||
| 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/')
|
|
||||||
|
|
||||||
# ── 2. The ignore still stands ───────────────────────────────────────────────
|
# ── 2. The ignore still stands ───────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
POSTGRES_PASSWORD=CHANGE_ME
|
||||||
|
API_TOKEN=
|
||||||
|
ALLOW_EMPTY_PASSWORD=yes
|
||||||
|
FEATURE_SECRET_ENABLED=false
|
||||||
|
PRIVATE_KEY_PATH=/run/secrets/private_key
|
||||||
|
TOKENIZER_MODEL=gpt2
|
||||||
|
PASSWORDLESS_PROVIDER=webauthn
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
This prose is not a Compose assignment: password: example.
|
||||||
|
It also says secret key without defining one.
|
||||||
|
The server README sentence that triggered the audit is reproduced here:
|
||||||
|
under `server/` carries anything that looks like a secret: this repository is
|
||||||
|
public, and the gitignore is a seatbelt, not the brakes.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
database:
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
|
||||||
|
API_TOKEN: ${API_TOKEN:-CHANGE_ME}
|
||||||
|
ALLOW_EMPTY_PASSWORD: yes
|
||||||
|
DATABASE_NAME: application
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-----BEGIN SYNTHETIC PUBLIC KEY-----
|
||||||
|
sk-ant-
|
||||||
|
ghp_0123456789ABCDEFGHI
|
||||||
|
xoxb-
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
API_TOKEN=fixture-should-be-rejected
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
sk-ant-SYNTHETIC_FIXTURE_TOKEN
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
database:
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: fixture-should-be-rejected
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ghp_0123456789ABCDEFGHIJ
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-----BEGIN SYNTHETIC PRIVATE KEY-----
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
application:
|
||||||
|
environment:
|
||||||
|
- API_TOKEN=fixture-should-be-rejected
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
application:
|
||||||
|
environment:
|
||||||
|
- "API_TOKEN=fixture-should-be-rejected"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
application:
|
||||||
|
environment:
|
||||||
|
"API_TOKEN": fixture-should-be-rejected
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
xoxb-SYNTHETIC_FIXTURE_TOKEN
|
||||||
@@ -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:]))
|
||||||
@@ -32,8 +32,6 @@ panama="$repo_dir/bin/panama"
|
|||||||
findings=()
|
findings=()
|
||||||
note() { findings+=("$1"); }
|
note() { findings+=("$1"); }
|
||||||
|
|
||||||
[[ -d "$apps_dir" ]] || { printf 'apps contract: no %s\n' "$apps_dir" >&2; exit 1; }
|
|
||||||
|
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
definitions=("$apps_dir"/*)
|
definitions=("$apps_dir"/*)
|
||||||
|
|
||||||
|
|||||||
+226
-53
@@ -1,18 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# The front door: `boot` is the script the README tells a fresh machine to
|
# `boot` is downloaded before the repository exists. It may hand off only
|
||||||
# curl, so it runs before anything else Panama ships -- including its own
|
# after both the downloaded script and the requested Git commit have been
|
||||||
# tests. What it must get right is small and worth pinning:
|
# verified. This fixture stubs Git and install inside a throwaway PANAMA_PATH;
|
||||||
#
|
# it never contacts the network or mutates the real checkout.
|
||||||
# * a machine without the clone gets one, from the documented URL, at
|
|
||||||
# PANAMA_PATH, and the install runs
|
|
||||||
# * a machine with the clone is not re-cloned -- the same command is the
|
|
||||||
# recovery command -- and a fast-forward failure does not stop the install
|
|
||||||
# * boot hands off to the clone's own install, with PANAMA_PATH exported,
|
|
||||||
# so a clone at a chosen location installs from that location
|
|
||||||
#
|
|
||||||
# Run against stub git and install in a throwaway PANAMA_PATH; nothing here
|
|
||||||
# touches the real clone or the network.
|
|
||||||
|
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
@@ -28,13 +19,15 @@ work="$(mktemp -d)"
|
|||||||
trap 'rm -rf "$work"' EXIT
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
|
||||||
calls="$work/calls"
|
calls="$work/calls"
|
||||||
|
state="$work/state"
|
||||||
stub_dir="$work/bin"
|
stub_dir="$work/bin"
|
||||||
clone_dir="$work/Panama"
|
clone_dir="$work/Panama"
|
||||||
mkdir -p "$stub_dir"
|
revision='0123456789abcdef0123456789abcdef01234567'
|
||||||
|
ancestor_revision='1111111111111111111111111111111111111111'
|
||||||
|
mismatched_revision='fedcba9876543210fedcba9876543210fedcba98'
|
||||||
|
boot_sha="$(sha256sum "$boot" | cut -d' ' -f1)"
|
||||||
|
mkdir -p "$stub_dir" "$state"
|
||||||
|
|
||||||
# The stub install records that it ran and what PANAMA_PATH it saw. The stub
|
|
||||||
# git records its arguments, and materializes a clone the way the real one
|
|
||||||
# would -- boot execs the clone's install, so the clone has to contain one.
|
|
||||||
cat >"$work/fake-install" <<STUB
|
cat >"$work/fake-install" <<STUB
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
printf 'install PANAMA_PATH=%s\n' "\${PANAMA_PATH:-unset}" >>"$calls"
|
printf 'install PANAMA_PATH=%s\n' "\${PANAMA_PATH:-unset}" >>"$calls"
|
||||||
@@ -44,56 +37,236 @@ chmod +x "$work/fake-install"
|
|||||||
cat >"$stub_dir/git" <<STUB
|
cat >"$stub_dir/git" <<STUB
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
printf 'git %s\n' "\$*" >>"$calls"
|
printf 'git %s\n' "\$*" >>"$calls"
|
||||||
if [[ "\$1" == "clone" ]]; then
|
mode="\$(<"$state/mode")"
|
||||||
mkdir -p "\$3/.git"
|
|
||||||
cp "$work/fake-install" "\$3/install"
|
case "\${1:-}" in
|
||||||
fi
|
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"
|
||||||
|
;;
|
||||||
|
*) exit 97 ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*) exit 97 ;;
|
||||||
|
esac
|
||||||
STUB
|
STUB
|
||||||
chmod +x "$stub_dir/git"
|
chmod +x "$stub_dir/git"
|
||||||
|
|
||||||
run_boot() {
|
configure_case() {
|
||||||
|
local mode="$1" head_revision="${2:-$revision}" status="${3:-}"
|
||||||
|
rm -rf "$clone_dir"
|
||||||
: >"$calls"
|
: >"$calls"
|
||||||
PATH="$stub_dir:$PATH" PANAMA_PATH="$clone_dir" bash "$boot" </dev/null >/dev/null 2>&1
|
printf '%s\n' "$mode" >"$state/mode"
|
||||||
|
printf '%s\n' "$head_revision" >"$state/head-revision"
|
||||||
|
printf '%s' "$status" >"$state/status"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── A machine without the clone ──────────────────────────────────────────────
|
configure_existing_case() {
|
||||||
|
configure_case "$@"
|
||||||
|
mkdir -p "$clone_dir/.git"
|
||||||
|
cp "$work/fake-install" "$clone_dir/install"
|
||||||
|
chmod +x "$clone_dir/install"
|
||||||
|
}
|
||||||
|
|
||||||
run_boot || note 'boot failed on a machine without the clone'
|
run_boot() {
|
||||||
|
local supplied_revision="$1" supplied_sha="$2"
|
||||||
|
local -a env_args=(
|
||||||
|
"PATH=$stub_dir:$PATH"
|
||||||
|
"PANAMA_PATH=$clone_dir"
|
||||||
|
)
|
||||||
|
[[ "$supplied_revision" == UNSET ]] \
|
||||||
|
&& env_args+=(-u PANAMA_BOOT_REVISION) \
|
||||||
|
|| env_args+=("PANAMA_BOOT_REVISION=$supplied_revision")
|
||||||
|
[[ "$supplied_sha" == UNSET ]] \
|
||||||
|
&& env_args+=(-u PANAMA_BOOT_SHA256) \
|
||||||
|
|| env_args+=("PANAMA_BOOT_SHA256=$supplied_sha")
|
||||||
|
env "${env_args[@]}" bash "$boot" </dev/null >/dev/null 2>&1
|
||||||
|
run_status=$?
|
||||||
|
}
|
||||||
|
|
||||||
grep -q "git clone https://git.gbrown.org/gib/Panama.git $clone_dir" "$calls" \
|
assert_no_git_or_install() {
|
||||||
|| note 'boot does not clone the documented repository to PANAMA_PATH'
|
local case_name="$1"
|
||||||
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|
if grep -qE '^(git|install) ' "$calls"; then
|
||||||
|| note 'boot does not hand off to the clone'\''s install with PANAMA_PATH exported'
|
note "$case_name reached Git or install"
|
||||||
|
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" \
|
for input_case in "${input_cases[@]}"; do
|
||||||
&& note 'boot re-clones over an existing checkout'
|
IFS='|' read -r name supplied_revision supplied_sha <<<"$input_case"
|
||||||
grep -q 'git -C .* pull --ff-only' "$calls" \
|
[[ "$supplied_revision" == REVISION ]] && supplied_revision="$revision"
|
||||||
|| note 'boot does not fast-forward an existing clone'
|
[[ "$supplied_sha" == BOOT_SHA ]] && supplied_sha="$boot_sha"
|
||||||
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|
configure_case validation-only
|
||||||
|| note 'boot does not run the install from an existing clone'
|
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 ──────────────────────────────────────────
|
# A fresh install fetches only the requested commit, verifies checked-out HEAD,
|
||||||
#
|
# creates the tracked local main branch, and hands off.
|
||||||
# pull --ff-only refusing is normal life -- local commits, a rebase upstream.
|
configure_case fresh
|
||||||
# The command doubles as the repair path, so a refusal must be stepped over.
|
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'
|
# A fresh fetch failure stops before any checked-out HEAD or executable
|
||||||
#!/usr/bin/env bash
|
# handoff exists. The empty initialized Git directory is not a usable checkout.
|
||||||
[[ "$*" == *pull* ]] && exit 1
|
configure_case fresh-fetch-failure
|
||||||
exit 0
|
run_boot "$revision" "$boot_sha"
|
||||||
STUB
|
(( run_status != 0 )) || note 'fresh fetch failure returned success'
|
||||||
chmod +x "$stub_dir/git"
|
grep -qF "git -C $clone_dir fetch --depth=1 origin $revision" "$calls" \
|
||||||
|
|| note 'fresh fetch failure did not exercise the exact fetch'
|
||||||
: >"$calls"
|
assert_no_install_or_rewrite 'fresh fetch failure'
|
||||||
if ! PATH="$stub_dir:$PATH" PANAMA_PATH="$clone_dir" bash "$boot" </dev/null >/dev/null 2>&1; then
|
if grep -qE '^git .* (checkout|rev-parse)($| )' "$calls"; then
|
||||||
note 'a clone that cannot fast-forward stops the install instead of proceeding'
|
note 'fresh fetch failure reported or materialized a checked-out HEAD'
|
||||||
fi
|
fi
|
||||||
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|
if [[ -x "$clone_dir/install" || -e "$clone_dir/.git/HEAD" \
|
||||||
|| note 'the install does not run when the fast-forward is refused'
|
|| -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'
|
||||||
|
|
||||||
if (( ${#findings[@]} > 0 )); then
|
if (( ${#findings[@]} > 0 )); then
|
||||||
printf 'boot contract: %d finding(s)\n' "${#findings[@]}" >&2
|
printf 'boot contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||||
|
|||||||
Executable
+195
@@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# The one download that gets to run as root, and how it earns that.
|
||||||
|
#
|
||||||
|
# OpenAI publishes no signing key and no fingerprint that a first install could
|
||||||
|
# fetch and compare against: the documented instructions are to download an RPM
|
||||||
|
# and install it, and that RPM's own root scriptlet is what decides afterwards
|
||||||
|
# which repository and which key the machine will trust. Panama pins the key
|
||||||
|
# instead -- setup/keys/ carries it, setup/lib/chatgpt-package verifies the copy
|
||||||
|
# and writes the repository -- so dnf checks a signature before root sees a byte
|
||||||
|
# of it.
|
||||||
|
#
|
||||||
|
# What must hold:
|
||||||
|
#
|
||||||
|
# 1. The pinned key is the key the library says it is. Everything else here
|
||||||
|
# is worthless if this drifts, and a changed key must be a failing test
|
||||||
|
# somebody reads rather than a quiet change of publisher.
|
||||||
|
# 2. A pinned key that is missing, unreadable, or simply not that key stops
|
||||||
|
# the install and leaves the machine untouched. Failing closed is the
|
||||||
|
# whole point; falling back to installing anyway would be worse than
|
||||||
|
# never having checked.
|
||||||
|
# 3. What it writes actually enforces the check: gpgcheck and repo_gpgcheck
|
||||||
|
# on, and the gpgkey pointing at the key it just installed.
|
||||||
|
# 4. Both callers go through it, and neither hands root a downloaded RPM.
|
||||||
|
# The installer and the codex-desktop migration install `chatgpt` by name
|
||||||
|
# from that repository, which is what makes the signature mandatory.
|
||||||
|
#
|
||||||
|
# Hermetic: the key file is read locally, root is a stub that records what it
|
||||||
|
# was asked to do, and the destinations are redirected into a temporary
|
||||||
|
# directory. Nothing here contacts OpenAI or touches /etc.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
library="$repo_dir/setup/lib/chatgpt-package"
|
||||||
|
installer="$repo_dir/setup/scripts/install-packages"
|
||||||
|
migration="$repo_dir/migrations/1787804505.sh"
|
||||||
|
pinned_key="$repo_dir/setup/keys/RPM-GPG-KEY-chatgpt"
|
||||||
|
|
||||||
|
findings=()
|
||||||
|
note() { findings+=("$1"); }
|
||||||
|
|
||||||
|
[[ -r "$library" ]] || {
|
||||||
|
printf 'chatgpt package contract: %s is missing\n' "$library" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
command -v gpg >/dev/null 2>&1 || {
|
||||||
|
printf 'chatgpt package contract: gpg is required to read the pinned key\n' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
|
||||||
|
# Root, as a recording stub. It logs the command and then runs it for real,
|
||||||
|
# which is safe because every destination below is redirected into $work.
|
||||||
|
stub="$work/bin"
|
||||||
|
mkdir -p "$stub"
|
||||||
|
cat >"$stub/sudo" <<'STUB'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' "$*" >>"$SUDO_RECORD"
|
||||||
|
exec "$@"
|
||||||
|
STUB
|
||||||
|
cat >"$stub/rpmkeys" <<'STUB'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' "$*" >>"$RPMKEYS_RECORD"
|
||||||
|
STUB
|
||||||
|
chmod +x "$stub/sudo" "$stub/rpmkeys"
|
||||||
|
export PATH="$stub:$PATH"
|
||||||
|
|
||||||
|
# ── 1. The pinned key is the pinned key ─────────────────────────────────────
|
||||||
|
|
||||||
|
if [[ ! -r "$pinned_key" ]]; then
|
||||||
|
note 'setup/keys/RPM-GPG-KEY-chatgpt is missing, so nothing can be verified'
|
||||||
|
else
|
||||||
|
declared="$(grep -oP '(?<=^CHATGPT_KEY_FINGERPRINT=")[0-9A-F]+' "$library" | head -1)"
|
||||||
|
actual="$(gpg --show-keys --with-colons "$pinned_key" 2>/dev/null \
|
||||||
|
| awk -F: '$1 == "fpr" { print $10; exit }')"
|
||||||
|
[[ -n "$declared" ]] \
|
||||||
|
|| note 'the library pins no fingerprint, so any key file would be accepted'
|
||||||
|
[[ -n "$actual" ]] \
|
||||||
|
|| note 'the pinned key file does not parse as a public key'
|
||||||
|
[[ "$declared" == "$actual" ]] \
|
||||||
|
|| note "the pinned key is $actual but the library expects $declared"
|
||||||
|
grep -q 'RPM-GPG-KEY-chatgpt' "$repo_dir/setup/keys/README.md" 2>/dev/null \
|
||||||
|
|| note 'setup/keys/README.md does not record where the pinned key came from'
|
||||||
|
grep -q "$actual" "$repo_dir/setup/keys/README.md" 2>/dev/null \
|
||||||
|
|| note 'setup/keys/README.md records a fingerprint other than the key it ships'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# One attempt, against redirected destinations and a recording root. Every
|
||||||
|
# variable the library exposes is set here rather than in the caller's shell,
|
||||||
|
# so a case cannot leak into the next one.
|
||||||
|
attempt() {
|
||||||
|
local dir="$1" panama_path="$2" fingerprint="${3:-}"
|
||||||
|
mkdir -p "$dir"
|
||||||
|
(
|
||||||
|
export SUDO_RECORD="$dir/sudo.log" RPMKEYS_RECORD="$dir/rpmkeys.log"
|
||||||
|
: >"$SUDO_RECORD"
|
||||||
|
: >"$RPMKEYS_RECORD"
|
||||||
|
PANAMA_PATH="$panama_path"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$library"
|
||||||
|
CHATGPT_KEY_FILE="$dir/pki/RPM-GPG-KEY-chatgpt"
|
||||||
|
CHATGPT_REPO_FILE="$dir/repos/chatgpt.repo"
|
||||||
|
mkdir -p "$dir/repos"
|
||||||
|
[[ -z "$fingerprint" ]] || CHATGPT_KEY_FINGERPRINT="$fingerprint"
|
||||||
|
chatgpt_install_repository sudo
|
||||||
|
) >"$dir/out" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 2. It fails closed ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# A checkout with no pinned key at all.
|
||||||
|
empty="$work/no-key"
|
||||||
|
mkdir -p "$empty/checkout/setup/keys"
|
||||||
|
attempt "$empty" "$empty/checkout" \
|
||||||
|
&& note 'a missing pinned key still established the repository'
|
||||||
|
[[ ! -e "$empty/repos/chatgpt.repo" ]] \
|
||||||
|
|| note 'a missing pinned key still wrote a repository file'
|
||||||
|
grep -qi 'missing' "$empty/out" \
|
||||||
|
|| note 'a missing pinned key does not say so'
|
||||||
|
|
||||||
|
# A key file that is not a key.
|
||||||
|
garbage="$work/garbage-key"
|
||||||
|
mkdir -p "$garbage/checkout/setup/keys"
|
||||||
|
printf 'not a key\n' >"$garbage/checkout/setup/keys/RPM-GPG-KEY-chatgpt"
|
||||||
|
attempt "$garbage" "$garbage/checkout" \
|
||||||
|
&& note 'an unreadable pinned key still established the repository'
|
||||||
|
[[ ! -e "$garbage/repos/chatgpt.repo" ]] \
|
||||||
|
|| note 'an unreadable pinned key still wrote a repository file'
|
||||||
|
|
||||||
|
# The real key, against a fingerprint that is not its own -- the shape a
|
||||||
|
# substituted publisher would take.
|
||||||
|
wrong="$work/wrong-fingerprint"
|
||||||
|
attempt "$wrong" "$repo_dir" '0000000000000000000000000000000000000000' \
|
||||||
|
&& note 'a key that does not match the pinned fingerprint was accepted'
|
||||||
|
[[ ! -e "$wrong/repos/chatgpt.repo" ]] \
|
||||||
|
|| note 'a fingerprint mismatch still wrote a repository file'
|
||||||
|
[[ ! -s "$wrong/rpmkeys.log" ]] \
|
||||||
|
|| note 'a fingerprint mismatch still imported the key into the rpm keyring'
|
||||||
|
|
||||||
|
# ── 3. What it writes enforces the check ────────────────────────────────────
|
||||||
|
|
||||||
|
good="$work/verified"
|
||||||
|
if ! attempt "$good" "$repo_dir"; then
|
||||||
|
note "the pinned key was rejected: $(cat "$good/out")"
|
||||||
|
else
|
||||||
|
repo_file="$good/repos/chatgpt.repo"
|
||||||
|
key_file="$good/pki/RPM-GPG-KEY-chatgpt"
|
||||||
|
|
||||||
|
cmp -s "$key_file" "$pinned_key" \
|
||||||
|
|| note 'the installed key is not the pinned key'
|
||||||
|
grep -q 'import' "$good/rpmkeys.log" \
|
||||||
|
|| note 'the verified key was never imported, so dnf has nothing to check against'
|
||||||
|
|
||||||
|
grep -qx 'gpgcheck=1' "$repo_file" \
|
||||||
|
|| note 'the repository does not set gpgcheck=1, so package signatures go unchecked'
|
||||||
|
grep -qx 'repo_gpgcheck=1' "$repo_file" \
|
||||||
|
|| note 'the repository does not set repo_gpgcheck=1, so the metadata goes unchecked'
|
||||||
|
grep -qx "gpgkey=file://$key_file" "$repo_file" \
|
||||||
|
|| note 'the repository does not point gpgkey at the key that was just installed'
|
||||||
|
grep -q 'baseurl=https://' "$repo_file" \
|
||||||
|
|| note 'the repository has no https base URL'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. Both callers go through it ───────────────────────────────────────────
|
||||||
|
|
||||||
|
for caller in "$installer" "$migration"; do
|
||||||
|
name="${caller#"$repo_dir"/}"
|
||||||
|
[[ -r "$caller" ]] || { note "$name is missing"; continue; }
|
||||||
|
|
||||||
|
grep -q 'setup/lib/chatgpt-package' "$caller" \
|
||||||
|
|| note "$name does not source the verified install library"
|
||||||
|
grep -q 'chatgpt_install_repository' "$caller" \
|
||||||
|
|| note "$name does not establish the verified repository before installing"
|
||||||
|
grep -qE 'dnf install -y chatgpt\b' "$caller" \
|
||||||
|
|| note "$name does not install chatgpt by name from that repository"
|
||||||
|
|
||||||
|
# The shape this contract exists to keep out: fetch an RPM, hand it to
|
||||||
|
# root, and let its scriptlet decide what the machine trusts afterwards.
|
||||||
|
grep -qE 'curl.*chatgpt.*\.rpm' "$caller" \
|
||||||
|
&& note "$name downloads a ChatGPT RPM instead of installing it from the verified repository"
|
||||||
|
grep -qE 'dnf install[^|]*\$\{?chatgpt_rpm' "$caller" \
|
||||||
|
&& note "$name installs a downloaded ChatGPT RPM as root"
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( ${#findings[@]} > 0 )); then
|
||||||
|
printf 'chatgpt package contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||||
|
printf ' - %s\n' "${findings[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'chatgpt package contract: PASS\n'
|
||||||
Executable
+200
@@ -0,0 +1,200 @@
|
|||||||
|
#!/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=()
|
||||||
|
# Byte order, exactly as the runner discovers them. A UTF-8 collation folds
|
||||||
|
# the punctuation away and reorders the pairs that differ only by `-` and
|
||||||
|
# `_`, so a manifest correct here would be wrong on a machine with a
|
||||||
|
# different LANG.
|
||||||
|
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__*' | LC_ALL=C sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_manifest() {
|
||||||
|
local candidate="$1"
|
||||||
|
local -n expected_contracts="$2"
|
||||||
|
# Byte order, for the same reason discover_contracts sorts in it.
|
||||||
|
local LC_ALL=C
|
||||||
|
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[@]}"
|
||||||
@@ -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-----
|
||||||
@@ -55,6 +55,7 @@ fi
|
|||||||
# ── The extension ────────────────────────────────────────────────────────────
|
# ── The extension ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
manifest="$extension/package.json"
|
manifest="$extension/package.json"
|
||||||
|
lockfile="$extension/package-lock.json"
|
||||||
if [[ ! -f "$manifest" ]]; then
|
if [[ ! -f "$manifest" ]]; then
|
||||||
note 'the panama-search extension has no manifest'
|
note 'the panama-search extension has no manifest'
|
||||||
else
|
else
|
||||||
@@ -76,6 +77,15 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$lockfile" ]]; then
|
||||||
|
note 'the panama-search extension has no package-lock.json, so npm ci cannot install a fresh clone'
|
||||||
|
elif git -C "$repo_dir" check-ignore -q "$lockfile" 2>/dev/null; then
|
||||||
|
note 'the panama-search package-lock.json is ignored, so a fresh clone cannot use npm ci'
|
||||||
|
elif ! git -C "$repo_dir" ls-files --error-unmatch -- \
|
||||||
|
'config/local/share/vicinae/extensions/panama-search/package-lock.json' >/dev/null 2>&1; then
|
||||||
|
note 'the panama-search package-lock.json is not tracked, so a fresh clone cannot use npm ci'
|
||||||
|
fi
|
||||||
|
|
||||||
# ── One engine, written twice ────────────────────────────────────────────────
|
# ── One engine, written twice ────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# The script command and the extension both have to know where a search goes.
|
# The script command and the extension both have to know where a search goes.
|
||||||
@@ -116,6 +126,47 @@ grep -q '/etc/profile.d/nvm.sh' "$stage" \
|
|||||||
git -C "$repo_dir" check-ignore -q "$extension/node_modules" 2>/dev/null \
|
git -C "$repo_dir" check-ignore -q "$extension/node_modules" 2>/dev/null \
|
||||||
|| note 'the extension node_modules is not gitignored'
|
|| note 'the extension node_modules is not gitignored'
|
||||||
|
|
||||||
|
# npm must honour the committed dependency graph. This disposable fixture
|
||||||
|
# simulates npm rejecting a mismatched lockfile, which must leave that lockfile
|
||||||
|
# untouched and keep the launcher stage nonfatal.
|
||||||
|
fixture_root="$(mktemp -d -t panama-vicinae-lock.XXXXXX)"
|
||||||
|
trap 'rm -rf -- "$fixture_root"' EXIT
|
||||||
|
mkdir -p "$fixture_root/config/local/share/vicinae/scripts" \
|
||||||
|
"$fixture_root/config/local/share/vicinae/extensions/panama-search/src" \
|
||||||
|
"$fixture_root/bin"
|
||||||
|
fixture_extension="$fixture_root/config/local/share/vicinae/extensions/panama-search"
|
||||||
|
cp -- "$manifest" "$fixture_extension/package.json"
|
||||||
|
cp -- "$lockfile" "$fixture_extension/package-lock.json"
|
||||||
|
cmp -s -- "$lockfile" "$fixture_extension/package-lock.json" \
|
||||||
|
|| note 'the fresh-clone fixture did not consume the repository package-lock.json'
|
||||||
|
sed -i 's/"dependencies": {/"dependencies": {"fixture-mismatch": "1.0.0",/' \
|
||||||
|
"$fixture_extension/package.json"
|
||||||
|
lockfile="$fixture_extension/package-lock.json"
|
||||||
|
lock_before="$fixture_root/package-lock.before"
|
||||||
|
cp -- "$lockfile" "$lock_before"
|
||||||
|
repository_lock_sha256="$(sha256sum -- "$lock_before" | awk '{ print $1 }')"
|
||||||
|
cat >"$fixture_root/bin/npm" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' "$*" >>"${NPM_LOG:?}"
|
||||||
|
[[ "${1:-}" == ci ]] || exit 64
|
||||||
|
[[ "$(sha256sum -- package-lock.json | awk '{ print $1 }')" == "${NPM_EXPECTED_LOCK_SHA256:?}" ]] || exit 65
|
||||||
|
exit 1
|
||||||
|
EOF
|
||||||
|
chmod +x "$fixture_root/bin/npm"
|
||||||
|
stage_status=0
|
||||||
|
stage_output="$(PATH="$fixture_root/bin:$PATH" PANAMA_PATH="$fixture_root" \
|
||||||
|
VICINAE_DATA_DIR="$fixture_root/vicinae-data" NPM_LOG="$fixture_root/npm.log" \
|
||||||
|
NPM_EXPECTED_LOCK_SHA256="$repository_lock_sha256" \
|
||||||
|
bash "$stage" 2>&1)" || stage_status=$?
|
||||||
|
[[ "$stage_status" -eq 0 ]] \
|
||||||
|
|| note "the Vicinae stage returned $stage_status for a lockfile mismatch instead of remaining nonfatal"
|
||||||
|
[[ "$(<"$fixture_root/npm.log")" == 'ci --silent' ]] \
|
||||||
|
|| note 'the Vicinae extension dependency command was not npm ci --silent'
|
||||||
|
[[ "$stage_output" == *'Vicinae extension panama-search did not build; skipping'* ]] \
|
||||||
|
|| note 'a nonzero Vicinae extension dependency install did not fail the extension build'
|
||||||
|
cmp -s -- "$lock_before" "$lockfile" \
|
||||||
|
|| note 'a rejected Vicinae lockfile mismatch changed package-lock.json'
|
||||||
|
|
||||||
# ── Report ───────────────────────────────────────────────────────────────────
|
# ── Report ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (( ${#findings[@]} > 0 )); then
|
if (( ${#findings[@]} > 0 )); then
|
||||||
|
|||||||
Executable
+1983
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,157 @@ panama="$repo_dir/bin/panama"
|
|||||||
|
|
||||||
findings=()
|
findings=()
|
||||||
note() { findings+=("$1"); }
|
note() { findings+=("$1"); }
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
trap 'rm -rf -- "$work"' EXIT
|
||||||
|
|
||||||
|
# ── Verified bootstrap command ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
if grep -qE 'bash[[:space:]]+<\(curl[^)]*/raw/branch/main/boot' "$readme"; then
|
||||||
|
note 'the README still executes the mutable main-branch bootstrap'
|
||||||
|
fi
|
||||||
|
if grep -q '/raw/branch/main/boot' "$readme"; then
|
||||||
|
note 'the README still names the mutable main-branch boot URL'
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_bootstrap_occurrences() {
|
||||||
|
local pattern="$1" expected="$2" explanation="$3" actual
|
||||||
|
actual="$(grep -cE -- "$pattern" "$readme")"
|
||||||
|
(( actual == expected )) || note "$explanation"
|
||||||
|
}
|
||||||
|
|
||||||
|
mapfile -t documented_commits < <(
|
||||||
|
sed -nE "s/^bootstrap_commit=['\"]?([0-9a-f]{40})['\"]?$/\1/p" "$readme" | sort -u
|
||||||
|
)
|
||||||
|
mapfile -t documented_shas < <(
|
||||||
|
sed -nE "s/^bootstrap_sha=['\"]?([0-9a-f]{64})['\"]?$/\1/p" "$readme" | sort -u
|
||||||
|
)
|
||||||
|
|
||||||
|
if (( ${#documented_commits[@]} != 1 )); then
|
||||||
|
note 'the README does not declare one full lowercase 40-hex bootstrap commit'
|
||||||
|
else
|
||||||
|
documented_commit="${documented_commits[0]}"
|
||||||
|
if ! git -C "$repo_dir" cat-file -e "$documented_commit^{commit}" 2>/dev/null; then
|
||||||
|
note 'the documented bootstrap commit does not resolve to a repository commit'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( ${#documented_shas[@]} != 1 )); then
|
||||||
|
note 'the README does not declare one lowercase 64-hex bootstrap SHA-256'
|
||||||
|
else
|
||||||
|
documented_sha="${documented_shas[0]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${documented_commit:-}" && -n "${documented_sha:-}" ]] \
|
||||||
|
&& [[ "$(git -C "$repo_dir" show "$documented_commit:boot" 2>/dev/null | sha256sum | cut -d' ' -f1)" != "$documented_sha" ]]; then
|
||||||
|
note 'the documented SHA-256 does not match boot in the documented commit'
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_bootstrap_occurrences '^bootstrap_commit=[0-9a-f]{40}$' 2 \
|
||||||
|
'the desktop and server commands do not declare the same full bootstrap commit'
|
||||||
|
require_bootstrap_occurrences '^bootstrap_sha=[0-9a-f]{64}$' 2 \
|
||||||
|
'the desktop and server commands do not declare the same full bootstrap SHA-256'
|
||||||
|
require_bootstrap_occurrences 'https://git\.gbrown\.org/gib/Panama/raw/commit/\$bootstrap_commit/boot' 2 \
|
||||||
|
'the desktop and server commands do not both use the commit-addressed boot URL'
|
||||||
|
require_bootstrap_occurrences '--connect-timeout 10' 2 \
|
||||||
|
'the desktop and server commands do not both use the 10-second connect timeout'
|
||||||
|
require_bootstrap_occurrences '--max-time 30' 2 \
|
||||||
|
'the desktop and server commands do not both use the 30-second total timeout'
|
||||||
|
require_bootstrap_occurrences '--max-filesize 262144' 2 \
|
||||||
|
'the desktop and server commands do not both use the 256 KiB response limit'
|
||||||
|
require_bootstrap_occurrences 'mktemp[[:space:]]+-d' 2 \
|
||||||
|
'the desktop and server commands do not both use a private temporary directory'
|
||||||
|
require_bootstrap_occurrences 'sha256sum[[:space:]]+-c' 2 \
|
||||||
|
'the desktop and server commands do not both verify with sha256sum -c'
|
||||||
|
require_bootstrap_occurrences 'PANAMA_BOOT_REVISION="?\$bootstrap_commit"?[[:space:]]+PANAMA_BOOT_SHA256="?\$bootstrap_sha"?' 2 \
|
||||||
|
'the desktop and server commands do not both pass the verified pins to boot'
|
||||||
|
grep -qE 'bash[[:space:]]+"?\$bootstrap"?([[:space:]]|$)' "$readme" \
|
||||||
|
|| note 'the README bootstrap does not execute the verified temporary file'
|
||||||
|
grep -qE 'bash[[:space:]]+"?\$bootstrap"?[[:space:]]+--server' "$readme" \
|
||||||
|
|| note 'the server bootstrap does not reuse the verified temporary file'
|
||||||
|
if grep -qE 'curl[^|]*\|[[:space:]]*(bash|sh)|bash[[:space:]]+<\(curl' "$readme"; then
|
||||||
|
note 'the README pipes a network response into a shell'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the two exact documented blocks with a successful download and a failing
|
||||||
|
# checksum. The Bash adapter records only the verified boot invocation; the
|
||||||
|
# contract itself uses /usr/bin/bash so the adapter cannot hide this behavior.
|
||||||
|
checksum_stub_dir="$work/checksum-bin"
|
||||||
|
checksum_boot_calls="$work/checksum-boot-calls"
|
||||||
|
mkdir -p "$checksum_stub_dir"
|
||||||
|
|
||||||
|
cat >"$checksum_stub_dir/curl" <<'STUB'
|
||||||
|
#!/usr/bin/bash
|
||||||
|
set -u
|
||||||
|
destination=""
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
case "$1" in
|
||||||
|
--output)
|
||||||
|
destination="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ -n "$destination" ]] || exit 97
|
||||||
|
printf 'tampered boot bytes\n' >"$destination"
|
||||||
|
STUB
|
||||||
|
chmod +x "$checksum_stub_dir/curl"
|
||||||
|
|
||||||
|
cat >"$checksum_stub_dir/sha256sum" <<'STUB'
|
||||||
|
#!/usr/bin/bash
|
||||||
|
[[ "${1:-}" == -c ]] || exit 97
|
||||||
|
exit 1
|
||||||
|
STUB
|
||||||
|
chmod +x "$checksum_stub_dir/sha256sum"
|
||||||
|
|
||||||
|
cat >"$checksum_stub_dir/bash" <<'STUB'
|
||||||
|
#!/usr/bin/bash
|
||||||
|
printf 'verified-boot %s\n' "$*" >>"$PANAMA_README_BOOT_CALLS"
|
||||||
|
exit 0
|
||||||
|
STUB
|
||||||
|
chmod +x "$checksum_stub_dir/bash"
|
||||||
|
|
||||||
|
mapfile -d $'\036' -t bootstrap_snippets < <(
|
||||||
|
awk '
|
||||||
|
/^```sh$/ { in_block = 1; block = ""; next }
|
||||||
|
/^```$/ && in_block {
|
||||||
|
if (block ~ /bootstrap_commit=/) printf "%s%c", block, 30
|
||||||
|
in_block = 0
|
||||||
|
next
|
||||||
|
}
|
||||||
|
in_block { block = block $0 "\n" }
|
||||||
|
' "$readme"
|
||||||
|
)
|
||||||
|
|
||||||
|
checksum_failure_stops_boot() {
|
||||||
|
local snippet="$1" status
|
||||||
|
: >"$checksum_boot_calls"
|
||||||
|
PATH="$checksum_stub_dir:/usr/bin:/bin" \
|
||||||
|
PANAMA_README_BOOT_CALLS="$checksum_boot_calls" \
|
||||||
|
/usr/bin/bash -c "$snippet" >/dev/null 2>&1
|
||||||
|
status=$?
|
||||||
|
(( status != 0 )) && [[ ! -s "$checksum_boot_calls" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( ${#bootstrap_snippets[@]} != 2 )); then
|
||||||
|
note 'the README does not contain exactly two executable verified bootstrap blocks'
|
||||||
|
else
|
||||||
|
bootstrap_labels=(desktop server)
|
||||||
|
for index in "${!bootstrap_snippets[@]}"; do
|
||||||
|
snippet="${bootstrap_snippets[$index]}"
|
||||||
|
label="${bootstrap_labels[$index]}"
|
||||||
|
if ! checksum_failure_stops_boot "$snippet"; then
|
||||||
|
note "the $label command invoked boot after checksum failure"
|
||||||
|
fi
|
||||||
|
|
||||||
|
weakened_snippet="${snippet//$'set -euo pipefail\n'/}"
|
||||||
|
if [[ "$weakened_snippet" == "$snippet" ]]; then
|
||||||
|
note "the $label command has no fail-closed shell control to test"
|
||||||
|
elif checksum_failure_stops_boot "$weakened_snippet"; then
|
||||||
|
note "the $label checksum assertion accepts removal of fail-closed shell control"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# ── The contract count ───────────────────────────────────────────────────────
|
# ── The contract count ───────────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
@@ -36,6 +187,86 @@ elif (( claimed != actual )); then
|
|||||||
note "the README says $claimed contracts; there are $actual"
|
note "the README says $claimed contracts; there are $actual"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Root server bootstrap ───────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# This path runs before the repository exists on a fresh VPS. Its safety
|
||||||
|
# properties need to be stated beside the public `boot --server` example, not
|
||||||
|
# inferred from the shell implementation or buried in a fixture.
|
||||||
|
|
||||||
|
bootstrap_doc="$(sed -n '/^That command also works from a brand-new VPS/,/^`install` asks/p' "$readme" | tr '\n' ' ')"
|
||||||
|
|
||||||
|
require_bootstrap_doc() {
|
||||||
|
local pattern="$1" explanation="$2"
|
||||||
|
grep -qiE "$pattern" <<<"$bootstrap_doc" || note "$explanation"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_bootstrap_probe_rejected() {
|
||||||
|
local name="$1" pattern="$2" weakened_doc="$3"
|
||||||
|
if grep -qiE "$pattern" <<<"$weakened_doc"; then
|
||||||
|
note "the $name assertion accepts its weakened documentation probe"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
target_key_requirement='target user owns[^.]*\.ssh[^.]*mode[^.]*([^0-9]|^)0700([^0-9]|$)[^.]*authorized_keys[^.]*mode[^.]*([^0-9]|^)0600([^0-9]|$)'
|
||||||
|
parseable_key_requirement='(every|each)[^.]*non-?comment[^.]*authorized_keys[^.]*(OpenSSH|ssh-keygen)[^.]*(parse|valid)|(OpenSSH|ssh-keygen)[^-]*parse[^.]*every[^.]*non-?comment'
|
||||||
|
hardening_continues_requirement='hardening[[:space:]]+is[[:space:]]+unavailable[^.]*without[^.]*verified[^.]*key[^.]*install[[:space:]]+continues[^.]*without[[:space:]]+(it|SSH[[:space:]]+hardening)'
|
||||||
|
atomic_dropin_requirement='atomic[[:space:]]+same-directory[[:space:]]+drop-in'
|
||||||
|
rollback_requirement='validation[^.]*reload[^.]*fail[^.]*(restor|rollback)[^.]*previous[[:space:]]+drop-in'
|
||||||
|
effective_policy_requirement='sshd -T[^.]*root[^.]*target|sshd -T[^.]*target[^.]*root'
|
||||||
|
|
||||||
|
require_bootstrap_doc "$target_key_requirement" \
|
||||||
|
'the root bootstrap docs do not require target-user ownership with exact 0700/0600 SSH modes'
|
||||||
|
require_bootstrap_doc "$parseable_key_requirement" \
|
||||||
|
'the root bootstrap docs do not require OpenSSH to parse every non-comment key entry'
|
||||||
|
require_bootstrap_doc "$hardening_continues_requirement" \
|
||||||
|
'the root bootstrap docs do not say bootstrap continues without unavailable SSH hardening'
|
||||||
|
require_bootstrap_doc 'sshd -t' \
|
||||||
|
'the root bootstrap docs do not name sshd -t validation'
|
||||||
|
require_bootstrap_doc "$effective_policy_requirement" \
|
||||||
|
'the root bootstrap docs do not name sshd -T checks for root and target contexts'
|
||||||
|
require_bootstrap_doc "$atomic_dropin_requirement" \
|
||||||
|
'the root bootstrap docs do not describe the atomic same-directory drop-in'
|
||||||
|
require_bootstrap_doc '00-panama\.conf' \
|
||||||
|
'the root bootstrap docs do not name the precedence-safe 00-panama.conf drop-in'
|
||||||
|
require_bootstrap_doc 'PermitRootLogin[^.]*no[^.]*PasswordAuthentication[^.]*no[^.]*KbdInteractiveAuthentication[^.]*no' \
|
||||||
|
'the root bootstrap docs do not state all three effective authentication denials'
|
||||||
|
require_bootstrap_doc 'detected (SSH )?unit.*reload|reload.*detected (SSH )?unit' \
|
||||||
|
'the root bootstrap docs do not describe reloading the detected SSH unit'
|
||||||
|
require_bootstrap_doc "$rollback_requirement" \
|
||||||
|
'the root bootstrap docs do not promise rollback on validation or reload failure'
|
||||||
|
require_bootstrap_doc 'fixture contracts.*(these|this) (path|branch)|fixture contracts.*test' \
|
||||||
|
'the root bootstrap docs do not limit proof to fixture contracts'
|
||||||
|
require_bootstrap_doc 'no real daemon reload.*panama test --safe|panama test --safe.*no real daemon reload' \
|
||||||
|
'the root bootstrap docs imply a live daemon reload under the safe suite'
|
||||||
|
|
||||||
|
# These prove the semantic assertions above reject the precise omissions they
|
||||||
|
# guard against. They mutate only the scoped documentation string; README.md
|
||||||
|
# itself remains the real input that must satisfy the contract.
|
||||||
|
assert_bootstrap_probe_rejected 'exact SSH modes' "$target_key_requirement" \
|
||||||
|
"${bootstrap_doc//0700/700}"
|
||||||
|
assert_bootstrap_probe_rejected 'target-user ownership' "$target_key_requirement" \
|
||||||
|
"${bootstrap_doc//target user owns/someone owns}"
|
||||||
|
weakened_key_doc="${bootstrap_doc//OpenSSH/text tooling}"
|
||||||
|
weakened_key_doc="${weakened_key_doc//ssh-keygen/text parser}"
|
||||||
|
assert_bootstrap_probe_rejected 'OpenSSH key parsing' "$parseable_key_requirement" \
|
||||||
|
"$weakened_key_doc"
|
||||||
|
assert_bootstrap_probe_rejected 'hardening availability' "$hardening_continues_requirement" \
|
||||||
|
"${bootstrap_doc//unavailable/available}"
|
||||||
|
assert_bootstrap_probe_rejected 'hardening continuation' "$hardening_continues_requirement" \
|
||||||
|
"${bootstrap_doc//continues/stops}"
|
||||||
|
assert_bootstrap_probe_rejected 'atomic drop-in' "$atomic_dropin_requirement" \
|
||||||
|
"${bootstrap_doc//atomic /}"
|
||||||
|
assert_bootstrap_probe_rejected 'rollback after failure' "$rollback_requirement" \
|
||||||
|
"${bootstrap_doc//restores /keeps }"
|
||||||
|
assert_bootstrap_probe_rejected 'rollback trigger' "$rollback_requirement" \
|
||||||
|
"${bootstrap_doc//fails/works}"
|
||||||
|
assert_bootstrap_probe_rejected 'effective target policy' "$effective_policy_requirement" \
|
||||||
|
"${bootstrap_doc//target/root}"
|
||||||
|
|
||||||
|
if grep -qiE 'merely writes? (the )?(SSH )?(drop-in|file)|reload failure.*ignored|ignores? .*reload failure' <<<"$bootstrap_doc"; then
|
||||||
|
note 'the root bootstrap docs weaken the transaction by treating the write or reload failure as harmless'
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Documented subcommands exist ─────────────────────────────────────────────
|
# ── Documented subcommands exist ─────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# A README listing a command the dispatcher does not have sends somebody to a
|
# A README listing a command the dispatcher does not have sends somebody to a
|
||||||
|
|||||||
Executable
+1428
File diff suppressed because it is too large
Load Diff
+41
-20
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# The manual this machine hands an agent.
|
# The manual this machine hands an agent.
|
||||||
#
|
#
|
||||||
# skills/ and .claude/skills/panama exist because an agent asked to do anything
|
# skills/ and .agents/skills/panama exist because an agent asked to do anything
|
||||||
# on a Panama desktop will otherwise infer it from the source and get half of it
|
# on a Panama desktop will otherwise infer it from the source and get half of it
|
||||||
# wrong. That only helps if what the skills say is true -- and a skill is worse
|
# wrong. That only helps if what the skills say is true -- and a skill is worse
|
||||||
# than no skill when it is stale, because an agent believes it verbatim and does
|
# than no skill when it is stale, because an agent believes it verbatim and does
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
# That is a convention on the prose -- name things exactly, in backticks --
|
# That is a convention on the prose -- name things exactly, in backticks --
|
||||||
# and it is how they should be written anyway.
|
# and it is how they should be written anyway.
|
||||||
# 3. The delivery works: link-skills is a stage, in the right place, and the
|
# 3. The delivery works: link-skills is a stage, in the right place, and the
|
||||||
# personal manifest hands ~/.claude/skills over to the linkdir kind.
|
# personal manifest hands both shared skill homes to the linkdir kind.
|
||||||
#
|
#
|
||||||
# Sections 1 and 2 report clearly and keep going when a skill is not written
|
# Sections 1 and 2 report clearly and keep going when a skill is not written
|
||||||
# yet, so this contract is useful while the skills are still being authored.
|
# yet, so this contract is useful while the skills are still being authored.
|
||||||
@@ -32,9 +32,21 @@ manifest="$repo_dir/user/manifest"
|
|||||||
findings=()
|
findings=()
|
||||||
note() { findings+=("$1"); }
|
note() { findings+=("$1"); }
|
||||||
|
|
||||||
# The three, and where each is delivered from. The two under skills/ are shipped
|
# Every directory under skills/ ships to every machine. The project-level
|
||||||
# to every machine; the third is project-level and needs no delivery at all.
|
# panama skill stays in this repository and needs no home-directory delivery.
|
||||||
SKILL_DIRS=(skills/panama-desktop skills/panama-sudo .claude/skills/panama)
|
SKILL_DIRS=()
|
||||||
|
for directory in "$repo_dir"/skills/*; do
|
||||||
|
[[ -d "$directory" ]] && SKILL_DIRS+=("${directory#"$repo_dir"/}")
|
||||||
|
done
|
||||||
|
SKILL_DIRS+=(.agents/skills/panama)
|
||||||
|
|
||||||
|
# The project skill has one agent-neutral source. Claude gets a compatibility
|
||||||
|
# symlink, while Codex and other Agent Skills readers use .agents directly.
|
||||||
|
[[ -L "$repo_dir/.claude/skills/panama" ]] \
|
||||||
|
|| note '.claude/skills/panama is not a compatibility symlink to the agent-neutral source'
|
||||||
|
[[ "$(readlink -f "$repo_dir/.claude/skills/panama" 2>/dev/null)" == \
|
||||||
|
"$(readlink -f "$repo_dir/.agents/skills/panama" 2>/dev/null)" ]] \
|
||||||
|
|| note '.claude and .agents resolve the project panama skill differently'
|
||||||
|
|
||||||
# ── 1. Each skill loads ─────────────────────────────────────────────────────
|
# ── 1. Each skill loads ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -54,7 +66,7 @@ for relative in "${SKILL_DIRS[@]}"; do
|
|||||||
present+=("$directory")
|
present+=("$directory")
|
||||||
|
|
||||||
# Frontmatter is the first --- delimited block, and a skill without one is
|
# Frontmatter is the first --- delimited block, and a skill without one is
|
||||||
# not a skill: Claude Code skips the directory entirely.
|
# not a skill: agent loaders skip the directory entirely.
|
||||||
frontmatter="$(awk 'NR==1 { if ($0 != "---") exit 1; next } $0 == "---" { exit } { print }' "$file")"
|
frontmatter="$(awk 'NR==1 { if ($0 != "---") exit 1; next } $0 == "---" { exit } { print }' "$file")"
|
||||||
if [[ -z "$frontmatter" ]]; then
|
if [[ -z "$frontmatter" ]]; then
|
||||||
note "$relative/SKILL.md does not open with a --- frontmatter block"
|
note "$relative/SKILL.md does not open with a --- frontmatter block"
|
||||||
@@ -207,12 +219,12 @@ else
|
|||||||
work="$(mktemp -d)"
|
work="$(mktemp -d)"
|
||||||
trap 'rm -rf "$work"' EXIT
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
|
||||||
# A checkout and a home of its own. Never the real ones: ~/.claude/skills on
|
# A checkout and a home of its own. Never the real ones: these are somebody's
|
||||||
# this machine is somebody's live agent setup, and a contract that broke it
|
# live agent setup, and a contract that broke them mid-session would be worse
|
||||||
# mid-session would be worse than the bug it was looking for.
|
# than the bug it was looking for.
|
||||||
checkout="$work/Panama"
|
checkout="$work/Panama"
|
||||||
home="$work/home"
|
home="$work/home"
|
||||||
mkdir -p "$checkout/setup/scripts" "$checkout/skills/shipped" "$home/.claude"
|
mkdir -p "$checkout/setup/scripts" "$checkout/skills/shipped" "$home/.claude" "$home/.agents"
|
||||||
cp "$linker" "$checkout/setup/scripts/link-skills"
|
cp "$linker" "$checkout/setup/scripts/link-skills"
|
||||||
printf 'a shipped skill\n' >"$checkout/skills/shipped/SKILL.md"
|
printf 'a shipped skill\n' >"$checkout/skills/shipped/SKILL.md"
|
||||||
|
|
||||||
@@ -220,6 +232,7 @@ else
|
|||||||
# symlink, which is what link-user used to leave here.
|
# symlink, which is what link-user used to leave here.
|
||||||
mkdir -p "$work/personal"
|
mkdir -p "$work/personal"
|
||||||
ln -s "$work/personal" "$home/.claude/skills"
|
ln -s "$work/personal" "$home/.claude/skills"
|
||||||
|
ln -s "$work/personal" "$home/.agents/skills"
|
||||||
|
|
||||||
run() { HOME="$home" PANAMA_PATH="$checkout" "$checkout/setup/scripts/link-skills" >"$work/log" 2>&1; }
|
run() { HOME="$home" PANAMA_PATH="$checkout" "$checkout/setup/scripts/link-skills" >"$work/log" 2>&1; }
|
||||||
|
|
||||||
@@ -231,22 +244,30 @@ else
|
|||||||
|| note 'link-skills left ~/.claude/skills a symlink, so nothing else can be linked into it'
|
|| note 'link-skills left ~/.claude/skills a symlink, so nothing else can be linked into it'
|
||||||
[[ -L "$home/.claude/skills/shipped" ]] \
|
[[ -L "$home/.claude/skills/shipped" ]] \
|
||||||
|| note 'link-skills did not link each shipped skill as a child of ~/.claude/skills'
|
|| note 'link-skills did not link each shipped skill as a child of ~/.claude/skills'
|
||||||
|
[[ -d "$home/.agents/skills" && ! -L "$home/.agents/skills" ]] \
|
||||||
|
|| note 'link-skills left ~/.agents/skills a symlink, so shipped and personal skills cannot coexist'
|
||||||
|
[[ -L "$home/.agents/skills/shipped" ]] \
|
||||||
|
|| note 'link-skills did not link each shipped skill as a child of ~/.agents/skills'
|
||||||
grep -q 'Agent skills: 1 linked' "$work/log" \
|
grep -q 'Agent skills: 1 linked' "$work/log" \
|
||||||
|| note 'link-skills does not report how many skills it linked'
|
|| note 'link-skills does not report how many skills it linked'
|
||||||
|
|
||||||
# A real directory at a shipped skill's name is somebody's work: it moves to
|
# A real directory at a shipped skill's name is somebody's work: it moves to
|
||||||
# config/old rather than being deleted, the same promise the other stages
|
# config/old rather than being deleted, the same promise the other stages
|
||||||
# make. A symlink is not, and must not accumulate there.
|
# make. A symlink is not, and must not accumulate there.
|
||||||
rm "$home/.claude/skills/shipped"
|
for skill_home in "$home/.claude/skills" "$home/.agents/skills"; do
|
||||||
mkdir -p "$home/.claude/skills/shipped"
|
rm -f "$skill_home/shipped"
|
||||||
printf 'installed by hand\n' >"$home/.claude/skills/shipped/SKILL.md"
|
mkdir -p "$skill_home/shipped"
|
||||||
mkdir -p "$home/.claude/skills/untouched"
|
printf 'installed by hand\n' >"$skill_home/shipped/SKILL.md"
|
||||||
|
mkdir -p "$skill_home/untouched"
|
||||||
|
done
|
||||||
|
|
||||||
run
|
run
|
||||||
grep -rq 'installed by hand' "$checkout/config/old" 2>/dev/null \
|
[[ "$(grep -rl 'installed by hand' "$checkout/config/old" 2>/dev/null | wc -l)" == 2 ]] \
|
||||||
|| note 'link-skills destroyed a real skill instead of moving it to config/old'
|
|| note 'link-skills did not preserve real skills from both agent homes'
|
||||||
[[ -d "$home/.claude/skills/untouched" ]] \
|
for skill_home in "$home/.claude/skills" "$home/.agents/skills"; do
|
||||||
|| note 'link-skills removed a skill it does not ship'
|
[[ -d "$skill_home/untouched" ]] \
|
||||||
|
|| note "link-skills removed an unshipped skill from $skill_home"
|
||||||
|
done
|
||||||
|
|
||||||
before="$(find "$checkout/config/old" | wc -l)"
|
before="$(find "$checkout/config/old" | wc -l)"
|
||||||
run
|
run
|
||||||
@@ -287,8 +308,8 @@ fi
|
|||||||
|
|
||||||
grep -qE '^\s*linkdir\s+agents/skills\s+~/\.claude/skills\s*$' "$manifest" \
|
grep -qE '^\s*linkdir\s+agents/skills\s+~/\.claude/skills\s*$' "$manifest" \
|
||||||
|| note 'the manifest does not use linkdir for ~/.claude/skills, so personal skills would replace the directory'
|
|| note 'the manifest does not use linkdir for ~/.claude/skills, so personal skills would replace the directory'
|
||||||
grep -qE '^\s*link\s+agents/skills\s+~/\.agents/skills\s*$' "$manifest" \
|
grep -qE '^\s*linkdir\s+agents/skills\s+~/\.agents/skills\s*$' "$manifest" \
|
||||||
|| note '~/.agents/skills is no longer a whole-directory link, and nothing else claims that path'
|
|| note 'the manifest does not use linkdir for ~/.agents/skills, so personal skills would replace shipped skills'
|
||||||
grep -q 'linkdir)' "$user_linker" \
|
grep -q 'linkdir)' "$user_linker" \
|
||||||
|| note 'link-user does not implement the linkdir kind the manifest asks for'
|
|| note 'link-user does not implement the linkdir kind the manifest asks for'
|
||||||
grep -q 'linkdir' "$repo_dir/user/README.md" \
|
grep -q 'linkdir' "$repo_dir/user/README.md" \
|
||||||
|
|||||||
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,35 @@ tmp="$(mktemp -d -t panama-update-contract.XXXXXX)"
|
|||||||
trap 'rm -rf "$tmp"' EXIT
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
STAGE_NAMES=(install-packages link-dotfiles link-skills link-user change-settings
|
STAGE_NAMES=(install-packages link-dotfiles link-skills link-user change-settings
|
||||||
link-vicinae-scripts setup-identity install-hardware)
|
link-vicinae-scripts setup-server link-server setup-identity
|
||||||
|
install-hardware)
|
||||||
|
|
||||||
|
copy_hash_inputs() {
|
||||||
|
local root="$1" source relative
|
||||||
|
while IFS= read -r -d '' source; do
|
||||||
|
relative="${source#"$repo_dir"/}"
|
||||||
|
mkdir -p "$(dirname "$root/$relative")"
|
||||||
|
cp -- "$source" "$root/$relative"
|
||||||
|
done < <(
|
||||||
|
find "$repo_dir/setup/packages" -maxdepth 1 -type f -print0
|
||||||
|
find "$repo_dir/setup/provenance" -type f -print0
|
||||||
|
)
|
||||||
|
mkdir -p "$root/setup/lib"
|
||||||
|
cp -- "$repo_dir/setup/lib/artifact-provenance" "$root/setup/lib/artifact-provenance"
|
||||||
|
}
|
||||||
|
|
||||||
# A PANAMA_PATH that looks enough like the real one for install to run, and
|
# A PANAMA_PATH that looks enough like the real one for install to run, and
|
||||||
# records what it was asked to do instead of doing it.
|
# records what it was asked to do instead of doing it.
|
||||||
build_fixture() {
|
build_fixture() {
|
||||||
local root="$1" packages_rc="${2:-0}"
|
local root="$1" packages_rc="${2:-0}" trust_rc="${3:-0}"
|
||||||
rm -rf "$root"
|
rm -rf "$root"
|
||||||
mkdir -p "$root/bin" "$root/setup/scripts" "$root/setup/packages" \
|
mkdir -p "$root/bin" "$root/setup/scripts" "$root/setup/packages" \
|
||||||
|
"$root/setup/lib" "$root/setup/provenance/keys" \
|
||||||
"$root/config/dot/quickshell/scripts"
|
"$root/config/dot/quickshell/scripts"
|
||||||
|
|
||||||
cp "$installer" "$root/install"
|
cp "$installer" "$root/install"
|
||||||
: >"$root/bin/ascii"
|
: >"$root/bin/ascii"
|
||||||
printf 'base-package\n' >"$root/setup/packages/base"
|
copy_hash_inputs "$root"
|
||||||
|
|
||||||
local stage
|
local stage
|
||||||
for stage in "${STAGE_NAMES[@]}"; do
|
for stage in "${STAGE_NAMES[@]}"; do
|
||||||
@@ -62,6 +78,10 @@ EOF
|
|||||||
# The one stage whose exit code the caller wants to control.
|
# The one stage whose exit code the caller wants to control.
|
||||||
cat >"$root/setup/scripts/install-packages" <<EOF
|
cat >"$root/setup/scripts/install-packages" <<EOF
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
if [[ "\${1:-}" == --trust-preflight ]]; then
|
||||||
|
printf 'trust-preflight\n' >>"\$PANAMA_RAN"
|
||||||
|
exit $trust_rc
|
||||||
|
fi
|
||||||
printf 'install-packages\n' >>"\$PANAMA_RAN"
|
printf 'install-packages\n' >>"\$PANAMA_RAN"
|
||||||
exit $packages_rc
|
exit $packages_rc
|
||||||
EOF
|
EOF
|
||||||
@@ -102,26 +122,50 @@ EOF
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
exit 0
|
exit 0
|
||||||
EOF
|
EOF
|
||||||
|
cat >"$root/shim/dnf" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'dnf-transaction\n' >>"$PANAMA_RAN"
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
for prerequisite in gum lspci mokutil fwupdmgr; do
|
||||||
|
ln -s gsettings "$root/shim/$prerequisite"
|
||||||
|
done
|
||||||
chmod +x "$root/shim"/*
|
chmod +x "$root/shim"/*
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run the fixture installer and echo what ran, one stage per line.
|
# Run the fixture installer and echo what ran, one stage per line.
|
||||||
run_install() {
|
run_install() {
|
||||||
local root="$1"; shift
|
local root="$1"; shift
|
||||||
|
local status=0
|
||||||
: >"$root/ran"
|
: >"$root/ran"
|
||||||
PATH="$root/shim:$PATH" \
|
PATH="$root/shim:$PATH" PANAMA_PATH="$root" PANAMA_RAN="$root/ran" \
|
||||||
PANAMA_PATH="$root" \
|
XDG_STATE_HOME="$root/state" bash "$root/install" "$@" \
|
||||||
PANAMA_RAN="$root/ran" \
|
>"$root/out" 2>&1 || status=$?
|
||||||
XDG_STATE_HOME="$root/state" \
|
|
||||||
bash "$root/install" "$@" >"$root/out" 2>&1
|
|
||||||
printf '%s' "$?" >"$root/rc"
|
|
||||||
cat "$root/ran"
|
cat "$root/ran"
|
||||||
|
return "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_hash() {
|
||||||
|
local root="$1"
|
||||||
|
sed -n '/^hash_packages() {/,/^}$/p' "$root/install" >"$root/hash-only"
|
||||||
|
printf 'set -uo pipefail\nhash_packages\n' >>"$root/hash-only"
|
||||||
|
PANAMA_PATH="$root" bash "$root/hash-only" 2>"$root/hash-only.err"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_hash_failure() {
|
||||||
|
local root="$1" description="$2" status=0 digest
|
||||||
|
digest="$(run_hash "$root")" || status=$?
|
||||||
|
[[ "$status" -ne 0 && -z "$digest" ]] \
|
||||||
|
|| note "$description produced a digest instead of failing closed"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 1. The interview never runs on an upgrade ────────────────────────────────
|
# ── 1. The interview never runs on an upgrade ────────────────────────────────
|
||||||
|
|
||||||
build_fixture "$tmp/a"
|
build_fixture "$tmp/a"
|
||||||
ran="$(run_install "$tmp/a" --upgrade)"
|
install_status=0
|
||||||
|
ran="$(run_install "$tmp/a" --upgrade)" || install_status=$?
|
||||||
|
[[ "$install_status" -eq 0 ]] \
|
||||||
|
|| note "initial install --upgrade failed with status $install_status"
|
||||||
|
|
||||||
if grep -qx 'interview' <<<"$ran"; then
|
if grep -qx 'interview' <<<"$ran"; then
|
||||||
note 'install --upgrade ran the interview, which is the whole regression this prevents'
|
note 'install --upgrade ran the interview, which is the whole regression this prevents'
|
||||||
@@ -129,7 +173,10 @@ fi
|
|||||||
|
|
||||||
# And the control: a real install must still ask.
|
# And the control: a real install must still ask.
|
||||||
build_fixture "$tmp/b"
|
build_fixture "$tmp/b"
|
||||||
ran_install="$(run_install "$tmp/b")"
|
install_status=0
|
||||||
|
ran_install="$(run_install "$tmp/b")" || install_status=$?
|
||||||
|
[[ "$install_status" -eq 0 ]] \
|
||||||
|
|| note "plain fixture install failed with status $install_status"
|
||||||
if ! grep -qx 'interview' <<<"$ran_install"; then
|
if ! grep -qx 'interview' <<<"$ran_install"; then
|
||||||
note 'a plain ./install no longer asks the interview, so a new machine is never configured'
|
note 'a plain ./install no longer asks the interview, so a new machine is never configured'
|
||||||
fi
|
fi
|
||||||
@@ -146,12 +193,28 @@ done
|
|||||||
|
|
||||||
# A stage added to STAGES without a decision about which path owns it shows up
|
# A stage added to STAGES without a decision about which path owns it shows up
|
||||||
# here, because this list is written down twice on purpose.
|
# here, because this list is written down twice on purpose.
|
||||||
mapfile -t declared < <(python3 - "$installer" <<'PY'
|
declared=()
|
||||||
import re, sys
|
if declared_output="$(python3 - "$installer" <<'PY'
|
||||||
line = next(l for l in open(sys.argv[1], encoding="utf-8") if l.startswith("STAGES="))
|
import re
|
||||||
print("\n".join(re.findall(r"[\w-]+", line)[1:]))
|
import sys
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for line in open(sys.argv[1], encoding="utf-8"):
|
||||||
|
match = re.match(r'^\s*STAGES=\((.*)\)\s*$', line)
|
||||||
|
if match and "$" not in match.group(1):
|
||||||
|
found = True
|
||||||
|
print("\n".join(re.findall(r"[\w-]+", match.group(1))))
|
||||||
|
if not found:
|
||||||
|
print("install has no STAGES assignment", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
PY
|
PY
|
||||||
)
|
)"; then
|
||||||
|
while IFS= read -r stage; do
|
||||||
|
[[ -n "$stage" ]] && declared+=("$stage")
|
||||||
|
done <<<"$declared_output"
|
||||||
|
else
|
||||||
|
note 'could not read the install STAGES assignment'
|
||||||
|
fi
|
||||||
for stage in "${declared[@]}"; do
|
for stage in "${declared[@]}"; do
|
||||||
printf '%s\n' "${STAGE_NAMES[@]}" | grep -qx "$stage" \
|
printf '%s\n' "${STAGE_NAMES[@]}" | grep -qx "$stage" \
|
||||||
|| note "install declares a stage this contract has never heard of: $stage"
|
|| note "install declares a stage this contract has never heard of: $stage"
|
||||||
@@ -159,96 +222,410 @@ done
|
|||||||
|
|
||||||
# ── 3. The packages hash gates the stage, and a failure does not record it ───
|
# ── 3. The packages hash gates the stage, and a failure does not record it ───
|
||||||
|
|
||||||
|
package_inputs=()
|
||||||
|
while IFS= read -r -d '' input; do
|
||||||
|
package_inputs+=("${input#"$repo_dir"/}")
|
||||||
|
done < <(find "$repo_dir/setup/packages" -maxdepth 1 -type f -print0)
|
||||||
|
provenance_inputs=()
|
||||||
|
while IFS= read -r -d '' input; do
|
||||||
|
provenance_inputs+=("${input#"$repo_dir"/}")
|
||||||
|
done < <(find "$repo_dir/setup/provenance" -type f -print0)
|
||||||
|
(( ${#package_inputs[@]} > 0 )) \
|
||||||
|
|| note 'the current repository has no top-level package input to exercise'
|
||||||
|
(( ${#provenance_inputs[@]} > 0 )) \
|
||||||
|
|| note 'the current repository has no provenance input to exercise'
|
||||||
|
|
||||||
# Second run, nothing changed: the stage must be skipped.
|
# Second run, nothing changed: the stage must be skipped.
|
||||||
ran_again="$(run_install "$tmp/a" --upgrade)"
|
install_status=0
|
||||||
|
ran_again="$(run_install "$tmp/a" --upgrade)" || install_status=$?
|
||||||
|
[[ "$install_status" -eq 0 ]] \
|
||||||
|
|| note "repeat install --upgrade failed with status $install_status"
|
||||||
grep -qx 'install-packages' <<<"$ran_again" \
|
grep -qx 'install-packages' <<<"$ran_again" \
|
||||||
&& note 'install-packages ran again with the package lists unchanged'
|
&& note 'install-packages ran again with the package lists unchanged'
|
||||||
|
|
||||||
# --packages overrides the hash.
|
# --packages overrides the hash.
|
||||||
ran_forced="$(run_install "$tmp/a" --upgrade --packages)"
|
install_status=0
|
||||||
|
ran_forced="$(run_install "$tmp/a" --upgrade --packages)" || install_status=$?
|
||||||
|
[[ "$install_status" -eq 0 ]] \
|
||||||
|
|| note "install --upgrade --packages failed with status $install_status"
|
||||||
grep -qx 'install-packages' <<<"$ran_forced" \
|
grep -qx 'install-packages' <<<"$ran_forced" \
|
||||||
|| note '--packages did not force install-packages to run'
|
|| note '--packages did not force install-packages to run'
|
||||||
|
|
||||||
# A changed list brings the stage back.
|
# Every current package and provenance member is part of the state definition.
|
||||||
printf 'another-package\n' >>"$tmp/a/setup/packages/base"
|
# Dynamically discovering them makes this fail when a new reviewed input is
|
||||||
ran_changed="$(run_install "$tmp/a" --upgrade)"
|
# added but omitted from hash_packages.
|
||||||
grep -qx 'install-packages' <<<"$ran_changed" \
|
for relative in "${package_inputs[@]}" "${provenance_inputs[@]}" \
|
||||||
|| note 'a changed package list did not bring install-packages back'
|
'setup/scripts/install-packages' 'setup/lib/artifact-provenance'; 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; 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"
|
||||||
|
|
||||||
|
# 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'
|
||||||
|
|
||||||
# A failing stage must not record the hash, or the failure is hidden forever.
|
# A failing stage must not record the hash, or the failure is hidden forever.
|
||||||
build_fixture "$tmp/c" 1
|
build_fixture "$tmp/c" 1
|
||||||
run_install "$tmp/c" --upgrade >/dev/null
|
install_status=0
|
||||||
|
run_install "$tmp/c" --upgrade >/dev/null || install_status=$?
|
||||||
|
[[ "$install_status" -eq 1 ]] \
|
||||||
|
|| note "install --upgrade returned $install_status instead of the failing stage status"
|
||||||
if [[ -r "$tmp/c/state/panama/packages-hash" ]]; then
|
if [[ -r "$tmp/c/state/panama/packages-hash" ]]; then
|
||||||
note 'install-packages failed but its hash was recorded, so it will never be retried'
|
note 'install-packages failed but its hash was recorded, so it will never be retried'
|
||||||
fi
|
fi
|
||||||
|
grep -qx 'link-dotfiles' "$tmp/c/ran" \
|
||||||
|
|| note 'an ordinary package-stage failure no longer allows later safe stages'
|
||||||
|
|
||||||
|
# An invalid enabled Terra root is not an ordinary package failure. It must
|
||||||
|
# stop before the installer's bootstrap DNF and before every stage.
|
||||||
|
build_fixture "$tmp/terra-preflight-hard" 0 78
|
||||||
|
install_status=0
|
||||||
|
run_install "$tmp/terra-preflight-hard" >/dev/null || install_status=$?
|
||||||
|
[[ "$install_status" -eq 78 ]] \
|
||||||
|
|| note "initial Terra trust failure returned $install_status instead of 78"
|
||||||
|
asserted_preflight="$(<"$tmp/terra-preflight-hard/ran")"
|
||||||
|
[[ "$asserted_preflight" == trust-preflight ]] \
|
||||||
|
|| note "initial Terra trust failure allowed later work: ${asserted_preflight//$'\n'/,}"
|
||||||
|
|
||||||
|
# The trust verifier is itself mandatory. Losing its executable adapter must
|
||||||
|
# fail closed before interview, bootstrap, or stage work.
|
||||||
|
build_fixture "$tmp/terra-preflight-missing"
|
||||||
|
rm "$tmp/terra-preflight-missing/setup/scripts/install-packages"
|
||||||
|
install_status=0
|
||||||
|
run_install "$tmp/terra-preflight-missing" >/dev/null || install_status=$?
|
||||||
|
[[ "$install_status" -eq 78 ]] \
|
||||||
|
|| note "missing Terra trust verifier returned $install_status instead of 78"
|
||||||
|
[[ ! -s "$tmp/terra-preflight-missing/ran" ]] \
|
||||||
|
|| note 'missing Terra trust verifier allowed later work'
|
||||||
|
|
||||||
|
# The package stage repeats the preflight to close a configuration-change race.
|
||||||
|
# Its hard status must also stop link stages and install-hardware immediately.
|
||||||
|
build_fixture "$tmp/terra-stage-hard" 78 0
|
||||||
|
install_status=0
|
||||||
|
run_install "$tmp/terra-stage-hard" >/dev/null || install_status=$?
|
||||||
|
[[ "$install_status" -eq 78 ]] \
|
||||||
|
|| note "stage-time Terra trust failure returned $install_status instead of 78"
|
||||||
|
grep -qx 'install-packages' "$tmp/terra-stage-hard/ran" \
|
||||||
|
|| note 'stage-time Terra trust fixture never reached install-packages'
|
||||||
|
for suppressed in link-dotfiles link-skills link-user change-settings install-hardware dnf-transaction; do
|
||||||
|
grep -qx "$suppressed" "$tmp/terra-stage-hard/ran" \
|
||||||
|
&& note "stage-time Terra trust failure still ran $suppressed"
|
||||||
|
done
|
||||||
|
|
||||||
# A full install always runs the stage, whatever any recorded hash says.
|
# A full install always runs the stage, whatever any recorded hash says.
|
||||||
build_fixture "$tmp/d"
|
build_fixture "$tmp/d"
|
||||||
run_install "$tmp/d" --upgrade >/dev/null
|
install_status=0
|
||||||
ran_full="$(run_install "$tmp/d")"
|
run_install "$tmp/d" --upgrade >/dev/null || install_status=$?
|
||||||
|
[[ "$install_status" -eq 0 ]] \
|
||||||
|
|| note "hash-seeding install --upgrade failed with status $install_status"
|
||||||
|
install_status=0
|
||||||
|
ran_full="$(run_install "$tmp/d")" || install_status=$?
|
||||||
|
[[ "$install_status" -eq 0 ]] \
|
||||||
|
|| note "full fixture reinstall failed with status $install_status"
|
||||||
grep -qx 'install-packages' <<<"$ran_full" \
|
grep -qx 'install-packages' <<<"$ran_full" \
|
||||||
|| note 'a full ./install skipped install-packages because of a recorded hash'
|
|| note 'a full ./install skipped install-packages because of a recorded hash'
|
||||||
|
|
||||||
# ── 4. A conflicted pop never leaves markers in a live config ────────────────
|
# ── 4. A conflicted pop never leaves markers in a live config ────────────────
|
||||||
#
|
#
|
||||||
# Two halves. The first checks that git still behaves the way the design
|
# The fixture also covers a clean fast-forward, installer status propagation,
|
||||||
# depends on; the second checks that panama acts on it. Neither is worth much
|
# and the boundary between update and sync before forcing the conflict below.
|
||||||
# without the other.
|
|
||||||
|
|
||||||
conflict="$tmp/conflict"
|
# Make every ambient configuration source hostile before constructing the Git
|
||||||
mkdir -p "$conflict"
|
# fixtures. A hermetic fixture overrides these values with its own empty state;
|
||||||
(
|
# consuming any of them either leaves a sentinel or prevents a commit.
|
||||||
set -e
|
hostile="$tmp/hostile-environment"
|
||||||
cd "$conflict"
|
mkdir -p "$hostile/home" "$hostile/xdg-config" "$hostile/xdg-state" \
|
||||||
git init -q up && cd up
|
"$hostile/xdg-cache" "$hostile/xdg-data" "$hostile/hooks" \
|
||||||
git config user.email contract@panama && git config user.name contract
|
"$hostile/template/hooks"
|
||||||
printf 'one\n' >f; git add -A; git commit -qm one
|
for profile in .bash_profile .bashrc .profile; do
|
||||||
cd "$conflict"; git clone -q up work; cd work
|
cat >"$hostile/home/$profile" <<'EOF'
|
||||||
git config user.email contract@panama && git config user.name contract
|
printf 'profile\n' >>"${PANAMA_HOSTILE_PROFILE_SENTINEL:?}"
|
||||||
cd "$conflict/up"; printf 'upstream\n' >f; git commit -qam two
|
EOF
|
||||||
cd "$conflict/work"; printf 'local\n' >f
|
done
|
||||||
git stash push --include-untracked -m contract >/dev/null
|
cat >"$hostile/bash-env" <<'EOF'
|
||||||
git pull -q --ff-only
|
printf 'BASH_ENV\n' >>"${PANAMA_HOSTILE_BASH_ENV_SENTINEL:?}"
|
||||||
git stash pop >/dev/null 2>&1 && exit 3 # a conflict was the point
|
EOF
|
||||||
git reset -q --hard HEAD
|
cat >"$hostile/hooks/pre-commit" <<'EOF'
|
||||||
[[ -n "$(git stash list)" ]] || exit 4 # the stash must survive
|
#!/usr/bin/env bash
|
||||||
grep -q '<<<<<<<' f && exit 5 # and no markers may remain
|
printf 'global hook\n' >>"${PANAMA_HOSTILE_GIT_SENTINEL:?}"
|
||||||
exit 0
|
exit 97
|
||||||
) >/dev/null 2>&1
|
EOF
|
||||||
case $? in
|
cat >"$hostile/template/hooks/pre-commit" <<'EOF'
|
||||||
0) ;;
|
#!/usr/bin/env bash
|
||||||
3) note 'the conflict fixture did not conflict, so this check proves nothing' ;;
|
# PANAMA_HOSTILE_TEMPLATE_HOOK
|
||||||
4) note 'git no longer keeps the stash after a conflicted pop; panama update would lose work' ;;
|
printf 'template hook\n' >>"${PANAMA_HOSTILE_TEMPLATE_SENTINEL:?}"
|
||||||
5) note 'git reset --hard left conflict markers behind' ;;
|
exit 98
|
||||||
*) note 'the stash conflict fixture could not be built' ;;
|
EOF
|
||||||
esac
|
chmod +x "$hostile/hooks/pre-commit" "$hostile/template/hooks/pre-commit"
|
||||||
|
cat >"$hostile/global.gitconfig" <<EOF
|
||||||
|
[core]
|
||||||
|
hooksPath = $hostile/hooks
|
||||||
|
[commit]
|
||||||
|
gpgSign = true
|
||||||
|
[init]
|
||||||
|
templateDir = $hostile/template
|
||||||
|
EOF
|
||||||
|
cp "$hostile/global.gitconfig" "$hostile/system.gitconfig"
|
||||||
|
|
||||||
# panama update must act on that: reset the tree rather than leave the markers.
|
export HOME="$hostile/home"
|
||||||
body="$(sed -n '/^cmd_update()/,/^}/p' "$panama")"
|
export XDG_CONFIG_HOME="$hostile/xdg-config"
|
||||||
if [[ -z "$body" ]]; then
|
export XDG_STATE_HOME="$hostile/xdg-state"
|
||||||
note 'bin/panama has no cmd_update to check'
|
export XDG_CACHE_HOME="$hostile/xdg-cache"
|
||||||
|
export XDG_DATA_HOME="$hostile/xdg-data"
|
||||||
|
export BASH_ENV="$hostile/bash-env"
|
||||||
|
export PANAMA_HOSTILE_PROFILE_SENTINEL="$hostile/profile-sourced"
|
||||||
|
export PANAMA_HOSTILE_BASH_ENV_SENTINEL="$hostile/bash-env-sourced"
|
||||||
|
export PANAMA_HOSTILE_GIT_SENTINEL="$hostile/global-config-sourced"
|
||||||
|
export PANAMA_HOSTILE_TEMPLATE_SENTINEL="$hostile/template-hook-sourced"
|
||||||
|
export GIT_CONFIG_NOSYSTEM=0
|
||||||
|
export GIT_CONFIG_SYSTEM="$hostile/system.gitconfig"
|
||||||
|
export GIT_CONFIG_GLOBAL="$hostile/global.gitconfig"
|
||||||
|
export GIT_CONFIG_COUNT=1
|
||||||
|
export GIT_CONFIG_KEY_0=core.hooksPath
|
||||||
|
export GIT_CONFIG_VALUE_0="$hostile/hooks"
|
||||||
|
export GIT_TEMPLATE_DIR="$hostile/template"
|
||||||
|
|
||||||
|
prepare_cli_fixture_environment() {
|
||||||
|
local root="$1"
|
||||||
|
mkdir -p "$root/home" "$root/xdg-config" "$root/xdg-state" \
|
||||||
|
"$root/xdg-cache" "$root/xdg-data" "$root/xdg-runtime" \
|
||||||
|
"$root/empty-templates" "$root/empty-hooks"
|
||||||
|
chmod 700 "$root/xdg-runtime"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_cli_fixture_environment() {
|
||||||
|
local root="$1"
|
||||||
|
shift
|
||||||
|
env -u BASH_ENV -u ENV -u GIT_CONFIG_PARAMETERS \
|
||||||
|
-u GIT_CONFIG_KEY_0 -u GIT_CONFIG_VALUE_0 \
|
||||||
|
HOME="$root/home" \
|
||||||
|
XDG_CONFIG_HOME="$root/xdg-config" \
|
||||||
|
XDG_STATE_HOME="$root/xdg-state" \
|
||||||
|
XDG_CACHE_HOME="$root/xdg-cache" \
|
||||||
|
XDG_DATA_HOME="$root/xdg-data" \
|
||||||
|
XDG_RUNTIME_DIR="$root/xdg-runtime" \
|
||||||
|
GIT_CONFIG_NOSYSTEM=1 \
|
||||||
|
GIT_CONFIG_SYSTEM=/dev/null \
|
||||||
|
GIT_CONFIG_GLOBAL=/dev/null \
|
||||||
|
GIT_CONFIG_COUNT=0 \
|
||||||
|
GIT_TEMPLATE_DIR="$root/empty-templates" \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture_git() {
|
||||||
|
local root="$1"
|
||||||
|
shift
|
||||||
|
run_cli_fixture_environment "$root" \
|
||||||
|
git -c commit.gpgSign=false -c tag.gpgSign=false \
|
||||||
|
-c core.hooksPath="$root/empty-hooks" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_fixture_repo() {
|
||||||
|
local root="$1" repository="$2"
|
||||||
|
fixture_git "$root" -C "$repository" config user.email contract@panama || return 1
|
||||||
|
fixture_git "$root" -C "$repository" config user.name contract || return 1
|
||||||
|
fixture_git "$root" -C "$repository" config commit.gpgSign false || return 1
|
||||||
|
fixture_git "$root" -C "$repository" config tag.gpgSign false || return 1
|
||||||
|
fixture_git "$root" -C "$repository" config core.hooksPath "$root/empty-hooks" || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Each fixture has the same three repositories as a real update: a bare remote,
|
||||||
|
# a clone that publishes upstream changes, and the machine clone being updated.
|
||||||
|
build_cli_fixture() (
|
||||||
|
local root="$1"
|
||||||
|
rm -rf "$root" || return 1
|
||||||
|
mkdir -p "$root" || return 1
|
||||||
|
prepare_cli_fixture_environment "$root" || return 1
|
||||||
|
fixture_git "$root" init -q --bare "$root/origin.git" || return 1
|
||||||
|
fixture_git "$root" -C "$root/origin.git" config core.hooksPath "$root/empty-hooks" || return 1
|
||||||
|
fixture_git "$root" clone -q "$root/origin.git" "$root/upstream" 2>/dev/null || return 1
|
||||||
|
configure_fixture_repo "$root" "$root/upstream" || return 1
|
||||||
|
|
||||||
|
mkdir -p "$root/upstream/bin" || return 1
|
||||||
|
cp "$panama" "$root/upstream/bin/panama" || return 1
|
||||||
|
cat >"$root/upstream/install" <<'EOF' || return 1
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' "$*" >>"${PANAMA_UPDATE_FIXTURE_LOG:?}"
|
||||||
|
exit "${PANAMA_UPDATE_INSTALL_RC:-0}"
|
||||||
|
EOF
|
||||||
|
chmod +x "$root/upstream/bin/panama" "$root/upstream/install" || return 1
|
||||||
|
printf 'one\n' >"$root/upstream/f" || return 1
|
||||||
|
fixture_git "$root" -C "$root/upstream" add -A || return 1
|
||||||
|
fixture_git "$root" -C "$root/upstream" commit -qm initial || return 1
|
||||||
|
fixture_git "$root" -C "$root/upstream" push -qu origin HEAD || return 1
|
||||||
|
|
||||||
|
fixture_git "$root" clone -q "$root/origin.git" "$root/machine" || return 1
|
||||||
|
configure_fixture_repo "$root" "$root/machine" || return 1
|
||||||
|
)
|
||||||
|
|
||||||
|
advance_upstream() (
|
||||||
|
local root="$1" file="$2" contents="$3"
|
||||||
|
printf '%s\n' "$contents" >"$root/upstream/$file" || return 1
|
||||||
|
fixture_git "$root" -C "$root/upstream" add "$file" || return 1
|
||||||
|
fixture_git "$root" -C "$root/upstream" commit -qm "update $file" || return 1
|
||||||
|
fixture_git "$root" -C "$root/upstream" push -q || return 1
|
||||||
|
)
|
||||||
|
|
||||||
|
# This write fails before the later Git commands. The helper must return that
|
||||||
|
# failure rather than let a final successful push hide it.
|
||||||
|
helper_probe="$tmp/helper-failure"
|
||||||
|
if build_cli_fixture "$helper_probe"; then
|
||||||
|
if advance_upstream "$helper_probe" missing/child probe \
|
||||||
|
2>"$helper_probe/intermediate-failure.err"; then
|
||||||
|
note 'advance_upstream hid an intermediate fixture setup failure'
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
grep -q 'git stash push' <<<"$body" \
|
note 'the fixture helper failure probe could not be built'
|
||||||
|| note 'cmd_update does not stash local changes, so a pull can fail on a dirty tree'
|
|
||||||
grep -q 'git reset --hard' <<<"$body" \
|
|
||||||
|| note 'cmd_update does not reset after a failed pop, so conflict markers reach ~/.config'
|
|
||||||
grep -q 'git pull --ff-only' <<<"$body" \
|
|
||||||
|| note 'cmd_update does not pull with --ff-only'
|
|
||||||
grep -q -- '--upgrade' <<<"$body" \
|
|
||||||
|| note 'cmd_update does not hand off to install --upgrade, so it would ask the interview'
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The two verbs stay separate: sync must never run the installer.
|
clean="$tmp/clean-update"
|
||||||
sync_body="$(sed -n '/^cmd_sync()/,/^}/p' "$panama")"
|
if build_cli_fixture "$clean" && advance_upstream "$clean" release new; then
|
||||||
if [[ -z "$sync_body" ]]; then
|
machine_before="$(fixture_git "$clean" -C "$clean/machine" rev-parse HEAD)"
|
||||||
note 'bin/panama has no cmd_sync, so the git workflow lost its home'
|
upstream_after="$(fixture_git "$clean" -C "$clean/upstream" rev-parse HEAD)"
|
||||||
|
[[ "$machine_before" != "$upstream_after" ]] \
|
||||||
|
|| note 'the clean update fixture started current, so it cannot prove a fast-forward'
|
||||||
|
|
||||||
|
: >"$clean/install.log"
|
||||||
|
update_status=0
|
||||||
|
run_cli_fixture_environment "$clean" \
|
||||||
|
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
|
||||||
|
"$clean/machine/bin/panama" update >"$clean/update.out" 2>&1 \
|
||||||
|
|| update_status=$?
|
||||||
|
[[ "$update_status" -eq 0 ]] \
|
||||||
|
|| note "panama update failed on a clean clone with status $update_status"
|
||||||
|
[[ "$(fixture_git "$clean" -C "$clean/machine" rev-parse HEAD)" == "$upstream_after" ]] \
|
||||||
|
|| note 'panama update did not fast-forward the clean machine clone'
|
||||||
|
grep -qx -- '--upgrade' "$clean/install.log" \
|
||||||
|
|| note 'panama update did not invoke the installer with --upgrade'
|
||||||
|
|
||||||
|
: >"$clean/install.log"
|
||||||
|
update_status=0
|
||||||
|
run_cli_fixture_environment "$clean" \
|
||||||
|
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" PANAMA_UPDATE_INSTALL_RC=23 \
|
||||||
|
"$clean/machine/bin/panama" update >"$clean/failing-update.out" 2>&1 \
|
||||||
|
|| update_status=$?
|
||||||
|
[[ "$update_status" -eq 23 ]] \
|
||||||
|
|| note "panama update returned $update_status instead of installer status 23"
|
||||||
|
grep -qx -- '--upgrade' "$clean/install.log" \
|
||||||
|
|| note 'the failing update did not reach the fixture installer'
|
||||||
|
|
||||||
|
: >"$clean/install.log"
|
||||||
|
printf 'local sync\n' >"$clean/machine/synced"
|
||||||
|
sync_status=0
|
||||||
|
printf 'y\ncontract sync\n' \
|
||||||
|
| run_cli_fixture_environment "$clean" \
|
||||||
|
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
|
||||||
|
"$clean/machine/bin/panama" sync >"$clean/sync.out" 2>&1 \
|
||||||
|
|| sync_status=$?
|
||||||
|
[[ "$sync_status" -eq 0 ]] \
|
||||||
|
|| note "panama sync failed in the local fixture with status $sync_status"
|
||||||
|
[[ ! -s "$clean/install.log" ]] \
|
||||||
|
|| note 'panama sync invoked the installer; sync and update are separate jobs'
|
||||||
else
|
else
|
||||||
grep -q 'install' <<<"$sync_body" \
|
note 'the clean update fixture could not be built'
|
||||||
&& note 'cmd_sync runs the installer; committing and updating are separate jobs'
|
|
||||||
grep -q 'git stash' <<<"$sync_body" \
|
|
||||||
&& note 'cmd_sync stashes, which the commit-before-pull order exists to avoid'
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# A conflicted stash pop must not leave markers in the checkout. The failed pop
|
||||||
|
# keeps the stash, so the local version remains recoverable after the reset.
|
||||||
|
conflict="$tmp/conflict-update"
|
||||||
|
if build_cli_fixture "$conflict"; then
|
||||||
|
printf 'local\n' >"$conflict/machine/f"
|
||||||
|
if advance_upstream "$conflict" f upstream; then
|
||||||
|
: >"$conflict/install.log"
|
||||||
|
conflict_status=0
|
||||||
|
run_cli_fixture_environment "$conflict" \
|
||||||
|
PANAMA_UPDATE_FIXTURE_LOG="$conflict/install.log" \
|
||||||
|
"$conflict/machine/bin/panama" update >"$conflict/update.out" 2>&1 \
|
||||||
|
|| conflict_status=$?
|
||||||
|
[[ "$conflict_status" -eq 0 ]] \
|
||||||
|
|| note "panama update failed while recovering a stash conflict with status $conflict_status"
|
||||||
|
[[ "$(<"$conflict/machine/f")" == upstream ]] \
|
||||||
|
|| note 'panama update did not reset the conflicted file to the upstream version'
|
||||||
|
if fixture_git "$conflict" -C "$conflict/machine" \
|
||||||
|
grep -qE '^(<<<<<<<|=======|>>>>>>>)' -- .; then
|
||||||
|
note 'panama update left conflict markers in the machine checkout'
|
||||||
|
fi
|
||||||
|
[[ -n "$(fixture_git "$conflict" -C "$conflict/machine" stash list)" ]] \
|
||||||
|
|| note 'panama update dropped the stash after its conflicted pop'
|
||||||
|
recovered="$(fixture_git "$conflict" -C "$conflict/machine" \
|
||||||
|
show 'stash@{0}:f' 2>/dev/null)"
|
||||||
|
[[ "$recovered" == local ]] \
|
||||||
|
|| note 'the stash left by panama update does not contain the local version'
|
||||||
|
else
|
||||||
|
note 'the conflict fixture could not publish its upstream edit'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
note 'the conflict update fixture could not be built'
|
||||||
|
fi
|
||||||
|
|
||||||
|
for sentinel in profile-sourced bash-env-sourced global-config-sourced template-hook-sourced; do
|
||||||
|
[[ ! -e "$hostile/$sentinel" ]] \
|
||||||
|
|| note "the update Git fixture consumed hostile state: $sentinel"
|
||||||
|
done
|
||||||
|
template_copy="$(
|
||||||
|
find "$tmp" -path "$hostile" -prune -o \
|
||||||
|
-type f -path '*/hooks/pre-commit' \
|
||||||
|
-exec grep -lF 'PANAMA_HOSTILE_TEMPLATE_HOOK' {} + 2>/dev/null
|
||||||
|
)"
|
||||||
|
[[ -z "$template_copy" ]] \
|
||||||
|
|| note "the update Git fixture copied a hostile template hook: $template_copy"
|
||||||
|
|
||||||
if (( ${#findings[@]} > 0 )); then
|
if (( ${#findings[@]} > 0 )); then
|
||||||
printf 'update command contract: %d finding(s)\n' "${#findings[@]}" >&2
|
printf 'update command contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||||
printf ' - %s\n' "${findings[@]}" >&2
|
printf ' - %s\n' "${findings[@]}" >&2
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ cat >"$checkout/user/manifest" <<'FIXTURE'
|
|||||||
# a comment, and a blank line follow
|
# a comment, and a blank line follow
|
||||||
|
|
||||||
link agents/AGENTS.md ~/.claude/CLAUDE.md
|
link agents/AGENTS.md ~/.claude/CLAUDE.md
|
||||||
link agents/skills ~/.agents/skills
|
linkdir agents/skills ~/.agents/skills
|
||||||
linkdir agents/skills ~/.claude/skills
|
linkdir agents/skills ~/.claude/skills
|
||||||
copy plain.txt ~/.config/plain.txt
|
copy plain.txt ~/.config/plain.txt
|
||||||
link missing.txt ~/.config/missing.txt
|
link missing.txt ~/.config/missing.txt
|
||||||
@@ -102,11 +102,12 @@ PANAMA_USER_CONTENT=no run
|
|||||||
|
|
||||||
# ── 2. Saying yes links, and keeps what was there ───────────────────────────
|
# ── 2. Saying yes links, and keeps what was there ───────────────────────────
|
||||||
|
|
||||||
# ~/.claude/skills is shared now: link-skills has already made it a real
|
# Both skill homes are shared now: link-skills has already made them real
|
||||||
# directory and linked Panama's own skills into it. A linkdir entry has to land
|
# directories and linked Panama's own skills into each. Personal linkdir
|
||||||
# beside those rather than replace the directory holding them.
|
# entries land beside those rather than replacing either directory.
|
||||||
mkdir -p "$home/.claude/skills"
|
mkdir -p "$home/.claude/skills" "$home/.agents/skills"
|
||||||
ln -s "$checkout/skills/shipped" "$home/.claude/skills/shipped"
|
ln -s "$checkout/skills/shipped" "$home/.claude/skills/shipped"
|
||||||
|
ln -s "$checkout/skills/shipped" "$home/.agents/skills/shipped"
|
||||||
|
|
||||||
PANAMA_USER_CONTENT=yes run
|
PANAMA_USER_CONTENT=yes run
|
||||||
|
|
||||||
@@ -114,8 +115,12 @@ PANAMA_USER_CONTENT=yes run
|
|||||||
|| note 'CLAUDE.md was not replaced with a symlink into the checkout'
|
|| note 'CLAUDE.md was not replaced with a symlink into the checkout'
|
||||||
[[ "$(cat "$home/.claude/CLAUDE.md")" == "tracked instructions" ]] \
|
[[ "$(cat "$home/.claude/CLAUDE.md")" == "tracked instructions" ]] \
|
||||||
|| note 'the CLAUDE.md link does not resolve to the tracked file'
|
|| note 'the CLAUDE.md link does not resolve to the tracked file'
|
||||||
[[ -L "$home/.agents/skills" && -f "$home/.agents/skills/example/SKILL.md" ]] \
|
[[ -d "$home/.agents/skills" && ! -L "$home/.agents/skills" ]] \
|
||||||
|| note 'the skills directory was not linked as a directory'
|
|| note 'a linkdir entry replaced ~/.agents/skills with a symlink'
|
||||||
|
[[ -L "$home/.agents/skills/example" && -f "$home/.agents/skills/example/SKILL.md" ]] \
|
||||||
|
|| note 'a linkdir entry did not publish the personal skill for agent-neutral readers'
|
||||||
|
[[ -L "$home/.agents/skills/shipped" ]] \
|
||||||
|
|| note 'a linkdir entry removed the shipped skill from ~/.agents/skills'
|
||||||
[[ -d "$home/.claude/skills" && ! -L "$home/.claude/skills" ]] \
|
[[ -d "$home/.claude/skills" && ! -L "$home/.claude/skills" ]] \
|
||||||
|| note 'a linkdir entry replaced its destination directory with a symlink'
|
|| note 'a linkdir entry replaced its destination directory with a symlink'
|
||||||
[[ -L "$home/.claude/skills/example" && -f "$home/.claude/skills/example/SKILL.md" ]] \
|
[[ -L "$home/.claude/skills/example" && -f "$home/.claude/skills/example/SKILL.md" ]] \
|
||||||
|
|||||||
+11
-11
@@ -13,7 +13,7 @@ and one file says where each piece goes.
|
|||||||
| Path | Goes to | Why |
|
| Path | Goes to | Why |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `agents/AGENTS.md` | `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` | Two tools, two names, one file. These were byte-identical copies before this, waiting to disagree. |
|
| `agents/AGENTS.md` | `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` | Two tools, two names, one file. These were byte-identical copies before this, waiting to disagree. |
|
||||||
| `agents/skills/` | `~/.agents/skills`, `~/.claude/skills` | A skill installed on any machine lands in the checkout. The Claude path is `linkdir` — see below. |
|
| `agents/skills/` | `~/.agents/skills`, `~/.claude/skills` | Personal skills live once in the checkout. Both destinations use `linkdir` so Panama's shipped skills can live beside them. |
|
||||||
| `agents/rules/` | `~/.claude/rules` | |
|
| `agents/rules/` | `~/.claude/rules` | |
|
||||||
| `ssh/config` | `~/.ssh/config` | Host aliases only. Keys are per-machine and are never tracked. |
|
| `ssh/config` | `~/.ssh/config` | Host aliases only. Keys are per-machine and are never tracked. |
|
||||||
| `espanso/identity.yml` | `~/.config/espanso/match/identity.yml` | Copied, not linked, because a machine may add its own triggers. |
|
| `espanso/identity.yml` | `~/.config/espanso/match/identity.yml` | Copied, not linked, because a machine may add its own triggers. |
|
||||||
@@ -27,22 +27,22 @@ destination is empty. `linkdir` is the third, and it exists because one
|
|||||||
destination is no longer only ours.
|
destination is no longer only ours.
|
||||||
|
|
||||||
Panama ships its own agent skills now (`skills/`, linked by
|
Panama ships its own agent skills now (`skills/`, linked by
|
||||||
`setup/scripts/link-skills`), and they go to `~/.claude/skills` — the same
|
`setup/scripts/link-skills`), and they go to both skill homes. A directory
|
||||||
directory the personal ones went to as a single symlink. A directory cannot be
|
cannot be a symlink to two places, so both personal entries use `linkdir`: each
|
||||||
a symlink to two places, so that entry became `linkdir`: the destination is a
|
destination is a real directory, and each child of `user/agents/skills/` is
|
||||||
real directory, and each child of `user/agents/skills/` is linked into it
|
linked into it individually.
|
||||||
individually. `~/.agents/skills` is still a whole-directory `link`, because
|
|
||||||
nothing else claims it.
|
|
||||||
|
|
||||||
Two consequences, recorded rather than fixed:
|
Two consequences, recorded rather than fixed:
|
||||||
|
|
||||||
- **A personal skill named like a shipped one shadows it.** `link-user` runs
|
- **A personal skill named like a shipped one shadows it.** `link-user` runs
|
||||||
after `link-skills` and displaces what it finds, so the personal one wins.
|
after `link-skills` and displaces what it finds, so the personal one wins in
|
||||||
That is the intent, and it is the precedence Claude Code uses anyway.
|
both skill homes.
|
||||||
- **A new personal skill needs a re-link to appear.** The whole-directory link
|
- **A new personal skill needs a re-link to appear.** The whole-directory link
|
||||||
showed a newly created skill instantly; per-child links do not know about a
|
showed a newly created skill instantly; per-child links do not know about a
|
||||||
child that did not exist when they were made. Run `panama update` or
|
child that did not exist when they were made. Put new shared skills in
|
||||||
`setup/scripts/link-user` after adding one.
|
`user/agents/skills/`, then run `panama update` or `setup/scripts/link-user`.
|
||||||
|
A tool that installs directly into a home skill directory creates a
|
||||||
|
machine-local skill until it is moved into the checkout.
|
||||||
|
|
||||||
## It is off unless you say yes
|
## It is off unless you say yes
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: ticket
|
name: ticket
|
||||||
description: End-to-end Jira ticket workflow — fetch a ticket into .claude/docs/epics/, write a plan, implement it with clean commits, run pre-mr-review to convergence, and write the MR doc. Use when the user gives you a Jira ticket key (e.g. KACP-11111) to work, or asks to plan/implement/wrap up a ticket.
|
description: End-to-end Jira ticket workflow — fetch a ticket into .claude/docs/epics/, write a plan, implement it with clean commits, drive the pre-mr-review audit to convergence (the user runs it themselves from their work Claude account), and write the MR doc. Use when the user gives you a Jira ticket key (e.g. KACP-11111) to work, or asks to plan/implement/wrap up a ticket.
|
||||||
disable-model-invocation: true
|
disable-model-invocation: true
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -372,7 +372,7 @@ place from the start.
|
|||||||
- Proceed to implementing the existing `plan.md` as-is (they reviewed and approved it)
|
- Proceed to implementing the existing `plan.md` as-is (they reviewed and approved it)
|
||||||
- Resume implementation (some plan steps are already checked off / some commits
|
- Resume implementation (some plan steps are already checked off / some commits
|
||||||
already exist on the ticket branch — pick up from the first unchecked step)
|
already exist on the ticket branch — pick up from the first unchecked step)
|
||||||
- Run `pre-mr-review` now (implementation looks done, just need the audit + MR doc)
|
- Move to the pre-mr-review stage (implementation looks done, just need the audit + MR doc; the user runs the audit from their work account, see Phase 2 step 7)
|
||||||
Route to **Phase 1** or **Phase 2** accordingly.
|
Route to **Phase 1** or **Phase 2** accordingly.
|
||||||
- **Both `plan.md` and `mr.md` exist** — this ticket looks finished. Tell the user
|
- **Both `plan.md` and `mr.md` exist** — this ticket looks finished. Tell the user
|
||||||
`mr.md` already exists at its path and ask whether they want you to refresh it
|
`mr.md` already exists at its path and ask whether they want you to refresh it
|
||||||
@@ -620,8 +620,8 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
|||||||
Order matters and is not just convenience. `pre-mr-review`'s output is a readiness
|
Order matters and is not just convenience. `pre-mr-review`'s output is a readiness
|
||||||
verdict that gets pasted verbatim into `mr.md`. Anything that runs after it
|
verdict that gets pasted verbatim into `mr.md`. Anything that runs after it
|
||||||
invalidates that verdict by construction, and you end up rerunning it and rewriting
|
invalidates that verdict by construction, and you end up rerunning it and rewriting
|
||||||
the handoff. Review first, fix, commit, and only then run `pre-mr-review` over the
|
the handoff. Review first, fix, commit, and only then have the user
|
||||||
final tree.
|
run `pre-mr-review` over the final tree.
|
||||||
|
|
||||||
- **Skip this step for genuinely trivial changes**: a copy tweak, a styling fix, a
|
- **Skip this step for genuinely trivial changes**: a copy tweak, a styling fix, a
|
||||||
one line correction with no logic in it. Run it whenever the change adds or
|
one line correction with no logic in it. Run it whenever the change adds or
|
||||||
@@ -671,11 +671,25 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
|||||||
in the current repo). If it doesn't, stop here, tell the user implementation and
|
in the current repo). If it doesn't, stop here, tell the user implementation and
|
||||||
local verification are done but this repo has no `pre-mr-review` skill to run, and
|
local verification are done but this repo has no `pre-mr-review` skill to run, and
|
||||||
let them decide how to proceed.
|
let them decide how to proceed.
|
||||||
7. Invoke the `pre-mr-review` skill. Read its verdict.
|
7. **Do NOT invoke the `pre-mr-review` skill yourself.** The org asks that this
|
||||||
- **Ready to Open MR**: continue to step 8.
|
review run from the developer's separate work Claude account, so the audit on
|
||||||
- **Almost Ready / Not Ready Yet**: fix what it flagged (each meaningful fix as its
|
record must come from there, not from this session. Instead:
|
||||||
own commit), then invoke `pre-mr-review` again. Repeat until the verdict is Ready
|
1. Tell the user the branch is ready for its pre-mr-review and ask them to run
|
||||||
to Open MR. Don't write `mr.md` before that verdict is reached.
|
`/pre-mr-review` from their work account, then let you know when it has
|
||||||
|
finished. Stop and wait, this is a hard gate.
|
||||||
|
2. When they say it ran, read the audit and handoff files it wrote under
|
||||||
|
`.claude/audits/pre-mr/` (the context script from the repo's `pre-mr-review`
|
||||||
|
skill prints the exact paths). Check the `last_reviewed_head` in the audit
|
||||||
|
header matches the current HEAD; if the branch moved after their run, say so
|
||||||
|
and ask them to rerun before acting on a stale audit.
|
||||||
|
3. Read the verdict.
|
||||||
|
- **Ready to Open MR**: continue to step 8.
|
||||||
|
- **Almost Ready / Not Ready Yet**: fix what it flagged (each meaningful fix
|
||||||
|
as its own commit), then ask the user to rerun the review from the work
|
||||||
|
account and wait again. Repeat until the verdict is Ready to Open MR. Don't
|
||||||
|
write `mr.md` before that verdict is reached.
|
||||||
|
Findings that need no code change (mentions, disclosures) get addressed in
|
||||||
|
`mr.md`'s Additional Notes or the Jira fields as usual.
|
||||||
8. Read `~/.agents/skills/ticket/templates/mr.md` — this is the org's MR template,
|
8. Read `~/.agents/skills/ticket/templates/mr.md` — this is the org's MR template,
|
||||||
copied into this skill so it still works even though the original
|
copied into this skill so it still works even though the original
|
||||||
`.claude/docs/mr/template.md` no longer exists in the command-center repo. `mr.md`
|
`.claude/docs/mr/template.md` no longer exists in the command-center repo. `mr.md`
|
||||||
@@ -707,9 +721,9 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
|||||||
the audit's own words, not a retelling of them. Concretely:
|
the audit's own words, not a retelling of them. Concretely:
|
||||||
|
|
||||||
- Never hand-write a handoff section, and never edit one in place. If it is
|
- Never hand-write a handoff section, and never edit one in place. If it is
|
||||||
wrong, thin, or stale, fix the handoff file by rerunning `/pre-mr-review`,
|
wrong, thin, or stale, ask the user to rerun `/pre-mr-review` from their work
|
||||||
then re-paste.
|
account to regenerate the handoff file, then re-paste.
|
||||||
- On EVERY rerun, replace the whole existing handoff block with the whole
|
- On EVERY rerun of theirs, replace the whole existing handoff block with the whole
|
||||||
regenerated one. Do not patch the copy sitting in `mr.md` to match the new
|
regenerated one. Do not patch the copy sitting in `mr.md` to match the new
|
||||||
head — that is how the two silently diverge, and the version the reviewer
|
head — that is how the two silently diverge, and the version the reviewer
|
||||||
reads stops being the version the audit actually produced.
|
reads stops being the version the audit actually produced.
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ ln -s ../../.agents/skills/<name> ~/.claude/skills/<name>
|
|||||||
Writing a second copy into `~/.claude/skills/` gives you two files that drift, and the drift is
|
Writing a second copy into `~/.claude/skills/` gives you two files that drift, and the drift is
|
||||||
silent because each harness only ever reads its own. One home, one symlink per harness that needs it.
|
silent because each harness only ever reads its own. One home, one symlink per harness that needs it.
|
||||||
|
|
||||||
A skill that only makes sense inside one repo belongs in that repo, at `<repo>/.claude/skills/`.
|
A skill that only makes sense inside one repo lives once at
|
||||||
|
`<repo>/.agents/skills/<name>/`. Point Claude Code at that source with a
|
||||||
|
`<repo>/.claude/skills/<name>` symlink.
|
||||||
|
|
||||||
## Frontmatter
|
## Frontmatter
|
||||||
|
|
||||||
|
|||||||
+6
-7
@@ -28,14 +28,13 @@
|
|||||||
link agents/AGENTS.md ~/.claude/CLAUDE.md
|
link agents/AGENTS.md ~/.claude/CLAUDE.md
|
||||||
link agents/AGENTS.md ~/.codex/AGENTS.md
|
link agents/AGENTS.md ~/.codex/AGENTS.md
|
||||||
|
|
||||||
# Skills, at both paths that look for them. Linked so that a skill installed by
|
# Skills, at both paths that look for them. Each tracked skill is linked from
|
||||||
# any tool lands in the checkout and `panama update` offers to commit it.
|
# this checkout so edits stay shared and `panama update` offers to commit them.
|
||||||
#
|
#
|
||||||
# ~/.agents/skills is this directory and nothing else, so it stays one link.
|
# Both destinations also hold skills Panama itself ships, put there by the
|
||||||
# ~/.claude/skills also holds the skills Panama itself ships, put there by the
|
# link-skills stage, so both are real directories with one link per skill. This
|
||||||
# link-skills stage, so it is a real directory with one link per skill -- and
|
# stage runs after that one, so a personal skill wins a name collision.
|
||||||
# this stage runs after that one, so a personal skill wins a name collision.
|
linkdir agents/skills ~/.agents/skills
|
||||||
link agents/skills ~/.agents/skills
|
|
||||||
linkdir agents/skills ~/.claude/skills
|
linkdir agents/skills ~/.claude/skills
|
||||||
|
|
||||||
link agents/rules ~/.claude/rules
|
link agents/rules ~/.claude/rules
|
||||||
|
|||||||
Reference in New Issue
Block a user