Compare commits
16
Commits
99156442b5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b06e9e24b | ||
|
|
59da2bb2fc | ||
|
|
5ebecd59bb | ||
|
|
501f7306ed | ||
|
|
130137485e | ||
|
|
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
|
||||||
|
|||||||
+169
-21
@@ -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 (require_pinned != "") {
|
||||||
if (values["baseurl"] != reviewed_baseurl || values["metalink"] != "" \
|
if (values["baseurl"] != reviewed_baseurl || values["metalink"] != "" \
|
||||||
|| values["mirrorlist"] != "" || values["gpgcheck"] != "1" \
|
|| values["mirrorlist"] != "" \
|
||||||
|| values["pkg_gpgcheck"] != "1" || values["repo_gpgcheck"] != "1" \
|
|
||||||
|| 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,19 +993,68 @@ 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
|
||||||
|
_terra_adoptable_status "$dump" || adoptable_status=$?
|
||||||
|
if (( adoptable_status != 0 )); then
|
||||||
log "Effective Terra repository configuration is not trusted"
|
log "Effective Terra repository configuration is not trusted"
|
||||||
return "$TERRA_TRUST_FAILURE_STATUS"
|
return "$TERRA_TRUST_FAILURE_STATUS"
|
||||||
fi
|
fi
|
||||||
|
log "Adopting the existing Terra repository into Panama's reviewed form"
|
||||||
|
adopt_terra_repository
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
if rpm -q terra-release >/dev/null 2>&1; then
|
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"
|
||||||
return "$TERRA_TRUST_FAILURE_STATUS"
|
return "$TERRA_TRUST_FAILURE_STATUS"
|
||||||
@@ -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
|
||||||
@@ -1266,8 +1405,17 @@ soft "the multimedia group update" \
|
|||||||
sync
|
sync
|
||||||
log "Swapping ffmpeg-free for ffmpeg"
|
log "Swapping ffmpeg-free for ffmpeg"
|
||||||
soft "the ffmpeg swap" sudo dnf swap -y 'ffmpeg-free' 'ffmpeg' --allowerasing
|
soft "the ffmpeg swap" sudo dnf swap -y 'ffmpeg-free' 'ffmpeg' --allowerasing
|
||||||
log "Swapping mesa-va-drivers for mesa-va-drivers-freeworld"
|
# mesa-va-drivers-freeworld Provides mesa-va-drivers, so once the swap has been
|
||||||
soft "the mesa driver swap" sudo dnf swap -y mesa-va-drivers mesa-va-drivers-freeworld
|
# done it is the only thing providing the name we ask dnf to remove. `dnf swap`
|
||||||
|
# then resolves to removing and installing the same package in one transaction
|
||||||
|
# and fails with "conflicting requests", which reports install-packages as a
|
||||||
|
# failed stage on every subsequent run. Guard on the end state instead.
|
||||||
|
if rpm -q mesa-va-drivers-freeworld >/dev/null 2>&1; then
|
||||||
|
log "mesa-va-drivers-freeworld is already installed; swap not needed"
|
||||||
|
else
|
||||||
|
log "Swapping mesa-va-drivers for mesa-va-drivers-freeworld"
|
||||||
|
soft "the mesa driver swap" sudo dnf swap -y mesa-va-drivers mesa-va-drivers-freeworld
|
||||||
|
fi
|
||||||
log "Upgrading Multimedia group with optional packages"
|
log "Upgrading Multimedia group with optional packages"
|
||||||
soft "the optional Multimedia upgrade" sudo dnf4 group upgrade -y --with-optional Multimedia
|
soft "the optional Multimedia upgrade" sudo dnf4 group upgrade -y --with-optional Multimedia
|
||||||
log "Installing GStreamer plugins (bad, good, base)"
|
log "Installing GStreamer plugins (bad, good, base)"
|
||||||
|
|||||||
Executable
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# MCP servers, registered with the agent runtimes on this machine.
|
||||||
|
#
|
||||||
|
# An MCP server is a URL plus a bearer token, and the token is the whole reason
|
||||||
|
# this is a stage rather than a manifest line. Panama is a public repository, so
|
||||||
|
# the tokens cannot live in it, and neither runtime keeps its server list in a
|
||||||
|
# file that could be symlinked anyway: Codex writes them into config.toml beside
|
||||||
|
# dozens of unrelated settings, and Claude Code into ~/.claude.json. There is no
|
||||||
|
# file to point at, so this registers them through the runtime's own CLI.
|
||||||
|
#
|
||||||
|
# What is tracked is user/agents/mcp/servers, which names each server and which
|
||||||
|
# variable carries its token. What is not tracked is user/agents/mcp/env, which
|
||||||
|
# holds the tokens. A new machine gets the servers by dropping its own env file
|
||||||
|
# beside the tracked one and re-running ./install.
|
||||||
|
#
|
||||||
|
# This is personal content, so it obeys the same interview decision link-user
|
||||||
|
# does. A machine that never said yes registers nothing.
|
||||||
|
#
|
||||||
|
# Only Claude Code is handled. Codex stores its servers inside config.toml, and
|
||||||
|
# rewriting a section of somebody's live TOML is a worse failure mode than
|
||||||
|
# leaving two lines for them to paste once.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||||
|
warn() { echo -e "\033[1;33m[WARN]\033[0m $*" >&2; }
|
||||||
|
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
|
MCP_DIR="$PANAMA_PATH/user/agents/mcp"
|
||||||
|
SERVERS="$MCP_DIR/servers"
|
||||||
|
ENV_FILE="$MCP_DIR/env"
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
|
||||||
|
DECISION="$STATE_DIR/user-content"
|
||||||
|
|
||||||
|
[[ -r "$SERVERS" ]] || { log "No MCP server list; nothing to register."; exit 0; }
|
||||||
|
|
||||||
|
# The same gate link-user uses, read the same way, so one answer governs all
|
||||||
|
# personal content rather than two stages disagreeing about it.
|
||||||
|
decision="$([[ -r "$DECISION" ]] && cat "$DECISION" || printf 'no')"
|
||||||
|
if [[ "$decision" != "yes" ]]; then
|
||||||
|
log "Personal content is not enabled on this machine; no MCP servers registered."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v claude >/dev/null 2>&1; then
|
||||||
|
log "Claude Code is not installed; nothing to register."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tokens are optional. A machine without the env file still registers any server
|
||||||
|
# that needs no header, and says which ones it skipped rather than failing.
|
||||||
|
if [[ -r "$ENV_FILE" ]]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
else
|
||||||
|
warn "No $ENV_FILE; servers needing a token will be skipped."
|
||||||
|
fi
|
||||||
|
|
||||||
|
registered=0
|
||||||
|
skipped=0
|
||||||
|
|
||||||
|
while read -r name transport url token_var _rest; do
|
||||||
|
case "${name:-}" in ''|'#'*) continue ;; esac
|
||||||
|
if [[ -z "${transport:-}" || -z "${url:-}" ]]; then
|
||||||
|
warn "Ignoring malformed row for '$name'."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
header=()
|
||||||
|
if [[ -n "${token_var:-}" ]]; then
|
||||||
|
token="${!token_var:-}"
|
||||||
|
if [[ -z "$token" ]]; then
|
||||||
|
warn "Skipping $name: $token_var is not set in $ENV_FILE."
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
header=(-H "Authorization: $token")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Re-registering is how this stays idempotent across upgrades, and how a
|
||||||
|
# rotated token reaches the runtime. Removing first avoids the CLI refusing
|
||||||
|
# a name it already knows. Neither call may print the token, so both are
|
||||||
|
# quiet unless they fail.
|
||||||
|
claude mcp remove "$name" -s user >/dev/null 2>&1 || true
|
||||||
|
if claude mcp add --transport "$transport" "$name" "$url" "${header[@]}" \
|
||||||
|
-s user >/dev/null 2>&1; then
|
||||||
|
log "Registered $name."
|
||||||
|
registered=$((registered + 1))
|
||||||
|
else
|
||||||
|
warn "Could not register $name."
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
fi
|
||||||
|
done <"$SERVERS"
|
||||||
|
|
||||||
|
log "MCP servers: $registered registered, $skipped skipped."
|
||||||
|
log "Claude Code loads them at start, so restart a running session to pick them up."
|
||||||
@@ -85,10 +85,16 @@ if [[ -d "$extensions_source" ]] && command -v npm >/dev/null 2>&1; then
|
|||||||
# alone takes long enough to be worth not repeating on every re-run of
|
# 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
|
||||||
|
newer_source=''
|
||||||
|
if newer_source="$(find "$extension/src" -type f -newer "$built" -print -quit)" \
|
||||||
|
&& [[ -z "$newer_source" \
|
||||||
|
&& ! "$extension/package.json" -nt "$built" \
|
||||||
|
&& ! "$extension/package-lock.json" -nt "$built" ]]; then
|
||||||
printf 'Vicinae extension %s is already built\n' "$name"
|
printf 'Vicinae extension %s is already built\n' "$name"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
printf 'Building Vicinae extension %s\n' "$name"
|
printf 'Building Vicinae extension %s\n' "$name"
|
||||||
if ! (cd "$extension" && npm ci --silent >/dev/null 2>&1 && npm run build >/dev/null 2>&1); then
|
if ! (cd "$extension" && npm ci --silent >/dev/null 2>&1 && npm run build >/dev/null 2>&1); then
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ live-desktop tests/quickshell/dock-position-contract
|
|||||||
# Reads Hyprland option descriptions from the live compositor to verify enum
|
# 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"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ I'm Gabriel. You're my agent. We will be working together a lot, so I thought I
|
|||||||
|
|
||||||
I'm a software developer at Ksense Technology Group. At Ksense, we basically sell our services as developers to clients to create web applications or web servers. Basically whatever the client wants, but for the most part, we create web applications with Next.js. I am the primary developer for internal tools & web applications. I mainly work on an application called Command Center, which serves as a portal for our project managers to interact with our clients.
|
I'm a software developer at Ksense Technology Group. At Ksense, we basically sell our services as developers to clients to create web applications or web servers. Basically whatever the client wants, but for the most part, we create web applications with Next.js. I am the primary developer for internal tools & web applications. I mainly work on an application called Command Center, which serves as a portal for our project managers to interact with our clients.
|
||||||
|
|
||||||
|
My Ksense work lives in ~/Documents/Code/Ksense. The Ksense-specific sections below (Merge Requests, Coworkers) mostly matter when I am working inside that folder. Anywhere else, assume a personal project.
|
||||||
|
|
||||||
I love to build. I focus on building complex things as simple as possible. I love to find ways to reduce complexity when solving problems.
|
I love to build. I focus on building complex things as simple as possible. I love to find ways to reduce complexity when solving problems.
|
||||||
|
|
||||||
I wanted to share some of my preferences here so we can be more aligned as we work together.
|
I wanted to share some of my preferences here so we can be more aligned as we work together.
|
||||||
@@ -62,6 +64,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 +120,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,25 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# Atlassian's hosted MCP server for Jira and Confluence. No token: it is an
|
||||||
|
# OAuth server and Claude Code signs in through /mcp on first use.
|
||||||
|
atlassian http https://mcp.atlassian.com/v1/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
|
||||||
|
|||||||
@@ -55,26 +55,30 @@ Removing patterns is half the job. Sterile, voiceless writing is just as obvious
|
|||||||
18. **Decorative emojis.** Remove from headings and bullets.
|
18. **Decorative emojis.** Remove from headings and bullets.
|
||||||
19. **Curly quotes.** Replace with straight quotes.
|
19. **Curly quotes.** Replace with straight quotes.
|
||||||
|
|
||||||
|
### Spelling
|
||||||
|
|
||||||
|
20. **British spelling.** The author is American. Catch -our (behaviour, flavour, colour, favour, honour), -ise and -ised (recognise, organise, authorise, prioritise, summarise, initialise), -yse (analyse), -re (centre, metre), -logue (catalogue, dialogue), doubled l (travelling, labelled, modelling), and the odd ones (licence as a noun, judgement, programme, grey, whilst). Fix them to the American form. Two exceptions: a word that is the project's own established vocabulary, spelled that way in the client's documents, the ticket titles and the codebase alike (on APSCA, "enrolment" and "cancelled"), and an existing code identifier, enum value, event type or template key, which is never renamed to fix spelling. Check the codebase once before deciding a word is project vocabulary, and do not flip it back and forth.
|
||||||
|
|
||||||
### Communication artifacts
|
### Communication artifacts
|
||||||
|
|
||||||
20. **Chatbot phrases.** "I hope this helps!", "Let me know if...", "Of course!", "Certainly!", "Found the smoking gun!" Remove.
|
21. **Chatbot phrases.** "I hope this helps!", "Let me know if...", "Of course!", "Certainly!", "Found the smoking gun!" Remove.
|
||||||
21. **Cutoff disclaimers.** "While specific details are limited..." Find sources or remove.
|
22. **Cutoff disclaimers.** "While specific details are limited..." Find sources or remove.
|
||||||
22. **Sycophantic tone.** "Great question! You're absolutely right!" Respond directly.
|
23. **Sycophantic tone.** "Great question! You're absolutely right!" Respond directly.
|
||||||
|
|
||||||
### Filler
|
### Filler
|
||||||
|
|
||||||
23. **Filler phrases.** "In order to" becomes "To". "Due to the fact that" becomes "Because". "It is important to note that" gets deleted.
|
24. **Filler phrases.** "In order to" becomes "To". "Due to the fact that" becomes "Because". "It is important to note that" gets deleted.
|
||||||
24. **Excessive hedging.** "could potentially possibly be argued that it might" becomes "may".
|
25. **Excessive hedging.** "could potentially possibly be argued that it might" becomes "may".
|
||||||
25. **Generic conclusions.** "The future looks bright." State specific plans or facts.
|
26. **Generic conclusions.** "The future looks bright." State specific plans or facts.
|
||||||
|
|
||||||
### Jargon
|
### Jargon
|
||||||
|
|
||||||
26. **Abstract metaphor nouns.** Substrate, wedge, vector, locus, vantage, nexus, primitive (as noun), harness (as metaphor), surface (as in "API surface"), bedrock, scaffolding (as metaphor), modality, paradigm, gold-plating, ratchet (as metaphor), evacuate (for moving code), endgame, north star, flywheel. These read as technical but usually have a plainer concrete word. "Substrate" becomes "base". "Wedge in" becomes "add". "Vector" becomes "way" or "method". "Gold-plating" becomes "more than the job needs". "Ratchet" becomes the mechanism's real name or "a limit that only tightens". "Evacuate" becomes "move out". "Endgame" becomes "the last phase". Pick the concrete word.
|
27. **Abstract metaphor nouns.** Substrate, wedge, vector, locus, vantage, nexus, primitive (as noun), harness (as metaphor), surface (as in "API surface"), bedrock, scaffolding (as metaphor), modality, paradigm, gold-plating, ratchet (as metaphor), evacuate (for moving code), endgame, north star, flywheel. These read as technical but usually have a plainer concrete word. "Substrate" becomes "base". "Wedge in" becomes "add". "Vector" becomes "way" or "method". "Gold-plating" becomes "more than the job needs". "Ratchet" becomes the mechanism's real name or "a limit that only tightens". "Evacuate" becomes "move out". "Endgame" becomes "the last phase". Pick the concrete word.
|
||||||
|
|
||||||
### Plain speech
|
### Plain speech
|
||||||
|
|
||||||
27. **Say what it does, not how it feels.** "the database stays close at hand", "SQL you can read", "types that follow your schema" name a feeling. The fix names the mechanism or a number: "`.toSQL()` returns the exact string sent to the database", "a column rename fails the build". Ask what the sentence tells the reader to do or know, then write that. If you can't restate it as a concrete instruction, fact, or number, cut it. One more check: if the sentence could appear unchanged in another project's docs, it says nothing about this one. Cut it.
|
28. **Say what it does, not how it feels.** "the database stays close at hand", "SQL you can read", "types that follow your schema" name a feeling. The fix names the mechanism or a number: "`.toSQL()` returns the exact string sent to the database", "a column rename fails the build". Ask what the sentence tells the reader to do or know, then write that. If you can't restate it as a concrete instruction, fact, or number, cut it. One more check: if the sentence could appear unchanged in another project's docs, it says nothing about this one. Cut it.
|
||||||
28. **Shorten or split dense sentences.** If the reader has to backtrack to parse a sentence, break it in two or drop clauses. One idea per sentence.
|
29. **Shorten or split dense sentences.** If the reader has to backtrack to parse a sentence, break it in two or drop clauses. One idea per sentence.
|
||||||
29. **Active voice.** Prefer it. Catch "is/are/was/were + past participle" and name the actor: "queries are validated" becomes "the compiler validates queries", "the file is parsed by the loader" becomes "the loader parses the file". Passive is fine only when the actor is unknown or genuinely doesn't matter.
|
30. **Active voice.** Prefer it. Catch "is/are/was/were + past participle" and name the actor: "queries are validated" becomes "the compiler validates queries", "the file is parsed by the loader" becomes "the loader parses the file". Passive is fine only when the actor is unknown or genuinely doesn't matter.
|
||||||
30. **Cut adverbs, or use a stronger verb.** "runs quickly" becomes "is fast" or the number. "significantly improves" becomes the measured delta. An adverb propping up a weak verb means the verb is wrong.
|
31. **Cut adverbs, or use a stronger verb.** "runs quickly" becomes "is fast" or the number. "significantly improves" becomes the measured delta. An adverb propping up a weak verb means the verb is wrong.
|
||||||
31. **Prefer the plain word.** "utilize" becomes "use", "leverage" becomes "use", "facilitate" becomes "help", "numerous" becomes "many", "in the event that" becomes "if". The fancier synonym is rarely clearer.
|
32. **Prefer the plain word.** "utilize" becomes "use", "leverage" becomes "use", "facilitate" becomes "help", "numerous" becomes "many", "in the event that" becomes "if". The fancier synonym is rarely clearer.
|
||||||
|
|||||||
@@ -14,3 +14,10 @@ Host git.gbrown.org
|
|||||||
User git
|
User git
|
||||||
IdentityFile ~/.ssh/id_ed25519
|
IdentityFile ~/.ssh/id_ed25519
|
||||||
IdentitiesOnly yes
|
IdentitiesOnly yes
|
||||||
|
|
||||||
|
# Agent harnesses (Claude Code, Codex) start their shells without
|
||||||
|
# SSH_AUTH_SOCK, and id_ed25519 has a passphrase, so batch ssh from them fails
|
||||||
|
# with "Permission denied (publickey)" even though the key is right. The
|
||||||
|
# desktop keyring agent already holds the unlocked key; point every host at it.
|
||||||
|
Host *
|
||||||
|
IdentityAgent ${XDG_RUNTIME_DIR}/keyring/ssh
|
||||||
|
|||||||
Reference in New Issue
Block a user