Compare commits
12
Commits
99156442b5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
130137485e | ||
|
|
5ebebc8aa2 | ||
|
|
85160ffef2 | ||
|
|
59192143c5 | ||
|
|
2f2e3b88e8 | ||
|
|
9156a7e05c | ||
|
|
39a2e8e9ca | ||
|
|
6206565d95 | ||
|
|
cceb7a707b | ||
|
|
c1bbc69c8a | ||
|
|
fa8b14e05e | ||
|
|
b5832fc94a |
@@ -12,6 +12,10 @@
|
||||
/config/dot/espanso/match/identity.yml
|
||||
# Ignore backups of old config files
|
||||
/config/old
|
||||
# MCP bearer tokens. The server list in user/agents/mcp/servers is tracked
|
||||
# and names which variable carries each token; the tokens themselves are
|
||||
# machine-local, because this repo is public.
|
||||
/user/agents/mcp/env
|
||||
# Ignore Wireguard config of course!
|
||||
/config/wg/**
|
||||
# Ignore Neovim lazy-lock file
|
||||
|
||||
@@ -250,7 +250,7 @@ docs/ Settings reference, and the design specs behind the work
|
||||
|
||||
## Tests
|
||||
|
||||
189 of them, under `tests/`. `tests/contracts.manifest` classifies every
|
||||
190 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:
|
||||
|
||||
|
||||
+2
-2
@@ -37,9 +37,9 @@ alias fprint-on='sudo authselect enable-feature with-fingerprint'
|
||||
alias fprint-off='sudo authselect disable-feature with-fingerprint'
|
||||
|
||||
# File system
|
||||
alias ls='eza -lh --group-directories-first --icons'
|
||||
alias ls='eza -lh --group-directories-first --icons=auto'
|
||||
alias lsa='ls -a'
|
||||
alias lt='eza --tree --level=2 --long --icons --git'
|
||||
alias lt='eza --tree --level=2 --long --icons=auto --git'
|
||||
alias lta='lt -a'
|
||||
# Fedora's bat installs /usr/bin/bat; batcat is the Debian name.
|
||||
alias ff="fzf --preview 'bat --style=numbers --color=always {}'"
|
||||
|
||||
@@ -244,10 +244,20 @@ local shipped_mode = "4500x3000@60"
|
||||
local shipped_scale = 1.5
|
||||
local shipped_transform = 0
|
||||
|
||||
-- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4
|
||||
-- HBR3, so this relies on DSC. If the display fails to light up or falls back to
|
||||
-- a lower mode, drop this to 8 first.
|
||||
local shipped_bitdepth = 10
|
||||
-- 8-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4
|
||||
-- HBR3 and reliant on DSC, and this panel's link is marginal: every modeset
|
||||
-- retrains it and blanks the screen. 8bpc keeps headroom on the link.
|
||||
--
|
||||
-- Related: directScanoutPolicy is 0 in Panama settings (2026-09-13). With
|
||||
-- scanout on, a fullscreen game whose buffer depth differs from the desktop
|
||||
-- (games ship both 8- and 10-bit swapchains) makes Hyprland change the output
|
||||
-- format, and on amdgpu a format change is a full modeset. Compositing always
|
||||
-- keeps the format fixed, so the link never retrains mid-game.
|
||||
--
|
||||
-- vrrPolicy is also 0 there. VRR on this panel loses sync and blacks out
|
||||
-- (seen on GNOME in July 2026 and again here); a 60Hz panel gains little
|
||||
-- from it anyway.
|
||||
local shipped_bitdepth = 8
|
||||
|
||||
-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
|
||||
local shipped_cm = "auto"
|
||||
|
||||
@@ -125,6 +125,16 @@ hl.window_rule({
|
||||
no_dim = true,
|
||||
})
|
||||
|
||||
-- Steam games never get true fullscreen. Everything Hyprland does only for a
|
||||
-- real fullscreen window (direct scanout, VRR, tearing, auto HDR) makes this
|
||||
-- panel's marginal DP link retrain and blank (2026-09-13). "1 2" keeps the
|
||||
-- window maximized internally while the game believes it is fullscreen, which
|
||||
-- is what borderless windowed looks like from the game's side.
|
||||
hl.window_rule({
|
||||
match = { class = "^steam_app_\\d+$" },
|
||||
fullscreen_state = "1 2",
|
||||
})
|
||||
|
||||
-- Steam itself is a normal window, but its transient popups are a mess.
|
||||
hl.window_rule({
|
||||
match = { class = "^steam$", title = "^(Friends List|Steam Settings|Special Offer.*)$" },
|
||||
@@ -269,16 +279,28 @@ hl.layer_rule({
|
||||
ignore_alpha = 0.2,
|
||||
})
|
||||
|
||||
-- Overview, capture and local screen-reading UI dim the desktop behind them.
|
||||
-- Overview and local screen-reading UI dim the desktop behind them.
|
||||
hl.layer_rule({
|
||||
name = "qs-overlay",
|
||||
match = { namespace = "^qs-(overview|capture|screen-intelligence)$" },
|
||||
match = { namespace = "^qs-(overview|screen-intelligence)$" },
|
||||
blur = true,
|
||||
ignore_alpha = 0.4,
|
||||
dim_around = true,
|
||||
no_screen_share = true,
|
||||
})
|
||||
|
||||
-- The screenshot picker paints an opaque frozen frame edge to edge, so blur and
|
||||
-- dim would be compositor work on pixels nobody sees. no_anim is load-bearing:
|
||||
-- services/Capture.qml unmaps the picker and runs grim 90ms later, and the
|
||||
-- 200ms layersOut fade would otherwise put the half-faded picker, dimming and
|
||||
-- all, into every screenshot.
|
||||
hl.layer_rule({
|
||||
name = "qs-capture",
|
||||
match = { namespace = "^qs-capture$" },
|
||||
no_anim = true,
|
||||
no_screen_share = true,
|
||||
})
|
||||
|
||||
-- Notification toasts. Blurred like every other shell surface -- without this
|
||||
-- the cards are a near-transparent fill sitting directly on the wallpaper and
|
||||
-- read as washed out rather than as glass.
|
||||
|
||||
@@ -27,9 +27,13 @@ PanelWindow {
|
||||
right: true
|
||||
}
|
||||
|
||||
// Fullscreen overlays must not reserve space, or every window on the
|
||||
// workspace gets resized as the picker opens and closes.
|
||||
exclusiveZone: 0
|
||||
// Ignore, and nothing else: reserve no space (or every window on the
|
||||
// workspace resizes as the picker opens and closes) and respect nobody's
|
||||
// (or the window starts under the bar, the full-output freeze frame is
|
||||
// squeezed into a shorter box, and every selection lands one bar-height
|
||||
// off in the real capture). Never pair this with exclusiveZone; see
|
||||
// tests/quickshell/exclusion-idiom-contract.
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WlrLayershell.namespace: "qs-capture" // matched by a layerrule in hypr/rules.lua
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
|
||||
@@ -57,7 +57,6 @@ PanelWindow {
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
|
||||
|
||||
@@ -55,7 +55,6 @@ PanelWindow {
|
||||
color: "transparent"
|
||||
|
||||
// A dock that reserved space would not be intellihiding.
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename.
|
||||
|
||||
@@ -40,7 +40,6 @@ PanelWindow {
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Blurred by the `^qs-popover` rule in hypr/rules.lua; the scrim is painted
|
||||
|
||||
@@ -44,7 +44,6 @@ PanelWindow {
|
||||
// Reserve nothing and respect nothing: the glow is drawn over the whole
|
||||
// output including under the bar and the dock, which is what makes it
|
||||
// visible from wherever the eyes happen to be.
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
color: "transparent"
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ PanelWindow {
|
||||
screen: root.modelData
|
||||
anchors.bottom: true
|
||||
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
implicitWidth: root.desiredWidth
|
||||
implicitHeight: 64
|
||||
|
||||
@@ -22,7 +22,6 @@ PanelWindow {
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Matched by the `qs-overlay` layer rule in hypr/rules.lua — do not rename.
|
||||
|
||||
@@ -34,11 +34,23 @@ Item {
|
||||
visible: root.rowCount > 0
|
||||
implicitHeight: rows.implicitHeight
|
||||
|
||||
// Probing I2C takes on the order of a second, so it waits until the panel
|
||||
// is actually on screen rather than running at shell startup. Monitors do
|
||||
// not come and go, so once is enough.
|
||||
onVisibleChanged: if (visible && !Brightness.scanned) Brightness.refresh()
|
||||
Component.onCompleted: if (root.visible && !Brightness.scanned) Brightness.refresh()
|
||||
// Probing I2C takes on the order of a second, so it waits until the quick
|
||||
// settings panel is actually on screen rather than running at shell
|
||||
// startup. Monitors do not come and go, so once is enough.
|
||||
//
|
||||
// The trigger is the panel opening, not this item becoming visible: on a
|
||||
// machine with no backlight, `visible` stays false until a scan has found
|
||||
// a DDC display, so keying the scan off it would never scan at all.
|
||||
Connections {
|
||||
target: ShellState
|
||||
function onQuickSettingsOpenChanged(): void { root.scanIfOpen(); }
|
||||
}
|
||||
Component.onCompleted: root.scanIfOpen()
|
||||
|
||||
function scanIfOpen(): void {
|
||||
if (ShellState.quickSettingsOpen && !Brightness.scanned)
|
||||
Brightness.refresh();
|
||||
}
|
||||
|
||||
// `-m` is the machine-readable form: name,class,current,percent,max
|
||||
Process {
|
||||
|
||||
@@ -22,7 +22,6 @@ Variants {
|
||||
implicitWidth: 260
|
||||
implicitHeight: 172
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
mask: Region {}
|
||||
|
||||
|
||||
@@ -383,6 +383,13 @@ Lua-configured Hyprland, prints the refusal to stdout, and exits 0. `eval` exits
|
||||
0 on syntax and runtime errors too. The only trustworthy signal that a write
|
||||
landed is reading the value back.
|
||||
|
||||
**`exclusionMode: ExclusionMode.Ignore` goes alone.** Quickshell's `exclusiveZone`
|
||||
setter flips `exclusionMode` back to `Normal` as a side effect, so a window that
|
||||
declares both is at the mercy of which property the QML engine applies last.
|
||||
The 2026-09-14 Qt update changed that order and every full-screen overlay slid
|
||||
under the bar. `tests/quickshell/exclusion-idiom-contract` fails any file that
|
||||
pairs them.
|
||||
|
||||
**The Settings window is tiled.** `implicitWidth` is a hint; the layout decides,
|
||||
and it ranges from a half-screen split to the full display. `SliderRow` stacks
|
||||
its control under the label below 520px. Test narrow.
|
||||
|
||||
@@ -78,7 +78,6 @@ PanelWindow {
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WlrLayershell.namespace: "qs-popover-welcome"
|
||||
|
||||
@@ -341,7 +341,10 @@ esac'
|
||||
property bool record: false
|
||||
property bool intelligence: false
|
||||
// A handful of frames at 60Hz, enough for the compositor to recomposite
|
||||
// the output without the overlay on it.
|
||||
// the output without the overlay on it. Only enough because the
|
||||
// qs-capture layer rule in hypr/rules.lua sets no_anim: with the
|
||||
// default 200ms layersOut fade the half-faded picker would still be
|
||||
// on screen when grim reads it.
|
||||
interval: 90
|
||||
onTriggered: {
|
||||
const r = commitDelay.rect;
|
||||
|
||||
@@ -71,7 +71,7 @@ source "$PANAMA_PATH/bin/ascii"
|
||||
# time. On an upgrade it is worth running only when its package lists or
|
||||
# reviewed installer trust inputs changed, so this hashes them and remembers
|
||||
# the result. The framed, sorted stream includes top-level package files, the
|
||||
# package-stage adapter, the provenance helper, and regular provenance files;
|
||||
# package-stage adapter, every helper it sources, 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:
|
||||
@@ -93,7 +93,10 @@ hash_packages() {
|
||||
|
||||
for fixed_input in \
|
||||
"$PANAMA_PATH/setup/scripts/install-packages" \
|
||||
"$PANAMA_PATH/setup/lib/artifact-provenance"; do
|
||||
"$PANAMA_PATH/setup/lib/artifact-provenance" \
|
||||
"$PANAMA_PATH/setup/lib/chatgpt-package" \
|
||||
"$PANAMA_PATH/setup/lib/extras-catalog" \
|
||||
"$PANAMA_PATH/setup/lib/machine-role"; do
|
||||
[[ -f "$fixed_input" && ! -L "$fixed_input" && -r "$fixed_input" ]] || return 1
|
||||
done
|
||||
|
||||
@@ -102,7 +105,10 @@ hash_packages() {
|
||||
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
|
||||
"$PANAMA_PATH/setup/lib/artifact-provenance" \
|
||||
"$PANAMA_PATH/setup/lib/chatgpt-package" \
|
||||
"$PANAMA_PATH/setup/lib/extras-catalog" \
|
||||
"$PANAMA_PATH/setup/lib/machine-role" || 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"/}"
|
||||
@@ -116,12 +122,11 @@ hash_packages() {
|
||||
}
|
||||
|
||||
packages_needed() {
|
||||
local current_hash recorded_hash
|
||||
local current_hash="$1" recorded_hash
|
||||
|
||||
(( FORCE_PACKAGES )) && return 0
|
||||
(( UPGRADE )) || return 0
|
||||
[[ -r "$PACKAGES_HASH" ]] || return 0
|
||||
current_hash="$(hash_packages)" || return 2
|
||||
recorded_hash="$(cat "$PACKAGES_HASH")" || return 2
|
||||
[[ "$current_hash" != "$recorded_hash" ]]
|
||||
}
|
||||
@@ -130,10 +135,12 @@ packages_needed() {
|
||||
# documents for its markers: a step that did not complete has not happened, and
|
||||
# recording it as done hides it forever.
|
||||
record_packages_hash() {
|
||||
local temporary_hash
|
||||
local starting_hash="$1" current_hash temporary_hash
|
||||
current_hash="$(hash_packages)" || return 1
|
||||
[[ "$current_hash" == "$starting_hash" ]] || return 1
|
||||
mkdir -p "$STATE_DIR"
|
||||
temporary_hash="$(mktemp "$STATE_DIR/.packages-hash.XXXXXX")" || return 1
|
||||
if hash_packages >"$temporary_hash"; then
|
||||
if printf '%s\n' "$starting_hash" >"$temporary_hash"; then
|
||||
mv -f -- "$temporary_hash" "$PACKAGES_HASH"
|
||||
else
|
||||
rm -f -- "$temporary_hash"
|
||||
@@ -142,20 +149,37 @@ record_packages_hash() {
|
||||
}
|
||||
|
||||
# Repository trust is checked before the installer can reach its bootstrap DNF.
|
||||
# Status 78 is reserved for a trust-root failure and is propagated unchanged so
|
||||
# no later stage, especially install-hardware, can invoke DNF with that repo.
|
||||
# Status 78 is reserved for a trust-root failure. It suppresses every stage that
|
||||
# opens DNF -- install-hardware included, which would otherwise pull drivers
|
||||
# through the very repository in doubt -- and the run still exits 78 at the end.
|
||||
#
|
||||
# It suppresses nothing else. Linking dotfiles, skills and user content reads no
|
||||
# repository and installs no package, and a machine whose Terra is in question
|
||||
# still wants its configuration. Refusing the safe work because the unsafe work
|
||||
# is unavailable does not make the machine safer, it just leaves the machine
|
||||
# unconfigured with no way to fix itself. Exiting here instead meant link-skills
|
||||
# never ran on a machine whose Terra was merely unadopted, so ~/.claude/skills
|
||||
# stayed the whole-directory symlink it had been before skills were linked one
|
||||
# by one, and not one shipped skill was reachable.
|
||||
TERRA_TRUST_FAILURE_STATUS=78
|
||||
DNF_STAGES=(install-packages change-settings install-hardware)
|
||||
package_trust_refused=0
|
||||
|
||||
stage_opens_dnf() {
|
||||
local candidate="$1" dnf_stage
|
||||
for dnf_stage in "${DNF_STAGES[@]}"; do
|
||||
[[ "$candidate" == "$dnf_stage" ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
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=$?
|
||||
package_trust_refused=1
|
||||
elif ! "$trust_preflight" --trust-preflight; then
|
||||
printf 'install: package repository trust preflight failed\n' >&2
|
||||
exit "$trust_status"
|
||||
package_trust_refused=1
|
||||
fi
|
||||
|
||||
# ── The interview ────────────────────────────────────────────────────────────
|
||||
@@ -175,7 +199,7 @@ fi
|
||||
# Gated exactly like the interview itself: under --upgrade no questions are
|
||||
# asked, so nothing here is used, and a machine that cannot install gum must
|
||||
# not have that stop an upgrade that never needed it.
|
||||
if (( ! UPGRADE )); then
|
||||
if (( ! UPGRADE && ! package_trust_refused )); then
|
||||
bootstrap=()
|
||||
command -v gum >/dev/null 2>&1 || bootstrap+=(gum)
|
||||
# The probe tools serve only the hardware questions, which a server is never
|
||||
@@ -319,7 +343,7 @@ fi
|
||||
if [[ "$PANAMA_ROLE" == server ]]; then
|
||||
STAGES=(install-packages link-dotfiles link-user setup-server link-server setup-identity)
|
||||
else
|
||||
STAGES=(install-packages link-dotfiles link-skills link-user change-settings link-vicinae-scripts setup-identity install-hardware)
|
||||
STAGES=(install-packages link-dotfiles link-skills link-user link-mcp change-settings link-vicinae-scripts setup-identity install-hardware)
|
||||
fi
|
||||
|
||||
# The two an upgrade drops. Both exist only to act on interview answers, and
|
||||
@@ -342,9 +366,16 @@ for stage in "${STAGES[@]}"; do
|
||||
script="$PANAMA_PATH/setup/scripts/$stage"
|
||||
[[ -x "$script" ]] || continue
|
||||
printf '\n=== %s ===\n' "$stage"
|
||||
if (( package_trust_refused )) && stage_opens_dnf "$stage"; then
|
||||
echo "Skipped: the package repository trust check refused package work."
|
||||
continue
|
||||
fi
|
||||
if [[ "$stage" == install-packages ]]; then
|
||||
package_state_status=0
|
||||
packages_needed || package_state_status=$?
|
||||
package_start_hash="$(hash_packages)" || package_state_status=2
|
||||
if (( package_state_status == 0 )); then
|
||||
packages_needed "$package_start_hash" || package_state_status=$?
|
||||
fi
|
||||
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."
|
||||
@@ -357,16 +388,19 @@ for stage in "${STAGES[@]}"; do
|
||||
fi
|
||||
if "$script"; then
|
||||
if [[ "$stage" == install-packages ]]; then
|
||||
if ! record_packages_hash; then
|
||||
if ! record_packages_hash "$package_start_hash"; then
|
||||
failed+=("$stage")
|
||||
printf '!!! %s could not record its tracked installation inputs\n' "$stage" >&2
|
||||
fi
|
||||
fi
|
||||
else
|
||||
stage_status=$?
|
||||
# A configuration change between the preflight and this stage. Suppress the
|
||||
# remaining DNF stages, keep the safe ones, and carry the status to the end.
|
||||
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"
|
||||
package_trust_refused=1
|
||||
continue
|
||||
fi
|
||||
failed+=("$stage")
|
||||
printf '!!! %s failed\n' "$stage" >&2
|
||||
@@ -392,8 +426,16 @@ done
|
||||
# written for -- and baselining would skip every one of them forever. Every
|
||||
# migration is self-guarding and a no-op where it does not apply, so running
|
||||
# them is the safe direction.
|
||||
#
|
||||
# Held back when package work was refused. A migration is free to run a DNF
|
||||
# transaction -- the ChatGPT package replacement does exactly that -- so the
|
||||
# repositories have to be trustworthy before any of them is allowed to run.
|
||||
# They are not marked applied either, so the next run still has them pending.
|
||||
migrate="$PANAMA_PATH/bin/panama-migrate"
|
||||
if [[ -x "$migrate" ]]; then
|
||||
if (( package_trust_refused )) && [[ -x "$migrate" ]]; then
|
||||
printf '\n=== migrations ===\n'
|
||||
echo "Skipped: the package repository trust check refused package work."
|
||||
elif [[ -x "$migrate" ]]; then
|
||||
printf '\n=== migrations ===\n'
|
||||
if (( UPGRADE )) || [[ -d "$STATE_DIR/migrations" ]]; then
|
||||
"$migrate" run || failed+=(migrations)
|
||||
@@ -441,6 +483,17 @@ else
|
||||
retry='./install'
|
||||
fi
|
||||
|
||||
# Reported last and on its own, because it is not an ordinary stage failure:
|
||||
# everything safe did run, and what did not run is named rather than buried in a
|
||||
# list. The exit status stays 78 so a caller can still tell the two apart.
|
||||
if (( package_trust_refused )); then
|
||||
printf 'Package work was refused: the Terra repository configuration on this\n' >&2
|
||||
printf 'machine is not one Panama can verify. Skipped: %s\n' "${DNF_STAGES[*]}" >&2
|
||||
printf 'Everything that touches no repository was still applied.\n' >&2
|
||||
printf 'Inspect it with: panama diagnose\n' >&2
|
||||
exit "$TERRA_TRUST_FAILURE_STATUS"
|
||||
fi
|
||||
|
||||
if (( ${#failed[@]} == 0 )); then
|
||||
if (( UPGRADE )); then
|
||||
echo "Panama is up to date."
|
||||
|
||||
@@ -8,6 +8,7 @@ declare -gA INSTALLER_PROVENANCE=()
|
||||
|
||||
_primary_key_fingerprints() (
|
||||
local home
|
||||
set -o pipefail
|
||||
home="$(mktemp -d)" || exit 1
|
||||
chmod 700 "$home"
|
||||
trap 'rm -rf -- "$home"' EXIT
|
||||
@@ -19,14 +20,20 @@ _primary_key_fingerprints() (
|
||||
|
||||
key_fingerprint_matches() {
|
||||
local file="$1" expected="$2"
|
||||
local output
|
||||
local -a primary_fingerprints=()
|
||||
mapfile -t primary_fingerprints < <(_primary_key_fingerprints "$file")
|
||||
output="$(_primary_key_fingerprints "$file")" || return 1
|
||||
[[ -n "$output" ]] || return 1
|
||||
mapfile -t primary_fingerprints <<<"$output"
|
||||
[[ ${#primary_fingerprints[@]} -eq 1 && "${primary_fingerprints[0]}" == "$expected" ]]
|
||||
}
|
||||
|
||||
_key_has_one_primary() {
|
||||
local output
|
||||
local -a primary_fingerprints=()
|
||||
mapfile -t primary_fingerprints < <(_primary_key_fingerprints "$1")
|
||||
output="$(_primary_key_fingerprints "$1")" || return 1
|
||||
[[ -n "$output" ]] || return 1
|
||||
mapfile -t primary_fingerprints <<<"$output"
|
||||
[[ ${#primary_fingerprints[@]} -eq 1 ]]
|
||||
}
|
||||
|
||||
@@ -105,6 +112,7 @@ load_installer_provenance() {
|
||||
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 \
|
||||
TERRA_METALINK_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 \
|
||||
|
||||
@@ -35,6 +35,7 @@ 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
|
||||
TERRA_METALINK_BASEURL=https://tetsudou.fyralabs.com/metalink
|
||||
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
|
||||
|
||||
+169
-21
@@ -725,8 +725,17 @@ _publish_repository_pair() {
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# Reads the gpgkey of the single enabled Terra identity out of dnf's effective
|
||||
# configuration, applying the structural safety rules either way: one enabled
|
||||
# identity, named terra, with every signature check turned on.
|
||||
#
|
||||
# With require_pinned set it additionally demands Panama's own reviewed baseurl
|
||||
# and key path. Without it, the answer is just "what trust root is this machine
|
||||
# actually verifying against?" -- the question adoption turns on.
|
||||
_effective_terra_key() {
|
||||
awk -v reviewed_baseurl="${INSTALLER_PROVENANCE[TERRA_BASEURL]}" '
|
||||
awk -v reviewed_baseurl="${INSTALLER_PROVENANCE[TERRA_BASEURL]}" \
|
||||
-v reviewed_metalink="${INSTALLER_PROVENANCE[TERRA_METALINK_BASEURL]}" \
|
||||
-v require_pinned="${1:-}" '
|
||||
function reset_block() {
|
||||
delete values
|
||||
delete seen
|
||||
@@ -746,10 +755,22 @@ _effective_terra_key() {
|
||||
for (key in required) {
|
||||
if (seen[key] != 1) bad = 1
|
||||
}
|
||||
if (require_pinned != "") {
|
||||
if (values["baseurl"] != reviewed_baseurl || values["metalink"] != "" \
|
||||
|| values["mirrorlist"] != "" || values["gpgcheck"] != "1" \
|
||||
|| values["pkg_gpgcheck"] != "1" || values["repo_gpgcheck"] != "1" \
|
||||
|| values["mirrorlist"] != "" \
|
||||
|| values["gpgkey"] != "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama") bad = 1
|
||||
} else if (values["mirrorlist"] != "") {
|
||||
bad = 1
|
||||
} else if (values["baseurl"] == reviewed_baseurl && values["metalink"] == "") {
|
||||
# Already on the reviewed baseurl, just not via the Panama key path.
|
||||
} else if (values["baseurl"] == "" \
|
||||
&& index(values["metalink"], reviewed_metalink "?") == 1) {
|
||||
# The stock terra-release metalink, on the reviewed host.
|
||||
} else {
|
||||
bad = 1
|
||||
}
|
||||
if (values["gpgcheck"] != "1" || values["pkg_gpgcheck"] != "1" \
|
||||
|| values["repo_gpgcheck"] != "1" || values["gpgkey"] == "") bad = 1
|
||||
trusted_key = values["gpgkey"]
|
||||
}
|
||||
BEGIN {
|
||||
@@ -807,12 +828,24 @@ _effective_terra_key() {
|
||||
'
|
||||
}
|
||||
|
||||
_terra_repo_config_dump() {
|
||||
LC_ALL=C dnf --quiet --no-plugins --dump-repo-config='*'
|
||||
}
|
||||
|
||||
# Status 0 is one trusted effective Terra identity, 1 is no enabled Terra
|
||||
# identity, and 2 is an unsafe, duplicated, or unreadable effective state.
|
||||
#
|
||||
# Both this and _terra_adoptable_status take an already-read dump when the
|
||||
# caller needs both verdicts, so the two cannot disagree about a configuration
|
||||
# that changed between them.
|
||||
_terra_effective_status() {
|
||||
local dump gpgkey parse_status=0 local_key
|
||||
dump="$(LC_ALL=C dnf --quiet --no-plugins --dump-repo-config='*')" || return 2
|
||||
gpgkey="$(printf '%s\n' "$dump" | _effective_terra_key)" || parse_status=$?
|
||||
if (( $# > 0 )); then
|
||||
dump="$1"
|
||||
else
|
||||
dump="$(_terra_repo_config_dump)" || return 2
|
||||
fi
|
||||
gpgkey="$(printf '%s\n' "$dump" | _effective_terra_key pinned)" || parse_status=$?
|
||||
(( parse_status == 0 )) || return "$parse_status"
|
||||
[[ "$gpgkey" == 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama' ]] || return 2
|
||||
local_key="$PANAMA_SYSTEM_ETC/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama"
|
||||
@@ -824,18 +857,71 @@ _terra_effective_status() {
|
||||
|| return 2
|
||||
}
|
||||
|
||||
# Whether an effective Terra that is not in Panama's pinned form can be safely
|
||||
# converted into it rather than refused.
|
||||
#
|
||||
# The trust root is the signing key, not the URL it is served from. A machine
|
||||
# that installed Terra the way Terra documents has terra-release's own repo
|
||||
# file: a metalink instead of the reviewed baseurl, and the key at
|
||||
# RPM-GPG-KEY-terra44 rather than Panama's renamed copy. Every signature check
|
||||
# is already on, and that key is the same fingerprint this repository reviewed
|
||||
# and pinned. Cosmetics, in other words -- not a compromised trust root.
|
||||
#
|
||||
# Refusing it outright built a gate with no door. install_terra_repository
|
||||
# declined to touch a machine terra-release had already reached, so an ordinary
|
||||
# Fedora desktop could never reach the pinned state, and a routine `panama
|
||||
# update` died before it ran a single stage. Adoption is the door.
|
||||
#
|
||||
# It is deliberately narrow: the pinned fingerprint must match on both the
|
||||
# reviewed key and the key the machine actually verifies against, and the
|
||||
# gpgkey must be a local file under the system trust directory. An unknown key,
|
||||
# a remote gpgkey, a second enabled Terra, or a disabled signature check is
|
||||
# still a hard refusal.
|
||||
_terra_adoptable_status() {
|
||||
local dump gpgkey parse_status=0 key_file key_path
|
||||
_require_policy_value TERRA_METALINK_BASEURL 'https://tetsudou.fyralabs.com/metalink' || return 2
|
||||
if (( $# > 0 )); then
|
||||
dump="$1"
|
||||
else
|
||||
dump="$(_terra_repo_config_dump)" || return 2
|
||||
fi
|
||||
gpgkey="$(printf '%s\n' "$dump" | _effective_terra_key)" || parse_status=$?
|
||||
(( parse_status == 0 )) || return "$parse_status"
|
||||
key_file="${gpgkey#file://}"
|
||||
[[ "$gpgkey" == "file://$key_file" && "$key_file" == /etc/pki/rpm-gpg/* ]] || return 2
|
||||
[[ "$key_file" != *..* ]] || return 2
|
||||
key_path="$PANAMA_SYSTEM_ETC${key_file#/etc}"
|
||||
[[ -f "$key_path" && ! -L "$key_path" ]] || return 2
|
||||
key_fingerprint_matches "$PANAMA_PATH/setup/provenance/keys/terra44.asc" \
|
||||
"${INSTALLER_PROVENANCE[TERRA_FINGERPRINT]}" \
|
||||
&& key_fingerprint_matches "$key_path" \
|
||||
"${INSTALLER_PROVENANCE[TERRA_FINGERPRINT]}" \
|
||||
|| return 2
|
||||
}
|
||||
|
||||
TERRA_TRUST_FAILURE_STATUS=78
|
||||
|
||||
preflight_terra_trust() {
|
||||
local status=0
|
||||
local status=0 adoptable_status=0 dump
|
||||
_require_policy_value TERRA_BASEURL 'https://repos.fyralabs.com/terra44' \
|
||||
|| return "$TERRA_TRUST_FAILURE_STATUS"
|
||||
_require_policy_value TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F \
|
||||
|| return "$TERRA_TRUST_FAILURE_STATUS"
|
||||
_terra_effective_status || status=$?
|
||||
dump="$(_terra_repo_config_dump)" || {
|
||||
log "Effective Terra repository configuration is not trusted; refusing all package work"
|
||||
return "$TERRA_TRUST_FAILURE_STATUS"
|
||||
}
|
||||
_terra_effective_status "$dump" || status=$?
|
||||
if (( status == 0 || status == 1 )); then
|
||||
return 0
|
||||
fi
|
||||
# Terra signed by the pinned key, on a reviewed endpoint, passes here because
|
||||
# install-packages adopts it into the pinned form before it opens any other
|
||||
# DNF transaction.
|
||||
_terra_adoptable_status "$dump" || adoptable_status=$?
|
||||
if (( adoptable_status == 0 )); then
|
||||
return 0
|
||||
fi
|
||||
log "Effective Terra repository configuration is not trusted; refusing all package work"
|
||||
return "$TERRA_TRUST_FAILURE_STATUS"
|
||||
}
|
||||
@@ -907,19 +993,68 @@ install_rpmfusion_repositories() {
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# Rewrites an adoptable Terra into Panama's pinned form.
|
||||
#
|
||||
# No network and no DNF: terra-release is already installed, so this is only the
|
||||
# key copy and the repository file, published as one pair so a half-written
|
||||
# trust root rolls back. The effective state is re-read afterwards, because the
|
||||
# only acceptable proof that adoption worked is the check that judged it.
|
||||
#
|
||||
# terra-release owns /etc/yum.repos.d/terra.repo, so a later update to that
|
||||
# package restores the stock file. That is fine and deliberate: the next run
|
||||
# adopts it again, which is why adoption has to be repeatable rather than a
|
||||
# one-time migration.
|
||||
adopt_terra_repository() {
|
||||
local work staged_key staged_repo status=0 effective_status=0
|
||||
work="$(mktemp -d)" || return 1
|
||||
chmod 0700 "$work"
|
||||
staged_key="$work/terra44.asc"
|
||||
staged_repo="$work/terra.repo"
|
||||
if ! _stage_reviewed_key "$PANAMA_PATH/setup/provenance/keys/terra44.asc" "$staged_key" \
|
||||
TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F; then
|
||||
rm -rf -- "$work"
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' \
|
||||
'[terra]' \
|
||||
'name=Panama reviewed Terra 44' \
|
||||
"baseurl=${INSTALLER_PROVENANCE[TERRA_BASEURL]}" \
|
||||
'enabled=1' \
|
||||
'gpgcheck=1' \
|
||||
'repo_gpgcheck=1' \
|
||||
'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama' > "$staged_repo"
|
||||
chmod 0600 "$staged_repo"
|
||||
_publish_repository_pair \
|
||||
"$staged_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama \
|
||||
"$staged_repo" /etc/yum.repos.d/terra.repo || status=$?
|
||||
if (( status == 0 )); then
|
||||
_terra_effective_status || effective_status=$?
|
||||
(( effective_status == 0 )) || status="$TERRA_TRUST_FAILURE_STATUS"
|
||||
fi
|
||||
rm -rf -- "$work"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
install_terra_repository() {
|
||||
local work staged_key staged_repo status effective_status=0
|
||||
local work staged_key staged_repo status effective_status=0 adoptable_status=0 dump
|
||||
require_reviewed_fedora_release || return 1
|
||||
_require_policy_value TERRA_BASEURL 'https://repos.fyralabs.com/terra44' || return 1
|
||||
_require_policy_value TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F || return 1
|
||||
_terra_effective_status || effective_status=$?
|
||||
dump="$(_terra_repo_config_dump)" || return 1
|
||||
_terra_effective_status "$dump" || effective_status=$?
|
||||
if (( effective_status == 0 )); then
|
||||
log "Terra repository already configured and verified"
|
||||
return 0
|
||||
elif (( effective_status != 1 )); then
|
||||
_terra_adoptable_status "$dump" || adoptable_status=$?
|
||||
if (( adoptable_status != 0 )); then
|
||||
log "Effective Terra repository configuration is not trusted"
|
||||
return "$TERRA_TRUST_FAILURE_STATUS"
|
||||
fi
|
||||
log "Adopting the existing Terra repository into Panama's reviewed form"
|
||||
adopt_terra_repository
|
||||
return $?
|
||||
fi
|
||||
if rpm -q terra-release >/dev/null 2>&1; then
|
||||
log "terra-release is installed without one trusted enabled Terra repository"
|
||||
return "$TERRA_TRUST_FAILURE_STATUS"
|
||||
@@ -1177,16 +1312,9 @@ if [[ "$ROLE" == server ]]; then
|
||||
fi
|
||||
|
||||
echo -e "\n--- Installing Repositories ---"
|
||||
log "Installing RPM Fusion Free and Nonfree Repositories"
|
||||
install_rpmfusion_repositories > /dev/null
|
||||
log "Enabling Fedora Cisco OpenH264 Repository"
|
||||
# soft: this repo does not exist on every spin, and its absence must not cost
|
||||
# the desktop -- the ordering rule at soft()'s definition applies to the
|
||||
# repository extras just as much as to the codec swaps below.
|
||||
soft "enabling the openh264 repository" sudo dnf config-manager setopt fedora-cisco-openh264.enabled=1
|
||||
log "Installing RPM Fusion AppStream Metadata"
|
||||
soft "the core group update" sudo dnf update @core -y
|
||||
soft "the RPM Fusion appstream metadata" sudo dnf install -y rpmfusion-\*-appstream-data
|
||||
# Terra goes first so a machine whose Terra is enabled but not yet in Panama's
|
||||
# reviewed form is adopted before any other transaction below runs against it.
|
||||
#
|
||||
# Terra bootstraps itself: --repofrompath defines a throwaway repo just long
|
||||
# enough to install terra-release, which then writes the real /etc/yum.repos.d
|
||||
# entry. Doing that a second time is not harmless -- dnf5 refuses the whole
|
||||
@@ -1200,6 +1328,17 @@ soft "the RPM Fusion appstream metadata" sudo dnf install -y rpmfusion-\*-appstr
|
||||
log "Installing Terra Repository"
|
||||
install_terra_repository > /dev/null
|
||||
|
||||
log "Installing RPM Fusion Free and Nonfree Repositories"
|
||||
install_rpmfusion_repositories > /dev/null
|
||||
log "Enabling Fedora Cisco OpenH264 Repository"
|
||||
# soft: this repo does not exist on every spin, and its absence must not cost
|
||||
# the desktop -- the ordering rule at soft()'s definition applies to the
|
||||
# repository extras just as much as to the codec swaps below.
|
||||
soft "enabling the openh264 repository" sudo dnf config-manager setopt fedora-cisco-openh264.enabled=1
|
||||
log "Installing RPM Fusion AppStream Metadata"
|
||||
soft "the core group update" sudo dnf update @core -y
|
||||
soft "the RPM Fusion appstream metadata" sudo dnf install -y rpmfusion-\*-appstream-data
|
||||
|
||||
echo -e "\n--- Installing relevant packages ---"
|
||||
log "Updating all packages. This may take a while"
|
||||
sudo dnf update -y --refresh > /dev/null
|
||||
@@ -1266,8 +1405,17 @@ soft "the multimedia group update" \
|
||||
sync
|
||||
log "Swapping ffmpeg-free for ffmpeg"
|
||||
soft "the ffmpeg swap" sudo dnf swap -y 'ffmpeg-free' 'ffmpeg' --allowerasing
|
||||
log "Swapping mesa-va-drivers for mesa-va-drivers-freeworld"
|
||||
soft "the mesa driver swap" sudo dnf swap -y mesa-va-drivers mesa-va-drivers-freeworld
|
||||
# mesa-va-drivers-freeworld Provides mesa-va-drivers, so once the swap has been
|
||||
# done it is the only thing providing the name we ask dnf to remove. `dnf swap`
|
||||
# then resolves to removing and installing the same package in one transaction
|
||||
# and fails with "conflicting requests", which reports install-packages as a
|
||||
# failed stage on every subsequent run. Guard on the end state instead.
|
||||
if rpm -q mesa-va-drivers-freeworld >/dev/null 2>&1; then
|
||||
log "mesa-va-drivers-freeworld is already installed; swap not needed"
|
||||
else
|
||||
log "Swapping mesa-va-drivers for mesa-va-drivers-freeworld"
|
||||
soft "the mesa driver swap" sudo dnf swap -y mesa-va-drivers mesa-va-drivers-freeworld
|
||||
fi
|
||||
log "Upgrading Multimedia group with optional packages"
|
||||
soft "the optional Multimedia upgrade" sudo dnf4 group upgrade -y --with-optional Multimedia
|
||||
log "Installing GStreamer plugins (bad, good, base)"
|
||||
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# MCP servers, registered with the agent runtimes on this machine.
|
||||
#
|
||||
# An MCP server is a URL plus a bearer token, and the token is the whole reason
|
||||
# this is a stage rather than a manifest line. Panama is a public repository, so
|
||||
# the tokens cannot live in it, and neither runtime keeps its server list in a
|
||||
# file that could be symlinked anyway: Codex writes them into config.toml beside
|
||||
# dozens of unrelated settings, and Claude Code into ~/.claude.json. There is no
|
||||
# file to point at, so this registers them through the runtime's own CLI.
|
||||
#
|
||||
# What is tracked is user/agents/mcp/servers, which names each server and which
|
||||
# variable carries its token. What is not tracked is user/agents/mcp/env, which
|
||||
# holds the tokens. A new machine gets the servers by dropping its own env file
|
||||
# beside the tracked one and re-running ./install.
|
||||
#
|
||||
# This is personal content, so it obeys the same interview decision link-user
|
||||
# does. A machine that never said yes registers nothing.
|
||||
#
|
||||
# Only Claude Code is handled. Codex stores its servers inside config.toml, and
|
||||
# rewriting a section of somebody's live TOML is a worse failure mode than
|
||||
# leaving two lines for them to paste once.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*" >&2; }
|
||||
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
MCP_DIR="$PANAMA_PATH/user/agents/mcp"
|
||||
SERVERS="$MCP_DIR/servers"
|
||||
ENV_FILE="$MCP_DIR/env"
|
||||
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
|
||||
DECISION="$STATE_DIR/user-content"
|
||||
|
||||
[[ -r "$SERVERS" ]] || { log "No MCP server list; nothing to register."; exit 0; }
|
||||
|
||||
# The same gate link-user uses, read the same way, so one answer governs all
|
||||
# personal content rather than two stages disagreeing about it.
|
||||
decision="$([[ -r "$DECISION" ]] && cat "$DECISION" || printf 'no')"
|
||||
if [[ "$decision" != "yes" ]]; then
|
||||
log "Personal content is not enabled on this machine; no MCP servers registered."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v claude >/dev/null 2>&1; then
|
||||
log "Claude Code is not installed; nothing to register."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Tokens are optional. A machine without the env file still registers any server
|
||||
# that needs no header, and says which ones it skipped rather than failing.
|
||||
if [[ -r "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
else
|
||||
warn "No $ENV_FILE; servers needing a token will be skipped."
|
||||
fi
|
||||
|
||||
registered=0
|
||||
skipped=0
|
||||
|
||||
while read -r name transport url token_var _rest; do
|
||||
case "${name:-}" in ''|'#'*) continue ;; esac
|
||||
if [[ -z "${transport:-}" || -z "${url:-}" ]]; then
|
||||
warn "Ignoring malformed row for '$name'."
|
||||
continue
|
||||
fi
|
||||
|
||||
header=()
|
||||
if [[ -n "${token_var:-}" ]]; then
|
||||
token="${!token_var:-}"
|
||||
if [[ -z "$token" ]]; then
|
||||
warn "Skipping $name: $token_var is not set in $ENV_FILE."
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
header=(-H "Authorization: $token")
|
||||
fi
|
||||
|
||||
# Re-registering is how this stays idempotent across upgrades, and how a
|
||||
# rotated token reaches the runtime. Removing first avoids the CLI refusing
|
||||
# a name it already knows. Neither call may print the token, so both are
|
||||
# quiet unless they fail.
|
||||
claude mcp remove "$name" -s user >/dev/null 2>&1 || true
|
||||
if claude mcp add --transport "$transport" "$name" "$url" "${header[@]}" \
|
||||
-s user >/dev/null 2>&1; then
|
||||
log "Registered $name."
|
||||
registered=$((registered + 1))
|
||||
else
|
||||
warn "Could not register $name."
|
||||
skipped=$((skipped + 1))
|
||||
fi
|
||||
done <"$SERVERS"
|
||||
|
||||
log "MCP servers: $registered registered, $skipped skipped."
|
||||
log "Claude Code loads them at start, so restart a running session to pick them up."
|
||||
@@ -85,10 +85,16 @@ if [[ -d "$extensions_source" ]] && command -v npm >/dev/null 2>&1; then
|
||||
# alone takes long enough to be worth not repeating on every re-run of
|
||||
# a stage that is otherwise nearly instant.
|
||||
built="$vicinae_data_dir/extensions/$name"
|
||||
if [[ -d "$built" && "$extension/src" -ot "$built" ]]; then
|
||||
if [[ -d "$built" ]]; then
|
||||
newer_source=''
|
||||
if newer_source="$(find "$extension/src" -type f -newer "$built" -print -quit)" \
|
||||
&& [[ -z "$newer_source" \
|
||||
&& ! "$extension/package.json" -nt "$built" \
|
||||
&& ! "$extension/package-lock.json" -nt "$built" ]]; then
|
||||
printf 'Vicinae extension %s is already built\n' "$name"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
printf 'Building Vicinae extension %s\n' "$name"
|
||||
if ! (cd "$extension" && npm ci --silent >/dev/null 2>&1 && npm run build >/dev/null 2>&1); then
|
||||
|
||||
@@ -82,6 +82,7 @@ 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/exclusion-idiom-contract
|
||||
hermetic tests/quickshell/fingerprint-contract
|
||||
# Reads the host firewall state through the production firewall helper.
|
||||
live-host tests/quickshell/firewall-contract
|
||||
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# A window that wants the whole output must ask for ExclusionMode.Ignore and
|
||||
# nothing else. Quickshell's exclusiveZone setter forces exclusionMode back to
|
||||
# Normal as a side effect, so a window declaring both
|
||||
#
|
||||
# exclusiveZone: 0
|
||||
# exclusionMode: ExclusionMode.Ignore
|
||||
#
|
||||
# ends up with whichever property the QML engine applied last, and that order
|
||||
# is not ours to control: the 2026-09-14 Qt/Quickshell update flipped it, every
|
||||
# full-screen overlay slid down under the bar, and the screenshot picker's
|
||||
# frozen frame stopped lining up with the screen it was a picture of. This pins
|
||||
# the order-independent idiom: Ignore alone, never paired with exclusiveZone.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
qs="$repo_dir/config/dot/quickshell"
|
||||
|
||||
fail() { printf 'exclusion idiom contract: %s\n' "$1" >&2; exit 1; }
|
||||
[[ -d "$qs/modules" ]] || fail "missing $qs/modules"
|
||||
|
||||
python3 - "$qs" <<'PY'
|
||||
import re, sys, pathlib
|
||||
qs = pathlib.Path(sys.argv[1])
|
||||
problems = []
|
||||
ignore = re.compile(r'\bexclusionMode\s*:\s*ExclusionMode\.Ignore\b')
|
||||
zone = re.compile(r'^\s*(?:WlrLayershell\.)?exclusiveZone\s*:', re.M)
|
||||
checked = 0
|
||||
for path in sorted(qs.rglob('*.qml')):
|
||||
text = path.read_text()
|
||||
if not ignore.search(text):
|
||||
continue
|
||||
checked += 1
|
||||
for m in zone.finditer(text):
|
||||
line = text.count('\n', 0, m.start()) + 1
|
||||
problems.append(f"{path.relative_to(qs)}:{line}: exclusiveZone set in a file that uses "
|
||||
"ExclusionMode.Ignore; drop it, Ignore already means -1")
|
||||
if checked == 0:
|
||||
problems.append("no window uses ExclusionMode.Ignore; the overlays this pins are gone")
|
||||
if problems:
|
||||
print('\n'.join(problems), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
@@ -65,6 +65,12 @@ trap 'rm -rf "$work"' EXIT
|
||||
|
||||
filter="$(sed -n '/^packages_in()/,/^}/p' "$installer")"
|
||||
loop="$(sed -n '/^install_extra_category()/,/^}/p' "$installer")"
|
||||
# install_extra_category verifies the Flathub remote before installing a
|
||||
# flatpak, and records a soft failure when it cannot. Both live outside the
|
||||
# extracted function and have contracts of their own, so they stand in here as
|
||||
# trusted -- what is under test is which targets reach which installer.
|
||||
deps='ensure_flathub_remote() { :; }
|
||||
softly_failed=()'
|
||||
[[ -n "$filter" && -n "$loop" ]] || {
|
||||
printf 'extras contract: install-packages no longer defines packages_in and install_extra_category\n' >&2
|
||||
exit 1
|
||||
@@ -99,6 +105,7 @@ LIST
|
||||
source "$catalog"
|
||||
eval "$filter"
|
||||
eval "$loop"
|
||||
eval "$deps"
|
||||
install_extra_category "$fixture"
|
||||
)
|
||||
|
||||
@@ -132,6 +139,7 @@ printf 'flatpak:org.example.OnlyFlatpak\n' >"$flatpak_only"
|
||||
source "$catalog"
|
||||
eval "$filter"
|
||||
eval "$loop"
|
||||
eval "$deps"
|
||||
install_extra_category "$flatpak_only"
|
||||
)
|
||||
flatpak_only_status=$?
|
||||
@@ -149,6 +157,7 @@ grep -q 'flatpak install -y flathub org.example.OnlyFlatpak' <<<"$(cat "$calls"
|
||||
source "$catalog"
|
||||
eval "$filter"
|
||||
eval "$loop"
|
||||
eval "$deps"
|
||||
EXTRAS_DIR="$extras_dir"
|
||||
for extra in ${PANAMA_EXTRAS:-}; do
|
||||
[[ -f "$EXTRAS_DIR/$extra" ]] && install_extra_category "$EXTRAS_DIR/$extra"
|
||||
|
||||
@@ -123,7 +123,8 @@ grep -q '/etc/profile.d/nvm.sh' "$stage" \
|
||||
|| note 'the extension build never sources nvm, so npm is missing on any machine without a system node'
|
||||
|
||||
# node_modules is a dependency tree, not configuration.
|
||||
git -C "$repo_dir" check-ignore -q "$extension/node_modules" 2>/dev/null \
|
||||
git -C "$repo_dir" check-ignore --no-index -q \
|
||||
"$extension/node_modules/package.json" 2>/dev/null \
|
||||
|| note 'the extension node_modules is not gitignored'
|
||||
|
||||
# npm must honour the committed dependency graph. This disposable fixture
|
||||
@@ -167,6 +168,66 @@ stage_output="$(PATH="$fixture_root/bin:$PATH" PANAMA_PATH="$fixture_root" \
|
||||
cmp -s -- "$lock_before" "$lockfile" \
|
||||
|| note 'a rejected Vicinae lockfile mismatch changed package-lock.json'
|
||||
|
||||
# Editing an existing source file does not change its parent directory's
|
||||
# timestamp, so freshness must inspect files rather than the src directory.
|
||||
freshness_root="$fixture_root/freshness"
|
||||
mkdir -p "$freshness_root/config/local/share/vicinae/scripts" \
|
||||
"$freshness_root/config/local/share/vicinae/extensions/panama-search/src" \
|
||||
"$freshness_root/bin"
|
||||
freshness_extension="$freshness_root/config/local/share/vicinae/extensions/panama-search"
|
||||
cp -- "$manifest" "$freshness_extension/package.json"
|
||||
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/package-lock.json" \
|
||||
"$freshness_extension/package-lock.json"
|
||||
cp -- "$extension/src/search.tsx" "$freshness_extension/src/search.tsx"
|
||||
cat >"$freshness_root/bin/npm" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >>"${NPM_LOG:?}"
|
||||
if [[ "${1:-}" == ci ]]; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${1:-} ${2:-}" == 'run build' ]]; then
|
||||
built="${VICINAE_DATA_DIR:?}/extensions/$(basename "$PWD")"
|
||||
mkdir -p "$built"
|
||||
touch "$built"
|
||||
exit 0
|
||||
fi
|
||||
exit 64
|
||||
EOF
|
||||
chmod +x "$freshness_root/bin/npm"
|
||||
: >"$freshness_root/npm.log"
|
||||
run_freshness_stage() {
|
||||
PATH="$freshness_root/bin:$PATH" PANAMA_PATH="$freshness_root" \
|
||||
VICINAE_DATA_DIR="$freshness_root/vicinae-data" \
|
||||
NPM_LOG="$freshness_root/npm.log" bash "$stage" >/dev/null 2>&1
|
||||
}
|
||||
run_freshness_stage || note 'the Vicinae freshness fixture did not build initially'
|
||||
initial_builds="$(grep -c '^run build$' "$freshness_root/npm.log")"
|
||||
run_freshness_stage || note 'the unchanged Vicinae freshness fixture failed'
|
||||
unchanged_builds="$(grep -c '^run build$' "$freshness_root/npm.log")"
|
||||
[[ "$unchanged_builds" == "$initial_builds" ]] \
|
||||
|| note 'an unchanged Vicinae extension rebuilt unnecessarily'
|
||||
touch -d '2030-01-01 UTC' "$freshness_extension/src/search.tsx"
|
||||
run_freshness_stage || note 'the source-changed Vicinae freshness fixture failed'
|
||||
source_changed_builds="$(grep -c '^run build$' "$freshness_root/npm.log")"
|
||||
[[ "$source_changed_builds" -eq $(( initial_builds + 1 )) ]] \
|
||||
|| note 'editing an existing Vicinae source file did not trigger a rebuild'
|
||||
|
||||
built_extension="$freshness_root/vicinae-data/extensions/panama-search"
|
||||
touch -r "$built_extension" "$freshness_extension/src/search.tsx"
|
||||
touch -d '2031-01-01 UTC' "$freshness_extension/package.json"
|
||||
run_freshness_stage || note 'the manifest-changed Vicinae freshness fixture failed'
|
||||
manifest_changed_builds="$(grep -c '^run build$' "$freshness_root/npm.log")"
|
||||
[[ "$manifest_changed_builds" -eq $(( source_changed_builds + 1 )) ]] \
|
||||
|| note 'changing a Vicinae package.json did not trigger a rebuild'
|
||||
|
||||
touch -r "$built_extension" "$freshness_extension/package.json" \
|
||||
"$freshness_extension/src/search.tsx"
|
||||
touch -d '2032-01-01 UTC' "$freshness_extension/package-lock.json"
|
||||
run_freshness_stage || note 'the lockfile-changed Vicinae freshness fixture failed'
|
||||
lockfile_changed_builds="$(grep -c '^run build$' "$freshness_root/npm.log")"
|
||||
[[ "$lockfile_changed_builds" -eq $(( manifest_changed_builds + 1 )) ]] \
|
||||
|| note 'changing a Vicinae package-lock.json did not trigger a rebuild'
|
||||
|
||||
# ── Report ───────────────────────────────────────────────────────────────────
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
|
||||
@@ -164,6 +164,31 @@ expect_failure key_fingerprint_matches "$fixtures/fixture-key.asc" '000000000000
|
||||
cat "$fixtures/fixture-key.asc" "$fixtures/wrong-signer-key.asc" > "$test_tmp/combined-key.asc"
|
||||
expect_failure key_fingerprint_matches "$test_tmp/combined-key.asc" "$fixture_fingerprint"
|
||||
|
||||
# A parser must not accept plausible output from a GPG process that failed.
|
||||
# The later import and verify calls succeed so both public helpers depend on
|
||||
# the show-only producer's status rather than failing for an unrelated reason.
|
||||
producer_failure_bin="$test_tmp/gpg-producer-failure-bin"
|
||||
mkdir "$producer_failure_bin"
|
||||
cat > "$producer_failure_bin/gpg" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
if [[ " \$* " == *' --import-options show-only '* ]]; then
|
||||
printf 'pub:::::::::\n'
|
||||
printf 'fpr:::::::::$fixture_fingerprint:\n'
|
||||
exit 42
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$producer_failure_bin/gpg"
|
||||
expect_failure env PATH="$producer_failure_bin:$PATH" bash -c '
|
||||
source "$1"
|
||||
key_fingerprint_matches "$2" "$3"
|
||||
' _ "$repo_dir/setup/lib/artifact-provenance" "$fixtures/fixture-key.asc" "$fixture_fingerprint"
|
||||
expect_failure env PATH="$producer_failure_bin:$PATH" bash -c '
|
||||
source "$1"
|
||||
verify_detached_signature "$2" "$3" "$4"
|
||||
' _ "$repo_dir/setup/lib/artifact-provenance" "$fixtures/fixture-key.asc" \
|
||||
"$fixtures/SHASUMS256.txt.asc" "$fixtures/SHASUMS256.txt"
|
||||
|
||||
expect_success verify_detached_signature \
|
||||
"$fixtures/fixture-key.asc" "$fixtures/SHASUMS256.txt.asc" "$fixtures/SHASUMS256.txt"
|
||||
expect_failure verify_detached_signature \
|
||||
@@ -869,7 +894,16 @@ if [[ -n "$query" ]]; then
|
||||
trusted|wrong-key) mode=trusted ;;
|
||||
nogpg) mode=legacy ;;
|
||||
wrong-url) mode=override-url ;;
|
||||
stock|stock-wrong-key) mode=stock ;;
|
||||
esac
|
||||
# Adoption rewrites the repository file. Once it is the pinned form the
|
||||
# dump has to say so, or the re-verification adoption performs on itself
|
||||
# could never pass.
|
||||
if [[ "$mode" == stock && -f "$STUB_ETC/yum.repos.d/terra.repo" ]] \
|
||||
&& grep -q '^gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama$' \
|
||||
"$STUB_ETC/yum.repos.d/terra.repo"; then
|
||||
mode=trusted
|
||||
fi
|
||||
if [[ "$mode" == auto && -f "$STUB_ETC/yum.repos.d/terra.repo" ]] \
|
||||
&& grep -q '^baseurl=https://repos.fyralabs.com/terra44$' "$STUB_ETC/yum.repos.d/terra.repo"; then
|
||||
mode=trusted
|
||||
@@ -901,6 +935,13 @@ if [[ -n "$query" ]]; then
|
||||
printf 'metalink = https://tetsudou.fyralabs.com/terra44\nmirrorlist = \n'
|
||||
printf 'pkg_gpgcheck = 0\nrepo_gpgcheck = 0\n'
|
||||
;;
|
||||
stock)
|
||||
printf '======== "terra" repository configuration: ========\n'
|
||||
printf 'baseurl = \nenabled = 1\ngpgcheck = 1\n'
|
||||
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44\n'
|
||||
printf 'metalink = https://tetsudou.fyralabs.com/metalink?repo=terra44&arch=x86_64\n'
|
||||
printf 'mirrorlist = \npkg_gpgcheck = 1\nrepo_gpgcheck = 1\n'
|
||||
;;
|
||||
override-url)
|
||||
printf '======== "terra" repository configuration: ========\n'
|
||||
printf 'baseurl = https://evil.invalid/terra44\nenabled = 1\ngpgcheck = 1\n'
|
||||
@@ -1095,6 +1136,17 @@ run_installer_function() {
|
||||
printf '[terra]\nbaseurl=https://evil.invalid/terra44\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n' \
|
||||
> "$case_root/etc/yum.repos.d/terra.repo"
|
||||
;;
|
||||
stock|stock-wrong-key)
|
||||
if [[ "${STUB_TERRA_REPO_MODE}" == stock ]]; then
|
||||
cp "$installer_fixture/setup/provenance/keys/terra44.asc" \
|
||||
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44"
|
||||
else
|
||||
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
||||
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44"
|
||||
fi
|
||||
printf '[terra]\nmetalink=https://tetsudou.fyralabs.com/metalink?repo=terra44&arch=$basearch\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44\n' \
|
||||
> "$case_root/etc/yum.repos.d/terra.repo"
|
||||
;;
|
||||
wrong-key)
|
||||
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
||||
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama"
|
||||
@@ -1745,6 +1797,39 @@ for mode in nogpg wrong-url wrong-key absent; do
|
||||
|| fail "untrusted existing Terra $mode state reached a mutation"
|
||||
done
|
||||
|
||||
# A machine that installed Terra the way Terra documents it. The repository file
|
||||
# is terra-release's own -- a metalink, and the key at its stock path -- so it is
|
||||
# not Panama's pinned form, but it IS the fingerprint this repository reviewed,
|
||||
# with every signature check on. That is an adoption, not a compromise.
|
||||
#
|
||||
# Refusing it was a gate with no door: the ordinary Fedora desktop could never
|
||||
# reach the pinned state, and status 78 then stopped every stage of every run,
|
||||
# including the ones that never open DNF.
|
||||
reset_installer_fixture
|
||||
STUB_TERRA_INSTALLED=1 STUB_TERRA_REPO_MODE=stock \
|
||||
expect_success run_installer_function terra-stock-preflight preflight_terra_trust
|
||||
|
||||
reset_installer_fixture
|
||||
STUB_TERRA_INSTALLED=1 STUB_TERRA_REPO_MODE=stock \
|
||||
expect_success run_installer_function terra-stock-adopt install_terra_repository
|
||||
terra_adopted="$test_tmp/cases/terra-stock-adopt/etc/yum.repos.d/terra.repo"
|
||||
grep -qx 'baseurl=https://repos.fyralabs.com/terra44' "$terra_adopted" \
|
||||
|| fail 'adoption left Terra off the reviewed baseurl'
|
||||
grep -qx 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama' "$terra_adopted" \
|
||||
|| fail 'adoption left Terra off the reviewed key path'
|
||||
grep -q 'metalink' "$terra_adopted" \
|
||||
&& fail 'adoption kept the metalink it was supposed to replace'
|
||||
[[ "$(<"$test_tmp/cases/terra-stock-adopt/commands.log")" != *'dnf:install'* ]] \
|
||||
|| fail 'adoption opened a DNF transaction it does not need'
|
||||
|
||||
# Adoption is anchored on the fingerprint, not the URL. The same stock shape
|
||||
# verifying against a key that is not Terra's is still a hard refusal.
|
||||
reset_installer_fixture
|
||||
STUB_TERRA_INSTALLED=1 STUB_TERRA_REPO_MODE=stock-wrong-key \
|
||||
expect_failure run_installer_function terra-stock-wrong-key install_terra_repository
|
||||
[[ "$(<"$test_tmp/cases/terra-stock-wrong-key/commands.log")" != *'sudo:'* ]] \
|
||||
|| fail 'a stock Terra signed by an unreviewed key reached a mutation'
|
||||
|
||||
# An optional security field may be absent, but duplicates are malformed even
|
||||
# when one copy looks safe. These cases catch the absent/duplicate conflation.
|
||||
for duplicate_case in \
|
||||
|
||||
@@ -39,6 +39,12 @@ trap 'rm -rf "$tmp"' EXIT
|
||||
STAGE_NAMES=(install-packages link-dotfiles link-skills link-user change-settings
|
||||
link-vicinae-scripts setup-server link-server setup-identity
|
||||
install-hardware)
|
||||
PACKAGE_BEHAVIOR_INPUTS=(
|
||||
setup/lib/artifact-provenance
|
||||
setup/lib/chatgpt-package
|
||||
setup/lib/extras-catalog
|
||||
setup/lib/machine-role
|
||||
)
|
||||
|
||||
copy_hash_inputs() {
|
||||
local root="$1" source relative
|
||||
@@ -50,8 +56,10 @@ copy_hash_inputs() {
|
||||
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"
|
||||
for relative in "${PACKAGE_BEHAVIOR_INPUTS[@]}"; do
|
||||
mkdir -p "$(dirname "$root/$relative")"
|
||||
cp -- "$repo_dir/$relative" "$root/$relative"
|
||||
done
|
||||
}
|
||||
|
||||
# A PANAMA_PATH that looks enough like the real one for install to run, and
|
||||
@@ -255,7 +263,7 @@ grep -qx 'install-packages' <<<"$ran_forced" \
|
||||
# Dynamically discovering them makes this fail when a new reviewed input is
|
||||
# added but omitted from hash_packages.
|
||||
for relative in "${package_inputs[@]}" "${provenance_inputs[@]}" \
|
||||
'setup/scripts/install-packages' 'setup/lib/artifact-provenance'; do
|
||||
'setup/scripts/install-packages' "${PACKAGE_BEHAVIOR_INPUTS[@]}"; do
|
||||
printf 'changed %s\n' "$relative" >>"$tmp/a/$relative"
|
||||
install_status=0
|
||||
ran_input_changed="$(run_install "$tmp/a" --upgrade)" || install_status=$?
|
||||
@@ -282,7 +290,7 @@ 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 fixed_input in setup/scripts/install-packages "${PACKAGE_BEHAVIOR_INPUTS[@]}"; do
|
||||
for case_name in missing directory symlink unreadable; do
|
||||
case_root="$tmp/hash-${fixed_input//\//-}-$case_name"
|
||||
build_fixture "$case_root"
|
||||
@@ -320,6 +328,26 @@ grep -qx 'install-packages' <<<"$ran_hash_failure" \
|
||||
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'
|
||||
|
||||
# Inputs changed while install-packages was running were not the inputs it
|
||||
# consumed at the start. Do not stamp the later bytes as successfully applied.
|
||||
build_fixture "$tmp/hash-mid-stage-drift"
|
||||
cat >"$tmp/hash-mid-stage-drift/setup/scripts/install-packages" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "${1:-}" == --trust-preflight ]]; then
|
||||
printf 'trust-preflight\n' >>"$PANAMA_RAN"
|
||||
exit 0
|
||||
fi
|
||||
printf 'install-packages\n' >>"$PANAMA_RAN"
|
||||
printf '\nchanged during package installation\n' >>"$PANAMA_PATH/setup/lib/machine-role"
|
||||
EOF
|
||||
chmod +x "$tmp/hash-mid-stage-drift/setup/scripts/install-packages"
|
||||
install_status=0
|
||||
run_install "$tmp/hash-mid-stage-drift" --upgrade >/dev/null || install_status=$?
|
||||
[[ "$install_status" -ne 0 ]] \
|
||||
|| note 'mid-stage package input drift returned success'
|
||||
[[ ! -e "$tmp/hash-mid-stage-drift/state/panama/packages-hash" ]] \
|
||||
|| note 'mid-stage package input drift stamped bytes the stage did not start with'
|
||||
|
||||
# A failing stage must not record the hash, or the failure is hidden forever.
|
||||
build_fixture "$tmp/c" 1
|
||||
install_status=0
|
||||
@@ -332,30 +360,48 @@ 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.
|
||||
# An untrusted Terra root is not an ordinary package failure, and it is not a
|
||||
# reason to abandon the machine either. It suppresses the stages that open DNF
|
||||
# and the migrations, which are free to run a transaction of their own. Every
|
||||
# stage that only links configuration still runs, and the status stays 78.
|
||||
SAFE_STAGES=(link-dotfiles link-skills link-user link-vicinae-scripts)
|
||||
DNF_SUPPRESSED=(install-packages change-settings install-hardware)
|
||||
|
||||
assert_trust_refusal() {
|
||||
local root="$1" label="$2" suppressed safe
|
||||
for suppressed in "${DNF_SUPPRESSED[@]}"; do
|
||||
grep -qx "$suppressed" "$root/ran" \
|
||||
&& note "$label still ran $suppressed"
|
||||
done
|
||||
grep -q '^migrate ' "$root/ran" \
|
||||
&& note "$label still ran migrations, which may open a DNF transaction"
|
||||
for safe in "${SAFE_STAGES[@]}"; do
|
||||
grep -qx "$safe" "$root/ran" \
|
||||
|| note "$label suppressed $safe, which touches no repository"
|
||||
done
|
||||
}
|
||||
|
||||
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'/,}"
|
||||
grep -qx 'trust-preflight' "$tmp/terra-preflight-hard/ran" \
|
||||
|| note 'initial Terra trust fixture never reached the preflight'
|
||||
assert_trust_refusal "$tmp/terra-preflight-hard" 'initial Terra trust failure'
|
||||
|
||||
# The trust verifier is itself mandatory. Losing its executable adapter must
|
||||
# fail closed before interview, bootstrap, or stage work.
|
||||
# The trust verifier is itself mandatory. Losing its executable adapter refuses
|
||||
# package work exactly as a failing verdict does, rather than being ignored.
|
||||
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'
|
||||
assert_trust_refusal "$tmp/terra-preflight-missing" 'missing Terra trust verifier'
|
||||
|
||||
# The package stage repeats the preflight to close a configuration-change race.
|
||||
# Its hard status must also stop link stages and install-hardware immediately.
|
||||
# Its hard status suppresses the DNF stages that would have followed it.
|
||||
build_fixture "$tmp/terra-stage-hard" 78 0
|
||||
install_status=0
|
||||
run_install "$tmp/terra-stage-hard" >/dev/null || install_status=$?
|
||||
@@ -363,10 +409,16 @@ run_install "$tmp/terra-stage-hard" >/dev/null || install_status=$?
|
||||
|| 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
|
||||
for suppressed in change-settings install-hardware; do
|
||||
grep -qx "$suppressed" "$tmp/terra-stage-hard/ran" \
|
||||
&& note "stage-time Terra trust failure still ran $suppressed"
|
||||
done
|
||||
grep -q '^migrate ' "$tmp/terra-stage-hard/ran" \
|
||||
&& note 'stage-time Terra trust failure still ran migrations'
|
||||
for safe in "${SAFE_STAGES[@]}"; do
|
||||
grep -qx "$safe" "$tmp/terra-stage-hard/ran" \
|
||||
|| note "stage-time Terra trust failure suppressed $safe"
|
||||
done
|
||||
|
||||
# A full install always runs the stage, whatever any recorded hash says.
|
||||
build_fixture "$tmp/d"
|
||||
|
||||
@@ -62,6 +62,40 @@ I wanted to share some of my preferences here so we can be more aligned as we wo
|
||||
|
||||
---
|
||||
|
||||
# The prose bar
|
||||
|
||||
Anything a person other than me will read gets a pattern pass before it ships. MR and PR
|
||||
descriptions, ticket fields, deliverable documents, reports, anything handed to a
|
||||
teammate or a client. Invoke the `unslop` skill and apply its pattern detection.
|
||||
|
||||
- Skip its "Adding soul" section. Opinions and deliberate mess are right for an essay and
|
||||
wrong for a risk table or a status field. These want plain, factual, and specific.
|
||||
- No em dashes, en dashes, semicolons, or arrow glyphs in prose. This is stricter than
|
||||
unslop's own punctuation pattern, and mine wins. Grep the file before calling it done.
|
||||
- Passive voice is the usual offender in technical writing, not puffery. "X was verified"
|
||||
wants to be "I verified X", which is shorter and says who did it.
|
||||
- Short reference cells still count. Three sentences wrapped around a filename is prose.
|
||||
- Never rewrite generated output. If a tool produced a block, an audit verdict or a test
|
||||
report, regenerate it rather than editing its words.
|
||||
- Write it as me, in first person. Never use my name or a stand-in for it. Not "Gib
|
||||
decided", not "the author", not "per the developer". If a sentence needs an actor, it
|
||||
is "I".
|
||||
- Never flag one of my own decisions as a deviation. Don't write that something was out
|
||||
of scope, that an AC or dev review said otherwise, that an earlier story should have
|
||||
caught it, or that a direction was reversed. I write the tickets, so I am allowed to
|
||||
change them, and a dev review is a proposal rather than a contract. Calling it out
|
||||
reads as my own AI tattling on me. It waves a red flag at a nothing burger, costs the
|
||||
PM time investigating a non-issue, and leaves them thinking I did something wrong.
|
||||
State what the change is and why it belongs where it is, then stop.
|
||||
- If a decision feels big enough that you want to narrate it, ask me about it before
|
||||
writing rather than annotating it in the document. Being aligned first is the point,
|
||||
and the writing should read as though we always were.
|
||||
|
||||
Files only I read are exempt: plans, scratch notes, working records. Those can name me
|
||||
and record who decided what, because that history is useful.
|
||||
|
||||
---
|
||||
|
||||
# Merge Requests
|
||||
|
||||
- Make sure titles follow conventions from the repo.
|
||||
@@ -84,5 +118,8 @@ I wanted to share some of my preferences here so we can be more aligned as we wo
|
||||
- Henry reviews all of my code & up until recently, would also write all the dev reviews for all the stories I completed as well. Nowadays, I write them myself & he reviews those too.
|
||||
- Henry likes work to be very considerate & he always prefers solutions that result in 0 downtime.
|
||||
- Despite the fact that Command Center does not have many users & the impact of it being down for a few minutes is small, Henry still leans on the side of solutions to problems that don't result in prod being down ever, even for just a few minutes during the build process. So our solutions should always keep that in mind. Any code that isn't considering everything & could result in a bug will probably be flagged by him, so its worthwhile to do right the first time!
|
||||
- Josh - Tech Lead for the APSCA project (apsca_next)
|
||||
- On APSCA, Josh is my lead instead of Henry. He reviews my code and writes the dev reviews, spikes, and proposed schemas on the APSCA stories.
|
||||
- His dev reviews are detailed and often include draft code, schema, and state machines. Treat them as the starting point for a story, not as a finished spec.
|
||||
- Hunter Southworth - Engineering Manager / Senior Developer
|
||||
- Kelson - Owner of Ksense
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# MCP servers registered with the local agent runtimes.
|
||||
#
|
||||
# <name> <transport> <url> <token variable>
|
||||
#
|
||||
# The token variable names an entry in user/agents/mcp/env, which is ignored by
|
||||
# git. This file carries no secret, so it is tracked like the rest of user/ and
|
||||
# a new machine gets the same servers by dropping its own env file beside it.
|
||||
#
|
||||
# A row with no token variable registers without an Authorization header.
|
||||
#
|
||||
# Lines beginning with # are comments and blank lines are ignored.
|
||||
|
||||
# The NanoKVM boards. These give an agent full console control of each machine,
|
||||
# including the parts SSH cannot reach: firmware, a locked screen, and the GUI
|
||||
# prompts Xcode and the keychain raise during a signed iOS build.
|
||||
nanokvm-fedora http https://nano.gbrown.org/api/mcp NANOKVM_FEDORA_TOKEN
|
||||
nanokvm-mac http https://macnano.gbrown.org/api/mcp NANOKVM_MAC_TOKEN
|
||||
|
||||
# Bill Tracker's hosted MCP server. No token: it is an OAuth server of its own
|
||||
# and Claude Code signs in through Gib's Auth on first use.
|
||||
bill-tracker http https://mcp.billtracker.gbrown.org/mcp
|
||||
@@ -108,6 +108,63 @@ rerun it, and verify the device log instead of accepting the fallback transcript
|
||||
- Don't check a checklist box (in `mr.md` or anywhere else) unless you actually verified
|
||||
it. Leave it unchecked and say why in the notes rather than guessing.
|
||||
|
||||
## The prose bar (anything another person reads)
|
||||
|
||||
Everything this skill produces for someone else gets a pattern pass before it ships:
|
||||
`mr.md`'s Summary and Additional Notes, every Jira field including the proof and
|
||||
mitigation cells, and every deliverable document. Call the Skill tool with "unslop" and
|
||||
apply its pattern detection.
|
||||
|
||||
- **Skip unslop's "Adding soul" section.** It asks for opinions, varied rhythm, and some
|
||||
mess. That is right for an essay and wrong for a mitigation cell Henry is auditing.
|
||||
These want plain, factual, and specific.
|
||||
- **Never edit generated output.** The pre-MR handoff pasted into `mr.md` is the audit's
|
||||
own words. If it reads badly, regenerate it, don't rewrite it.
|
||||
- **Proof cells are prose too.** A cell that wraps three sentences around a filename is
|
||||
writing, not a citation, and it picks up passive voice and jargon like any other
|
||||
paragraph. Real examples caught this way: "affordance" for button, "vacuously" for
|
||||
"for the wrong reason", "the test was confirmed able to fail" for "I emptied the
|
||||
legacy render and confirmed the test fails".
|
||||
- **Passive voice is the usual offender here,** not puffery. Technical writing drifts
|
||||
into "X was verified" when "I verified X" is shorter and says who did it.
|
||||
|
||||
### Write as Gabriel, and never flag his own decisions
|
||||
|
||||
Everything that ships under his name is written **as him**, in first person. Never write
|
||||
his name or any stand-in for it. Not "Gib decided", not "the author", not "per the
|
||||
developer". When a sentence needs an actor, it is "I".
|
||||
|
||||
**Never frame one of his decisions as a deviation.** Do not write that something "was not
|
||||
in this story's scope", that an AC or dev review said otherwise, that an earlier story
|
||||
"should have caught it", or that a direction was "reversed". State what the change is and
|
||||
why it belongs where it is, then stop.
|
||||
|
||||
He writes the tickets himself, so he is entitled to change them, and a dev review is a
|
||||
proposal rather than a contract. Calling the change out reads as his own AI tattling on
|
||||
him. It waves a red flag at a nothing burger, costs the PM time investigating a
|
||||
non-issue, and leaves them assuming he did something wrong. The real story is normally
|
||||
mundane and good: the better component already existed and cost almost nothing to use.
|
||||
|
||||
Compare, on KACP-23058:
|
||||
|
||||
> Bad: "This was not in this story's scope. KACP-23059 shipped that page still on the
|
||||
> legacy tables, and Gib reversed the reuse-legacy direction on 2026-09-02."
|
||||
>
|
||||
> Good: "Both detail pages now share one set of grid components instead of two divergent
|
||||
> implementations, so the move ships with the components it shares."
|
||||
|
||||
Same information, no flag attached. **If a decision feels big enough that you want to
|
||||
narrate it, that is the signal to ask him about it before writing, not to annotate it in
|
||||
the document.** Being aligned first is the point, and the writing should read as though
|
||||
you always were.
|
||||
|
||||
This governs `mr.md`, `customfield_10260` and `10261`, every other Jira field, and PM
|
||||
deliverables. It does not govern files only he reads. `plan.md`, `bugs.md`, audits and
|
||||
working notes can name him and record who decided what, because that history is useful.
|
||||
|
||||
The House style punctuation rule above is the stricter one where the two overlap. Keep
|
||||
it. Personal working files (`plan.md`, `ticket.md`) are exempt, nobody else reads them.
|
||||
|
||||
## Verification tools available
|
||||
|
||||
The goal isn't "a plausible-sounding plan" or "code that compiles" — it's a plan and
|
||||
@@ -226,15 +283,19 @@ method for producing accurate screenshots, not part of the deliverable's story.
|
||||
Process, per deliverable:
|
||||
|
||||
1. Write `<slug>.md`.
|
||||
2. Write `<slug>.typ` from it, per the styling notes above.
|
||||
3. Compile: `typst compile <slug>.typ <slug>.pdf`. `typst` should already be on PATH;
|
||||
2. Run the prose bar over `<slug>.md` before building anything from it. A spike
|
||||
deliverable is the most prose-heavy thing this skill produces and it goes straight to
|
||||
Conrad, who is skeptical of AI. Fixing it here means fixing it once, rather than in
|
||||
the markdown and again in the Typst.
|
||||
3. Write `<slug>.typ` from it, per the styling notes above.
|
||||
4. Compile: `typst compile <slug>.typ <slug>.pdf`. `typst` should already be on PATH;
|
||||
if it isn't, tell the user rather than silently skipping the PDF.
|
||||
4. Actually look at the compiled PDF using the Read tool (it reads PDFs directly, page
|
||||
5. Actually look at the compiled PDF using the Read tool (it reads PDFs directly, page
|
||||
by page for longer documents). Check every page for real layout problems: text or
|
||||
a table overflowing a page, an awkward page break splitting a table or diagram,
|
||||
cramped or excessive spacing, a diagram that rendered wrong. A successful compile
|
||||
only means valid Typst, not that it looks right — actually look.
|
||||
5. If anything looks wrong, fix `<slug>.typ` and go back to step 3. Repeat until the
|
||||
6. If anything looks wrong, fix `<slug>.typ` and go back to step 4. Repeat until the
|
||||
PDF genuinely looks right, not just until it compiles without erroring.
|
||||
|
||||
If `.claude/docs/` is tracked by git in this repo (check with
|
||||
@@ -690,6 +751,35 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
||||
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.
|
||||
|
||||
**The verdict is the gate and it is not negotiable.** Ready to Open MR is what
|
||||
goes on record with the Lead, so never reason your way past it because the
|
||||
remaining items look harmless. That judgment is not yours to make.
|
||||
|
||||
**Don't confuse the verdict with the handoff's recommendation.** They are
|
||||
different fields in different files for different readers. The audit's
|
||||
`Verdict` says whether the branch is ready to become an MR, and that is the
|
||||
gate. The handoff's `Recommendation` (`Merge`, `Merge after conditions`) is
|
||||
advice to the maintainer about merging it, and `Merge after conditions` is
|
||||
normal and expected at a Ready verdict. Never treat the recommendation as a
|
||||
blocker or report it as one.
|
||||
|
||||
**Reruns converge by giving the audit more, not by repeating the command.**
|
||||
When a rerun is needed, the user is running it, so every rerun costs them.
|
||||
Make each one count:
|
||||
- **Feed it context on the first run.** It reviews the diff, and left alone it
|
||||
re-derives intent from the branch name and re-raises decisions that are
|
||||
already settled. Ask the user to point it at the ticket's
|
||||
`resources/ticket.md` and `plan.md`. This is the cheapest thing that reduces
|
||||
repeat findings.
|
||||
- **Answer its decisions explicitly in the rerun.** When it parks a finding
|
||||
under "Decide before opening", write the decision and its reasoning into the
|
||||
prompt for the next run, so it can reclassify rather than re-ask. A silent
|
||||
rerun on an unchanged diff re-asks the same question.
|
||||
- **Say so when it finds new things in unchanged code.** That means the earlier
|
||||
run was an incomplete pass, not that the branch got worse. Tell the user
|
||||
plainly, because it is the audit's problem to fix and it changes how much
|
||||
weight the next run deserves.
|
||||
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
|
||||
`.claude/docs/mr/template.md` no longer exists in the command-center repo. `mr.md`
|
||||
@@ -714,7 +804,9 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
||||
verified" if you actually ran a build and it passed).
|
||||
- Replace `<!-- Paste the Pre-MR handoff here -->` with the exact content of the
|
||||
latest `pre-mr-review` handoff file (the one with verdict Ready to Open MR) —
|
||||
paste it verbatim, don't summarize it.
|
||||
paste it verbatim, don't summarize it. Its `Recommendation` line is the
|
||||
audit's advice to the maintainer and is left exactly as written, including
|
||||
`Merge after conditions`.
|
||||
|
||||
The handoff section of `mr.md` is GENERATED OUTPUT, not something you author.
|
||||
It is the block that states whether the branch is ready to merge, so it must be
|
||||
@@ -756,13 +848,10 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
||||
content. A reviewer should be able to read the whole file in about a minute; if it
|
||||
has grown past roughly 120 lines including the handoff, it has drifted.
|
||||
|
||||
Before saving, call the Skill tool with "unslop" and apply its pattern detection to
|
||||
the Summary and Additional Notes ONLY: puffery, superficial -ing phrases, "not just
|
||||
X but Y", vague attributions, rule-of-three padding. The pasted handoff is generated
|
||||
output and is never edited, per the rule above. Skip unslop's "Adding soul" section
|
||||
too — an MR description wants plain and factual, not voice. Henry reads every one of
|
||||
these, and prose that reads as machine-generated costs the MR its credibility before
|
||||
anyone looks at the diff.
|
||||
Before saving, run the prose bar (see **The prose bar** above) over the Summary and
|
||||
Additional Notes ONLY. The pasted handoff is generated output and is never edited.
|
||||
Henry reads every one of these, and prose that reads as machine-generated costs the
|
||||
MR its credibility before anyone looks at the diff.
|
||||
9. Fill the Jira ticket fields directly, by issue type. Rich text fields are ADF:
|
||||
render markdown with `python3 ~/.agents/skills/review-ticket/scripts/review2adf.py
|
||||
render <file.md>` and PUT via `{"fields": {...}}` to `/rest/api/3/issue/<KEY>`.
|
||||
@@ -771,11 +860,9 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
||||
field-to-type map below is verified against the project's edit screens, don't PUT
|
||||
a field to a type that doesn't carry it.
|
||||
|
||||
**Same prose bar as `mr.md`.** Every field authored here is read by the Lead and the
|
||||
PM. Run the `unslop` skill's pattern detection over the markdown before rendering it
|
||||
to ADF, skipping its "Adding soul" section — Jira fields want plain, factual, and
|
||||
specific. This does not apply to proof cells, which are references to artifacts and
|
||||
test names rather than prose.
|
||||
**Run the prose bar** (see **The prose bar** above) over every field before rendering
|
||||
it to ADF, proof and mitigation cells included. Everything here is read by the Lead
|
||||
and the PM.
|
||||
|
||||
**Proof first.** Before filling any proof column, capture working feature proof
|
||||
yourself wherever possible: run the app (`run` skill) and screenshot the real
|
||||
@@ -835,13 +922,12 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
||||
handled and what covers it. Deliver the number asked for, not more, and pick the
|
||||
ones a reviewer most benefits from. This field, not `mr.md`.
|
||||
- `customfield_10261` User Story Patch Notes: the same change in user-facing words.
|
||||
- `customfield_10142` Working Feature Proof: ALWAYS fill this on a Story, never
|
||||
leave it empty. It renders as its own panel in the ticket's testing section, so
|
||||
an empty field reads as unfilled testing even when every test-table cell is
|
||||
complete (this happened on KACP-23143). At minimum it lists the files staged in
|
||||
`proof/` for Gib to attach, one line each saying what the artifact shows, plus a
|
||||
pointer that per-row proof lives in the Test Cases table. A headline artifact (a
|
||||
demo capture, a before/after pair) leads the list when one exists.
|
||||
- `customfield_10142` Working Feature Proof: leave it alone. Write nothing here.
|
||||
This field is where Gib drops the proof attachments, and dragging files into it
|
||||
replaces whatever text is sitting there, so a written list is destroyed by the
|
||||
upload it was describing. Naming each artifact is already the job of the Test
|
||||
Cases proof cells, which reference it by filename. Step 10 tells Gib which files
|
||||
to upload. Do not PUT this field on a Story.
|
||||
|
||||
**Spike** (deliverables, not code): no test or proof fields to fill. The output is
|
||||
the deliverables directory, and step 10 tells Gib which files to upload where (the
|
||||
|
||||
Reference in New Issue
Block a user