Compare commits
11
Commits
99156442b5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ebebc8aa2 | ||
|
|
85160ffef2 | ||
|
|
59192143c5 | ||
|
|
2f2e3b88e8 | ||
|
|
9156a7e05c | ||
|
|
39a2e8e9ca | ||
|
|
6206565d95 | ||
|
|
cceb7a707b | ||
|
|
c1bbc69c8a | ||
|
|
fa8b14e05e | ||
|
|
b5832fc94a |
@@ -12,6 +12,10 @@
|
|||||||
/config/dot/espanso/match/identity.yml
|
/config/dot/espanso/match/identity.yml
|
||||||
# Ignore backups of old config files
|
# Ignore backups of old config files
|
||||||
/config/old
|
/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!
|
# Ignore Wireguard config of course!
|
||||||
/config/wg/**
|
/config/wg/**
|
||||||
# Ignore Neovim lazy-lock file
|
# Ignore Neovim lazy-lock file
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ docs/ Settings reference, and the design specs behind the work
|
|||||||
|
|
||||||
## Tests
|
## 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
|
contract by the capabilities it needs. Run the hermetic set, or grant a
|
||||||
specific external capability when automation needs it:
|
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'
|
alias fprint-off='sudo authselect disable-feature with-fingerprint'
|
||||||
|
|
||||||
# File system
|
# File system
|
||||||
alias ls='eza -lh --group-directories-first --icons'
|
alias ls='eza -lh --group-directories-first --icons=auto'
|
||||||
alias lsa='ls -a'
|
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'
|
alias lta='lt -a'
|
||||||
# Fedora's bat installs /usr/bin/bat; batcat is the Debian name.
|
# Fedora's bat installs /usr/bin/bat; batcat is the Debian name.
|
||||||
alias ff="fzf --preview 'bat --style=numbers --color=always {}'"
|
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_scale = 1.5
|
||||||
local shipped_transform = 0
|
local shipped_transform = 0
|
||||||
|
|
||||||
-- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4
|
-- 8-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
|
-- HBR3 and reliant on DSC, and this panel's link is marginal: every modeset
|
||||||
-- a lower mode, drop this to 8 first.
|
-- retrains it and blanks the screen. 8bpc keeps headroom on the link.
|
||||||
local shipped_bitdepth = 10
|
--
|
||||||
|
-- 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.
|
-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
|
||||||
local shipped_cm = "auto"
|
local shipped_cm = "auto"
|
||||||
|
|||||||
@@ -125,6 +125,16 @@ hl.window_rule({
|
|||||||
no_dim = true,
|
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.
|
-- Steam itself is a normal window, but its transient popups are a mess.
|
||||||
hl.window_rule({
|
hl.window_rule({
|
||||||
match = { class = "^steam$", title = "^(Friends List|Steam Settings|Special Offer.*)$" },
|
match = { class = "^steam$", title = "^(Friends List|Steam Settings|Special Offer.*)$" },
|
||||||
@@ -269,16 +279,28 @@ hl.layer_rule({
|
|||||||
ignore_alpha = 0.2,
|
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({
|
hl.layer_rule({
|
||||||
name = "qs-overlay",
|
name = "qs-overlay",
|
||||||
match = { namespace = "^qs-(overview|capture|screen-intelligence)$" },
|
match = { namespace = "^qs-(overview|screen-intelligence)$" },
|
||||||
blur = true,
|
blur = true,
|
||||||
ignore_alpha = 0.4,
|
ignore_alpha = 0.4,
|
||||||
dim_around = true,
|
dim_around = true,
|
||||||
no_screen_share = 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
|
-- Notification toasts. Blurred like every other shell surface -- without this
|
||||||
-- the cards are a near-transparent fill sitting directly on the wallpaper and
|
-- the cards are a near-transparent fill sitting directly on the wallpaper and
|
||||||
-- read as washed out rather than as glass.
|
-- read as washed out rather than as glass.
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ PanelWindow {
|
|||||||
right: true
|
right: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fullscreen overlays must not reserve space, or every window on the
|
// Ignore, and nothing else: reserve no space (or every window on the
|
||||||
// workspace gets resized as the picker opens and closes.
|
// workspace resizes as the picker opens and closes) and respect nobody's
|
||||||
exclusiveZone: 0
|
// (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.namespace: "qs-capture" // matched by a layerrule in hypr/rules.lua
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ PanelWindow {
|
|||||||
|
|
||||||
anchors { top: true; bottom: true; left: true; right: true }
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
|
// The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ PanelWindow {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
// A dock that reserved space would not be intellihiding.
|
// A dock that reserved space would not be intellihiding.
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename.
|
// 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 }
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// Blurred by the `^qs-popover` rule in hypr/rules.lua; the scrim is painted
|
// 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
|
// 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
|
// output including under the bar and the dock, which is what makes it
|
||||||
// visible from wherever the eyes happen to be.
|
// visible from wherever the eyes happen to be.
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ PanelWindow {
|
|||||||
screen: root.modelData
|
screen: root.modelData
|
||||||
anchors.bottom: true
|
anchors.bottom: true
|
||||||
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
implicitWidth: root.desiredWidth
|
implicitWidth: root.desiredWidth
|
||||||
implicitHeight: 64
|
implicitHeight: 64
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// Matched by the `qs-overlay` layer rule in hypr/rules.lua — do not rename.
|
// Matched by the `qs-overlay` layer rule in hypr/rules.lua — do not rename.
|
||||||
|
|||||||
@@ -34,11 +34,23 @@ Item {
|
|||||||
visible: root.rowCount > 0
|
visible: root.rowCount > 0
|
||||||
implicitHeight: rows.implicitHeight
|
implicitHeight: rows.implicitHeight
|
||||||
|
|
||||||
// Probing I2C takes on the order of a second, so it waits until the panel
|
// Probing I2C takes on the order of a second, so it waits until the quick
|
||||||
// is actually on screen rather than running at shell startup. Monitors do
|
// settings panel is actually on screen rather than running at shell
|
||||||
// not come and go, so once is enough.
|
// 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()
|
// 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
|
// `-m` is the machine-readable form: name,class,current,percent,max
|
||||||
Process {
|
Process {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ Variants {
|
|||||||
implicitWidth: 260
|
implicitWidth: 260
|
||||||
implicitHeight: 172
|
implicitHeight: 172
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
mask: Region {}
|
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
|
0 on syntax and runtime errors too. The only trustworthy signal that a write
|
||||||
landed is reading the value back.
|
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,
|
**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
|
and it ranges from a half-screen split to the full display. `SliderRow` stacks
|
||||||
its control under the label below 520px. Test narrow.
|
its control under the label below 520px. Test narrow.
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ PanelWindow {
|
|||||||
|
|
||||||
anchors { top: true; bottom: true; left: true; right: true }
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
WlrLayershell.namespace: "qs-popover-welcome"
|
WlrLayershell.namespace: "qs-popover-welcome"
|
||||||
|
|||||||
@@ -341,7 +341,10 @@ esac'
|
|||||||
property bool record: false
|
property bool record: false
|
||||||
property bool intelligence: false
|
property bool intelligence: false
|
||||||
// A handful of frames at 60Hz, enough for the compositor to recomposite
|
// 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
|
interval: 90
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
const r = commitDelay.rect;
|
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
|
# 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
|
# reviewed installer trust inputs changed, so this hashes them and remembers
|
||||||
# the result. The framed, sorted stream includes top-level package files, the
|
# 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.
|
# 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:
|
||||||
@@ -93,7 +93,10 @@ hash_packages() {
|
|||||||
|
|
||||||
for fixed_input in \
|
for fixed_input in \
|
||||||
"$PANAMA_PATH/setup/scripts/install-packages" \
|
"$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
|
[[ -f "$fixed_input" && ! -L "$fixed_input" && -r "$fixed_input" ]] || return 1
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -102,7 +105,10 @@ hash_packages() {
|
|||||||
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -print0 || exit 1
|
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -print0 || exit 1
|
||||||
printf '%s\0' \
|
printf '%s\0' \
|
||||||
"$PANAMA_PATH/setup/scripts/install-packages" \
|
"$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
|
find "$PANAMA_PATH/setup/provenance" -type f -print0 || exit 1
|
||||||
} | LC_ALL=C sort -z | while IFS= read -r -d '' file; do
|
} | LC_ALL=C sort -z | while IFS= read -r -d '' file; do
|
||||||
relative="${file#"$PANAMA_PATH"/}"
|
relative="${file#"$PANAMA_PATH"/}"
|
||||||
@@ -116,12 +122,11 @@ hash_packages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
packages_needed() {
|
packages_needed() {
|
||||||
local current_hash recorded_hash
|
local current_hash="$1" 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
|
||||||
current_hash="$(hash_packages)" || return 2
|
|
||||||
recorded_hash="$(cat "$PACKAGES_HASH")" || return 2
|
recorded_hash="$(cat "$PACKAGES_HASH")" || return 2
|
||||||
[[ "$current_hash" != "$recorded_hash" ]]
|
[[ "$current_hash" != "$recorded_hash" ]]
|
||||||
}
|
}
|
||||||
@@ -130,10 +135,12 @@ packages_needed() {
|
|||||||
# 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
|
local starting_hash="$1" current_hash temporary_hash
|
||||||
|
current_hash="$(hash_packages)" || return 1
|
||||||
|
[[ "$current_hash" == "$starting_hash" ]] || return 1
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
temporary_hash="$(mktemp "$STATE_DIR/.packages-hash.XXXXXX")" || return 1
|
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"
|
mv -f -- "$temporary_hash" "$PACKAGES_HASH"
|
||||||
else
|
else
|
||||||
rm -f -- "$temporary_hash"
|
rm -f -- "$temporary_hash"
|
||||||
@@ -142,20 +149,37 @@ record_packages_hash() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Repository trust is checked before the installer can reach its bootstrap DNF.
|
# 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
|
# Status 78 is reserved for a trust-root failure. It suppresses every stage that
|
||||||
# no later stage, especially install-hardware, can invoke DNF with that repo.
|
# 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
|
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"
|
trust_preflight="$PANAMA_PATH/setup/scripts/install-packages"
|
||||||
if [[ ! -x "$trust_preflight" ]]; then
|
if [[ ! -x "$trust_preflight" ]]; then
|
||||||
printf 'install: package repository trust preflight is unavailable\n' >&2
|
printf 'install: package repository trust preflight is unavailable\n' >&2
|
||||||
exit "$TERRA_TRUST_FAILURE_STATUS"
|
package_trust_refused=1
|
||||||
fi
|
elif ! "$trust_preflight" --trust-preflight; then
|
||||||
if "$trust_preflight" --trust-preflight; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
trust_status=$?
|
|
||||||
printf 'install: package repository trust preflight failed\n' >&2
|
printf 'install: package repository trust preflight failed\n' >&2
|
||||||
exit "$trust_status"
|
package_trust_refused=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── The interview ────────────────────────────────────────────────────────────
|
# ── The interview ────────────────────────────────────────────────────────────
|
||||||
@@ -175,7 +199,7 @@ fi
|
|||||||
# Gated exactly like the interview itself: under --upgrade no questions are
|
# 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
|
# asked, so nothing here is used, and a machine that cannot install gum must
|
||||||
# not have that stop an upgrade that never needed it.
|
# not have that stop an upgrade that never needed it.
|
||||||
if (( ! UPGRADE )); then
|
if (( ! UPGRADE && ! package_trust_refused )); then
|
||||||
bootstrap=()
|
bootstrap=()
|
||||||
command -v gum >/dev/null 2>&1 || bootstrap+=(gum)
|
command -v gum >/dev/null 2>&1 || bootstrap+=(gum)
|
||||||
# The probe tools serve only the hardware questions, which a server is never
|
# The probe tools serve only the hardware questions, which a server is never
|
||||||
@@ -319,7 +343,7 @@ fi
|
|||||||
if [[ "$PANAMA_ROLE" == server ]]; then
|
if [[ "$PANAMA_ROLE" == server ]]; then
|
||||||
STAGES=(install-packages link-dotfiles link-user setup-server link-server setup-identity)
|
STAGES=(install-packages link-dotfiles link-user setup-server link-server setup-identity)
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
# The two an upgrade drops. Both exist only to act on interview answers, and
|
# 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"
|
script="$PANAMA_PATH/setup/scripts/$stage"
|
||||||
[[ -x "$script" ]] || continue
|
[[ -x "$script" ]] || continue
|
||||||
printf '\n=== %s ===\n' "$stage"
|
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
|
if [[ "$stage" == install-packages ]]; then
|
||||||
package_state_status=0
|
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
|
if (( package_state_status == 1 )); then
|
||||||
echo "The package lists have not changed since the last run; skipping."
|
echo "The package lists have not changed since the last run; skipping."
|
||||||
echo "Run with --packages to install them anyway."
|
echo "Run with --packages to install them anyway."
|
||||||
@@ -357,16 +388,19 @@ for stage in "${STAGES[@]}"; do
|
|||||||
fi
|
fi
|
||||||
if "$script"; then
|
if "$script"; then
|
||||||
if [[ "$stage" == install-packages ]]; then
|
if [[ "$stage" == install-packages ]]; then
|
||||||
if ! record_packages_hash; then
|
if ! record_packages_hash "$package_start_hash"; then
|
||||||
failed+=("$stage")
|
failed+=("$stage")
|
||||||
printf '!!! %s could not record its tracked installation inputs\n' "$stage" >&2
|
printf '!!! %s could not record its tracked installation inputs\n' "$stage" >&2
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
stage_status=$?
|
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
|
if [[ "$stage" == install-packages && "$stage_status" -eq "$TERRA_TRUST_FAILURE_STATUS" ]]; then
|
||||||
printf '!!! %s stopped on an untrusted package repository\n' "$stage" >&2
|
printf '!!! %s stopped on an untrusted package repository\n' "$stage" >&2
|
||||||
exit "$stage_status"
|
package_trust_refused=1
|
||||||
|
continue
|
||||||
fi
|
fi
|
||||||
failed+=("$stage")
|
failed+=("$stage")
|
||||||
printf '!!! %s failed\n' "$stage" >&2
|
printf '!!! %s failed\n' "$stage" >&2
|
||||||
@@ -392,8 +426,16 @@ done
|
|||||||
# written for -- and baselining would skip every one of them forever. Every
|
# 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
|
# migration is self-guarding and a no-op where it does not apply, so running
|
||||||
# them is the safe direction.
|
# 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"
|
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'
|
printf '\n=== migrations ===\n'
|
||||||
if (( UPGRADE )) || [[ -d "$STATE_DIR/migrations" ]]; then
|
if (( UPGRADE )) || [[ -d "$STATE_DIR/migrations" ]]; then
|
||||||
"$migrate" run || failed+=(migrations)
|
"$migrate" run || failed+=(migrations)
|
||||||
@@ -441,6 +483,17 @@ else
|
|||||||
retry='./install'
|
retry='./install'
|
||||||
fi
|
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 (( ${#failed[@]} == 0 )); then
|
||||||
if (( UPGRADE )); then
|
if (( UPGRADE )); then
|
||||||
echo "Panama is up to date."
|
echo "Panama is up to date."
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ declare -gA INSTALLER_PROVENANCE=()
|
|||||||
|
|
||||||
_primary_key_fingerprints() (
|
_primary_key_fingerprints() (
|
||||||
local home
|
local home
|
||||||
|
set -o pipefail
|
||||||
home="$(mktemp -d)" || exit 1
|
home="$(mktemp -d)" || exit 1
|
||||||
chmod 700 "$home"
|
chmod 700 "$home"
|
||||||
trap 'rm -rf -- "$home"' EXIT
|
trap 'rm -rf -- "$home"' EXIT
|
||||||
@@ -19,14 +20,20 @@ _primary_key_fingerprints() (
|
|||||||
|
|
||||||
key_fingerprint_matches() {
|
key_fingerprint_matches() {
|
||||||
local file="$1" expected="$2"
|
local file="$1" expected="$2"
|
||||||
|
local output
|
||||||
local -a primary_fingerprints=()
|
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" ]]
|
[[ ${#primary_fingerprints[@]} -eq 1 && "${primary_fingerprints[0]}" == "$expected" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
_key_has_one_primary() {
|
_key_has_one_primary() {
|
||||||
|
local output
|
||||||
local -a primary_fingerprints=()
|
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 ]]
|
[[ ${#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 \
|
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 \
|
FEDORA_RELEASE RPMFUSION_FREE_RELEASE_URL RPMFUSION_FREE_RELEASE_MAX_BYTES \
|
||||||
RPMFUSION_NONFREE_RELEASE_URL RPMFUSION_NONFREE_RELEASE_MAX_BYTES TERRA_BASEURL \
|
RPMFUSION_NONFREE_RELEASE_URL RPMFUSION_NONFREE_RELEASE_MAX_BYTES TERRA_BASEURL \
|
||||||
|
TERRA_METALINK_BASEURL \
|
||||||
HYPRLAND_COPR_BASEURL FLATHUB_DESCRIPTOR_URL FLATHUB_DESCRIPTOR_MAX_BYTES \
|
HYPRLAND_COPR_BASEURL FLATHUB_DESCRIPTOR_URL FLATHUB_DESCRIPTOR_MAX_BYTES \
|
||||||
CLAUDE_CODE_BASEURL CLAUDE_DESKTOP_BASEURL TERRA_FINGERPRINT CLAUDE_CODE_FINGERPRINT \
|
CLAUDE_CODE_BASEURL CLAUDE_DESKTOP_BASEURL TERRA_FINGERPRINT CLAUDE_CODE_FINGERPRINT \
|
||||||
BUN_FINGERPRINT RPMFUSION_FREE_FINGERPRINT RPMFUSION_NONFREE_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_URL=https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm
|
||||||
RPMFUSION_NONFREE_RELEASE_MAX_BYTES=4194304
|
RPMFUSION_NONFREE_RELEASE_MAX_BYTES=4194304
|
||||||
TERRA_BASEURL=https://repos.fyralabs.com/terra44
|
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/
|
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_URL=https://flathub.org/repo/flathub.flatpakrepo
|
||||||
FLATHUB_DESCRIPTOR_MAX_BYTES=1048576
|
FLATHUB_DESCRIPTOR_MAX_BYTES=1048576
|
||||||
|
|||||||
+162
-23
@@ -725,8 +725,17 @@ _publish_repository_pair() {
|
|||||||
return "$status"
|
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() {
|
_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() {
|
function reset_block() {
|
||||||
delete values
|
delete values
|
||||||
delete seen
|
delete seen
|
||||||
@@ -746,10 +755,22 @@ _effective_terra_key() {
|
|||||||
for (key in required) {
|
for (key in required) {
|
||||||
if (seen[key] != 1) bad = 1
|
if (seen[key] != 1) bad = 1
|
||||||
}
|
}
|
||||||
if (values["baseurl"] != reviewed_baseurl || values["metalink"] != "" \
|
if (require_pinned != "") {
|
||||||
|| values["mirrorlist"] != "" || values["gpgcheck"] != "1" \
|
if (values["baseurl"] != reviewed_baseurl || values["metalink"] != "" \
|
||||||
|| values["pkg_gpgcheck"] != "1" || values["repo_gpgcheck"] != "1" \
|
|| values["mirrorlist"] != "" \
|
||||||
|| values["gpgkey"] != "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama") bad = 1
|
|| 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"]
|
trusted_key = values["gpgkey"]
|
||||||
}
|
}
|
||||||
BEGIN {
|
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
|
# Status 0 is one trusted effective Terra identity, 1 is no enabled Terra
|
||||||
# identity, and 2 is an unsafe, duplicated, or unreadable effective state.
|
# 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() {
|
_terra_effective_status() {
|
||||||
local dump gpgkey parse_status=0 local_key
|
local dump gpgkey parse_status=0 local_key
|
||||||
dump="$(LC_ALL=C dnf --quiet --no-plugins --dump-repo-config='*')" || return 2
|
if (( $# > 0 )); then
|
||||||
gpgkey="$(printf '%s\n' "$dump" | _effective_terra_key)" || parse_status=$?
|
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"
|
(( parse_status == 0 )) || return "$parse_status"
|
||||||
[[ "$gpgkey" == 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama' ]] || return 2
|
[[ "$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"
|
local_key="$PANAMA_SYSTEM_ETC/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama"
|
||||||
@@ -824,18 +857,71 @@ _terra_effective_status() {
|
|||||||
|| return 2
|
|| 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
|
TERRA_TRUST_FAILURE_STATUS=78
|
||||||
|
|
||||||
preflight_terra_trust() {
|
preflight_terra_trust() {
|
||||||
local status=0
|
local status=0 adoptable_status=0 dump
|
||||||
_require_policy_value TERRA_BASEURL 'https://repos.fyralabs.com/terra44' \
|
_require_policy_value TERRA_BASEURL 'https://repos.fyralabs.com/terra44' \
|
||||||
|| return "$TERRA_TRUST_FAILURE_STATUS"
|
|| return "$TERRA_TRUST_FAILURE_STATUS"
|
||||||
_require_policy_value TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F \
|
_require_policy_value TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F \
|
||||||
|| return "$TERRA_TRUST_FAILURE_STATUS"
|
|| 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
|
if (( status == 0 || status == 1 )); then
|
||||||
return 0
|
return 0
|
||||||
fi
|
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"
|
log "Effective Terra repository configuration is not trusted; refusing all package work"
|
||||||
return "$TERRA_TRUST_FAILURE_STATUS"
|
return "$TERRA_TRUST_FAILURE_STATUS"
|
||||||
}
|
}
|
||||||
@@ -907,18 +993,67 @@ install_rpmfusion_repositories() {
|
|||||||
return "$status"
|
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() {
|
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_reviewed_fedora_release || return 1
|
||||||
_require_policy_value TERRA_BASEURL 'https://repos.fyralabs.com/terra44' || return 1
|
_require_policy_value TERRA_BASEURL 'https://repos.fyralabs.com/terra44' || return 1
|
||||||
_require_policy_value TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F || 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
|
if (( effective_status == 0 )); then
|
||||||
log "Terra repository already configured and verified"
|
log "Terra repository already configured and verified"
|
||||||
return 0
|
return 0
|
||||||
elif (( effective_status != 1 )); then
|
elif (( effective_status != 1 )); then
|
||||||
log "Effective Terra repository configuration is not trusted"
|
_terra_adoptable_status "$dump" || adoptable_status=$?
|
||||||
return "$TERRA_TRUST_FAILURE_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
|
fi
|
||||||
if rpm -q terra-release >/dev/null 2>&1; then
|
if rpm -q terra-release >/dev/null 2>&1; then
|
||||||
log "terra-release is installed without one trusted enabled Terra repository"
|
log "terra-release is installed without one trusted enabled Terra repository"
|
||||||
@@ -1177,16 +1312,9 @@ if [[ "$ROLE" == server ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "\n--- Installing Repositories ---"
|
echo -e "\n--- Installing Repositories ---"
|
||||||
log "Installing RPM Fusion Free and Nonfree Repositories"
|
# Terra goes first so a machine whose Terra is enabled but not yet in Panama's
|
||||||
install_rpmfusion_repositories > /dev/null
|
# reviewed form is adopted before any other transaction below runs against it.
|
||||||
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 bootstraps itself: --repofrompath defines a throwaway repo just long
|
# Terra bootstraps itself: --repofrompath defines a throwaway repo just long
|
||||||
# enough to install terra-release, which then writes the real /etc/yum.repos.d
|
# 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
|
# 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"
|
log "Installing Terra Repository"
|
||||||
install_terra_repository > /dev/null
|
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 ---"
|
echo -e "\n--- Installing relevant packages ---"
|
||||||
log "Updating all packages. This may take a while"
|
log "Updating all packages. This may take a while"
|
||||||
sudo dnf update -y --refresh > /dev/null
|
sudo dnf update -y --refresh > /dev/null
|
||||||
|
|||||||
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,9 +85,15 @@ 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
|
# 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"
|
||||||
if [[ -d "$built" && "$extension/src" -ot "$built" ]]; then
|
if [[ -d "$built" ]]; then
|
||||||
printf 'Vicinae extension %s is already built\n' "$name"
|
newer_source=''
|
||||||
continue
|
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
|
fi
|
||||||
|
|
||||||
printf 'Building Vicinae extension %s\n' "$name"
|
printf 'Building Vicinae extension %s\n' "$name"
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ live-desktop tests/quickshell/dock-position-contract
|
|||||||
# Reads Hyprland option descriptions from the live compositor to verify enum
|
# Reads Hyprland option descriptions from the live compositor to verify enum
|
||||||
# mappings.
|
# mappings.
|
||||||
live-compositor tests/quickshell/enum-hypr-map-contract
|
live-compositor tests/quickshell/enum-hypr-map-contract
|
||||||
|
hermetic tests/quickshell/exclusion-idiom-contract
|
||||||
hermetic tests/quickshell/fingerprint-contract
|
hermetic tests/quickshell/fingerprint-contract
|
||||||
# Reads the host firewall state through the production firewall helper.
|
# Reads the host firewall state through the production firewall helper.
|
||||||
live-host tests/quickshell/firewall-contract
|
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")"
|
filter="$(sed -n '/^packages_in()/,/^}/p' "$installer")"
|
||||||
loop="$(sed -n '/^install_extra_category()/,/^}/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" ]] || {
|
[[ -n "$filter" && -n "$loop" ]] || {
|
||||||
printf 'extras contract: install-packages no longer defines packages_in and install_extra_category\n' >&2
|
printf 'extras contract: install-packages no longer defines packages_in and install_extra_category\n' >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -99,6 +105,7 @@ LIST
|
|||||||
source "$catalog"
|
source "$catalog"
|
||||||
eval "$filter"
|
eval "$filter"
|
||||||
eval "$loop"
|
eval "$loop"
|
||||||
|
eval "$deps"
|
||||||
install_extra_category "$fixture"
|
install_extra_category "$fixture"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,6 +139,7 @@ printf 'flatpak:org.example.OnlyFlatpak\n' >"$flatpak_only"
|
|||||||
source "$catalog"
|
source "$catalog"
|
||||||
eval "$filter"
|
eval "$filter"
|
||||||
eval "$loop"
|
eval "$loop"
|
||||||
|
eval "$deps"
|
||||||
install_extra_category "$flatpak_only"
|
install_extra_category "$flatpak_only"
|
||||||
)
|
)
|
||||||
flatpak_only_status=$?
|
flatpak_only_status=$?
|
||||||
@@ -149,6 +157,7 @@ grep -q 'flatpak install -y flathub org.example.OnlyFlatpak' <<<"$(cat "$calls"
|
|||||||
source "$catalog"
|
source "$catalog"
|
||||||
eval "$filter"
|
eval "$filter"
|
||||||
eval "$loop"
|
eval "$loop"
|
||||||
|
eval "$deps"
|
||||||
EXTRAS_DIR="$extras_dir"
|
EXTRAS_DIR="$extras_dir"
|
||||||
for extra in ${PANAMA_EXTRAS:-}; do
|
for extra in ${PANAMA_EXTRAS:-}; do
|
||||||
[[ -f "$EXTRAS_DIR/$extra" ]] && install_extra_category "$EXTRAS_DIR/$extra"
|
[[ -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'
|
|| note 'the extension build never sources nvm, so npm is missing on any machine without a system node'
|
||||||
|
|
||||||
# node_modules is a dependency tree, not configuration.
|
# node_modules is a dependency tree, not configuration.
|
||||||
git -C "$repo_dir" check-ignore -q "$extension/node_modules" 2>/dev/null \
|
git -C "$repo_dir" check-ignore --no-index -q \
|
||||||
|
"$extension/node_modules/package.json" 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
|
# 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" \
|
cmp -s -- "$lock_before" "$lockfile" \
|
||||||
|| note 'a rejected Vicinae lockfile mismatch changed package-lock.json'
|
|| 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 ───────────────────────────────────────────────────────────────────
|
# ── Report ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (( ${#findings[@]} > 0 )); then
|
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"
|
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"
|
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 \
|
expect_success verify_detached_signature \
|
||||||
"$fixtures/fixture-key.asc" "$fixtures/SHASUMS256.txt.asc" "$fixtures/SHASUMS256.txt"
|
"$fixtures/fixture-key.asc" "$fixtures/SHASUMS256.txt.asc" "$fixtures/SHASUMS256.txt"
|
||||||
expect_failure verify_detached_signature \
|
expect_failure verify_detached_signature \
|
||||||
@@ -869,7 +894,16 @@ if [[ -n "$query" ]]; then
|
|||||||
trusted|wrong-key) mode=trusted ;;
|
trusted|wrong-key) mode=trusted ;;
|
||||||
nogpg) mode=legacy ;;
|
nogpg) mode=legacy ;;
|
||||||
wrong-url) mode=override-url ;;
|
wrong-url) mode=override-url ;;
|
||||||
|
stock|stock-wrong-key) mode=stock ;;
|
||||||
esac
|
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" ]] \
|
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
|
&& grep -q '^baseurl=https://repos.fyralabs.com/terra44$' "$STUB_ETC/yum.repos.d/terra.repo"; then
|
||||||
mode=trusted
|
mode=trusted
|
||||||
@@ -901,6 +935,13 @@ if [[ -n "$query" ]]; then
|
|||||||
printf 'metalink = https://tetsudou.fyralabs.com/terra44\nmirrorlist = \n'
|
printf 'metalink = https://tetsudou.fyralabs.com/terra44\nmirrorlist = \n'
|
||||||
printf 'pkg_gpgcheck = 0\nrepo_gpgcheck = 0\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)
|
override-url)
|
||||||
printf '======== "terra" repository configuration: ========\n'
|
printf '======== "terra" repository configuration: ========\n'
|
||||||
printf 'baseurl = https://evil.invalid/terra44\nenabled = 1\ngpgcheck = 1\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' \
|
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"
|
> "$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)
|
wrong-key)
|
||||||
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
||||||
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama"
|
"$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"
|
|| fail "untrusted existing Terra $mode state reached a mutation"
|
||||||
done
|
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
|
# An optional security field may be absent, but duplicates are malformed even
|
||||||
# when one copy looks safe. These cases catch the absent/duplicate conflation.
|
# when one copy looks safe. These cases catch the absent/duplicate conflation.
|
||||||
for duplicate_case in \
|
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
|
STAGE_NAMES=(install-packages link-dotfiles link-skills link-user change-settings
|
||||||
link-vicinae-scripts setup-server link-server setup-identity
|
link-vicinae-scripts setup-server link-server setup-identity
|
||||||
install-hardware)
|
install-hardware)
|
||||||
|
PACKAGE_BEHAVIOR_INPUTS=(
|
||||||
|
setup/lib/artifact-provenance
|
||||||
|
setup/lib/chatgpt-package
|
||||||
|
setup/lib/extras-catalog
|
||||||
|
setup/lib/machine-role
|
||||||
|
)
|
||||||
|
|
||||||
copy_hash_inputs() {
|
copy_hash_inputs() {
|
||||||
local root="$1" source relative
|
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/packages" -maxdepth 1 -type f -print0
|
||||||
find "$repo_dir/setup/provenance" -type f -print0
|
find "$repo_dir/setup/provenance" -type f -print0
|
||||||
)
|
)
|
||||||
mkdir -p "$root/setup/lib"
|
for relative in "${PACKAGE_BEHAVIOR_INPUTS[@]}"; do
|
||||||
cp -- "$repo_dir/setup/lib/artifact-provenance" "$root/setup/lib/artifact-provenance"
|
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
|
# 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
|
# Dynamically discovering them makes this fail when a new reviewed input is
|
||||||
# added but omitted from hash_packages.
|
# added but omitted from hash_packages.
|
||||||
for relative in "${package_inputs[@]}" "${provenance_inputs[@]}" \
|
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"
|
printf 'changed %s\n' "$relative" >>"$tmp/a/$relative"
|
||||||
install_status=0
|
install_status=0
|
||||||
ran_input_changed="$(run_install "$tmp/a" --upgrade)" || install_status=$?
|
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
|
# 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
|
# link. An unreadable package input also proves a failed content read cannot be
|
||||||
# hidden by the final digest command.
|
# 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
|
for case_name in missing directory symlink unreadable; do
|
||||||
case_root="$tmp/hash-${fixed_input//\//-}-$case_name"
|
case_root="$tmp/hash-${fixed_input//\//-}-$case_name"
|
||||||
build_fixture "$case_root"
|
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" \
|
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'
|
|| 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.
|
# A failing stage must not record the hash, or the failure is hidden forever.
|
||||||
build_fixture "$tmp/c" 1
|
build_fixture "$tmp/c" 1
|
||||||
install_status=0
|
install_status=0
|
||||||
@@ -332,30 +360,48 @@ fi
|
|||||||
grep -qx 'link-dotfiles' "$tmp/c/ran" \
|
grep -qx 'link-dotfiles' "$tmp/c/ran" \
|
||||||
|| note 'an ordinary package-stage failure no longer allows later safe stages'
|
|| 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
|
# An untrusted Terra root is not an ordinary package failure, and it is not a
|
||||||
# stop before the installer's bootstrap DNF and before every stage.
|
# 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
|
build_fixture "$tmp/terra-preflight-hard" 0 78
|
||||||
install_status=0
|
install_status=0
|
||||||
run_install "$tmp/terra-preflight-hard" >/dev/null || install_status=$?
|
run_install "$tmp/terra-preflight-hard" >/dev/null || install_status=$?
|
||||||
[[ "$install_status" -eq 78 ]] \
|
[[ "$install_status" -eq 78 ]] \
|
||||||
|| note "initial Terra trust failure returned $install_status instead of 78"
|
|| note "initial Terra trust failure returned $install_status instead of 78"
|
||||||
asserted_preflight="$(<"$tmp/terra-preflight-hard/ran")"
|
grep -qx 'trust-preflight' "$tmp/terra-preflight-hard/ran" \
|
||||||
[[ "$asserted_preflight" == trust-preflight ]] \
|
|| note 'initial Terra trust fixture never reached the preflight'
|
||||||
|| note "initial Terra trust failure allowed later work: ${asserted_preflight//$'\n'/,}"
|
assert_trust_refusal "$tmp/terra-preflight-hard" 'initial Terra trust failure'
|
||||||
|
|
||||||
# The trust verifier is itself mandatory. Losing its executable adapter must
|
# The trust verifier is itself mandatory. Losing its executable adapter refuses
|
||||||
# fail closed before interview, bootstrap, or stage work.
|
# package work exactly as a failing verdict does, rather than being ignored.
|
||||||
build_fixture "$tmp/terra-preflight-missing"
|
build_fixture "$tmp/terra-preflight-missing"
|
||||||
rm "$tmp/terra-preflight-missing/setup/scripts/install-packages"
|
rm "$tmp/terra-preflight-missing/setup/scripts/install-packages"
|
||||||
install_status=0
|
install_status=0
|
||||||
run_install "$tmp/terra-preflight-missing" >/dev/null || install_status=$?
|
run_install "$tmp/terra-preflight-missing" >/dev/null || install_status=$?
|
||||||
[[ "$install_status" -eq 78 ]] \
|
[[ "$install_status" -eq 78 ]] \
|
||||||
|| note "missing Terra trust verifier returned $install_status instead of 78"
|
|| note "missing Terra trust verifier returned $install_status instead of 78"
|
||||||
[[ ! -s "$tmp/terra-preflight-missing/ran" ]] \
|
assert_trust_refusal "$tmp/terra-preflight-missing" 'missing Terra trust verifier'
|
||||||
|| note 'missing Terra trust verifier allowed later work'
|
|
||||||
|
|
||||||
# The package stage repeats the preflight to close a configuration-change race.
|
# 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
|
build_fixture "$tmp/terra-stage-hard" 78 0
|
||||||
install_status=0
|
install_status=0
|
||||||
run_install "$tmp/terra-stage-hard" >/dev/null || install_status=$?
|
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"
|
|| note "stage-time Terra trust failure returned $install_status instead of 78"
|
||||||
grep -qx 'install-packages' "$tmp/terra-stage-hard/ran" \
|
grep -qx 'install-packages' "$tmp/terra-stage-hard/ran" \
|
||||||
|| note 'stage-time Terra trust fixture never reached install-packages'
|
|| 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" \
|
grep -qx "$suppressed" "$tmp/terra-stage-hard/ran" \
|
||||||
&& note "stage-time Terra trust failure still ran $suppressed"
|
&& note "stage-time Terra trust failure still ran $suppressed"
|
||||||
done
|
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.
|
# A full install always runs the stage, whatever any recorded hash says.
|
||||||
build_fixture "$tmp/d"
|
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
|
# Merge Requests
|
||||||
|
|
||||||
- Make sure titles follow conventions from the repo.
|
- 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 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.
|
- 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!
|
- 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
|
- Hunter Southworth - Engineering Manager / Senior Developer
|
||||||
- Kelson - Owner of Ksense
|
- 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
|
- 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.
|
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
|
## Verification tools available
|
||||||
|
|
||||||
The goal isn't "a plausible-sounding plan" or "code that compiles" — it's a plan and
|
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:
|
Process, per deliverable:
|
||||||
|
|
||||||
1. Write `<slug>.md`.
|
1. Write `<slug>.md`.
|
||||||
2. Write `<slug>.typ` from it, per the styling notes above.
|
2. Run the prose bar over `<slug>.md` before building anything from it. A spike
|
||||||
3. Compile: `typst compile <slug>.typ <slug>.pdf`. `typst` should already be on PATH;
|
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.
|
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
|
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,
|
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
|
cramped or excessive spacing, a diagram that rendered wrong. A successful compile
|
||||||
only means valid Typst, not that it looks right — actually look.
|
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.
|
PDF genuinely looks right, not just until it compiles without erroring.
|
||||||
|
|
||||||
If `.claude/docs/` is tracked by git in this repo (check with
|
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.
|
write `mr.md` before that verdict is reached.
|
||||||
Findings that need no code change (mentions, disclosures) get addressed in
|
Findings that need no code change (mentions, disclosures) get addressed in
|
||||||
`mr.md`'s Additional Notes or the Jira fields as usual.
|
`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,
|
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`
|
||||||
@@ -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).
|
verified" if you actually ran a build and it passed).
|
||||||
- Replace `<!-- Paste the Pre-MR handoff here -->` with the exact content of the
|
- 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) —
|
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.
|
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
|
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
|
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.
|
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
|
Before saving, run the prose bar (see **The prose bar** above) over the Summary and
|
||||||
the Summary and Additional Notes ONLY: puffery, superficial -ing phrases, "not just
|
Additional Notes ONLY. The pasted handoff is generated output and is never edited.
|
||||||
X but Y", vague attributions, rule-of-three padding. The pasted handoff is generated
|
Henry reads every one of these, and prose that reads as machine-generated costs the
|
||||||
output and is never edited, per the rule above. Skip unslop's "Adding soul" section
|
MR its credibility before anyone looks at the diff.
|
||||||
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.
|
|
||||||
9. Fill the Jira ticket fields directly, by issue type. Rich text fields are ADF:
|
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 markdown with `python3 ~/.agents/skills/review-ticket/scripts/review2adf.py
|
||||||
render <file.md>` and PUT via `{"fields": {...}}` to `/rest/api/3/issue/<KEY>`.
|
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
|
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.
|
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
|
**Run the prose bar** (see **The prose bar** above) over every field before rendering
|
||||||
PM. Run the `unslop` skill's pattern detection over the markdown before rendering it
|
it to ADF, proof and mitigation cells included. Everything here is read by the Lead
|
||||||
to ADF, skipping its "Adding soul" section — Jira fields want plain, factual, and
|
and the PM.
|
||||||
specific. This does not apply to proof cells, which are references to artifacts and
|
|
||||||
test names rather than prose.
|
|
||||||
|
|
||||||
**Proof first.** Before filling any proof column, capture working feature proof
|
**Proof first.** Before filling any proof column, capture working feature proof
|
||||||
yourself wherever possible: run the app (`run` skill) and screenshot the real
|
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
|
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`.
|
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_10261` User Story Patch Notes: the same change in user-facing words.
|
||||||
- `customfield_10142` Working Feature Proof: ALWAYS fill this on a Story, never
|
- `customfield_10142` Working Feature Proof: leave it alone. Write nothing here.
|
||||||
leave it empty. It renders as its own panel in the ticket's testing section, so
|
This field is where Gib drops the proof attachments, and dragging files into it
|
||||||
an empty field reads as unfilled testing even when every test-table cell is
|
replaces whatever text is sitting there, so a written list is destroyed by the
|
||||||
complete (this happened on KACP-23143). At minimum it lists the files staged in
|
upload it was describing. Naming each artifact is already the job of the Test
|
||||||
`proof/` for Gib to attach, one line each saying what the artifact shows, plus a
|
Cases proof cells, which reference it by filename. Step 10 tells Gib which files
|
||||||
pointer that per-row proof lives in the Test Cases table. A headline artifact (a
|
to upload. Do not PUT this field on a Story.
|
||||||
demo capture, a before/after pair) leads the list when one exists.
|
|
||||||
|
|
||||||
**Spike** (deliverables, not code): no test or proof fields to fill. The output is
|
**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
|
the deliverables directory, and step 10 tells Gib which files to upload where (the
|
||||||
|
|||||||
Reference in New Issue
Block a user