Files
Panama/tests/quickshell/agent-usage-contract
T
Gabriel Brown 669a228286 Install the desktop before anything allowed to fail, and let Terra be installed twice
Two bugs, one story: ./install on a machine that had been half set up before
produced no Hyprland at all, and said so in one line among twenty minutes of
scrollback.

Terra bootstraps itself with --repofrompath, which defines a throwaway repo id
just long enough to install terra-release. Run it again on a machine that
already has terra-release and dnf5 refuses the whole transaction -- the
throwaway id collides with the real one. That step sits above everything, so
set -e ended the stage before a single package was considered. It is skipped
now when terra-release is already installed.

The rest is the reason one failed repo cost the desktop. Hyprland was installed
near the bottom of the stage, below a codec swap, two group updates and a
GStreamer glob, any one of which can fail for reasons outside this repository.
It now installs directly after the packages it needs and before anything
optional, and everything fragile below it runs through a soft helper that logs
and continues rather than ending the run. What was stepped over is listed at
the end, because tolerating a failure only beats aborting on it if somebody is
told.

A missing Hyprland is still fatal, and now says so in words.

Also removes the leftover disabled solopasha/hyprland COPR, which would mix
with lionheartp's the moment anyone enabled it while debugging.

Fixes the usage widget reading 1500%: the endpoint reports percentages, not
0..1 fractions. Clamped as well, and the widget answers a click now -- it set
interactive:false, which disables the mouse area its own handler needed.
2026-08-22 09:36:54 -04:00

182 lines
8.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# How much of the Claude subscription is gone, in the bar.
#
# This is the only thing in Panama that reads an authentication token, so most
# of what is pinned here is about that rather than about the number:
#
# 1. The token never reaches argv. A header passed as an argument is
# world-readable in /proc/<pid>/cmdline for as long as the request takes,
# which is the same rule the password and MOK paths already follow.
# 2. The token never reaches the output. The widget has no business seeing a
# credential and neither does anyone reading the state file.
# 3. It NEVER refreshes the token and never writes to the credentials file.
# That token expires hourly and Claude Code refreshes it on demand; two
# processes rotating one credential means being silently signed out of
# Claude Code by a status widget, and no bar indicator is worth that.
# 4. An expired token is reported as waiting, not as an error, and no request
# is made with it.
# 5. The widget hides unless it was asked for AND there are real numbers. An
# indicator reading "unknown" is worse than an empty space.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
collector="$repo_dir/config/dot/quickshell/scripts/panama-agent-usage"
service="$repo_dir/config/dot/quickshell/services/AgentUsage.qml"
widget="$repo_dir/config/dot/quickshell/modules/bar/AgentUsageWidget.qml"
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
aliases="$repo_dir/config/dot/quickshell/config/Settings.qml"
findings=()
note() { findings+=("$1"); }
[[ -x "$collector" ]] || { printf 'agent usage contract: %s is not executable\n' "$collector" >&2; exit 1; }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
stub="$work/bin"
mkdir -p "$stub"
export XDG_STATE_HOME="$work/state"
output="$XDG_STATE_HOME/panama/agent-usage.json"
readonly SECRET='sk-fixture-token-must-never-appear'
credentials() {
local expires="$1"
cat >"$work/credentials.json" <<CREDS
{"claudeAiOauth":{"accessToken":"$SECRET","refreshToken":"refresh-fixture",
"expiresAt":$expires,"rateLimitTier":"default_claude_max_5x","subscriptionType":"max"}}
CREDS
}
# Records how it was invoked, including whether the secret was in argv.
cat >"$stub/curl" <<STUB
#!/usr/bin/env bash
printf '%s\n' "\$*" >>"$work/curl-argv"
printf '%s\n' '{"five_hour":{"utilization":42,"resets_at":"2026-08-22T14:00:00Z"},"seven_day":{"utilization":71,"resets_at":"2026-08-27T00:00:00Z"},"rate_limit_tier":"default_claude_max_5x"}'
STUB
chmod +x "$stub/curl"
run() {
PATH="$stub:$PATH" PANAMA_AGENT_CREDENTIALS="$work/credentials.json" \
PANAMA_AGENT_USAGE_ENDPOINT="https://example.invalid/usage" \
"$collector" >/dev/null 2>&1
}
# ── 4. An expired token waits rather than failing ───────────────────────────
: >"$work/curl-argv"
credentials 1000
run
[[ "$(jq -r .status "$output")" == "stale" ]] \
|| note "an expired token did not report as stale (got: $(jq -r .status "$output"))"
[[ -s "$work/curl-argv" ]] \
&& note 'a request was made with an expired token'
# ── 1 & 2. The token stays out of argv and out of the output ────────────────
: >"$work/curl-argv"
credentials "$(( ($(date +%s) + 3600) * 1000 ))"
run
[[ "$(jq -r .status "$output")" == "ok" ]] \
|| note "a valid token did not produce a reading (got: $(jq -r .detail "$output"))"
grep -qF "$SECRET" "$work/curl-argv" \
&& note 'the token was passed to curl as an argument, where /proc makes it world-readable'
grep -q -- '--config' "$work/curl-argv" \
|| note 'the token is not passed through a curl config file, so it may reach argv'
grep -qrF "$SECRET" "$XDG_STATE_HOME" \
&& note 'the token appears in the state file the widget reads'
# ── 3. Credentials are never written ────────────────────────────────────────
before="$(sha256sum "$work/credentials.json" | cut -d' ' -f1)"
run
[[ "$(sha256sum "$work/credentials.json" | cut -d' ' -f1)" == "$before" ]] \
|| note 'the collector modified the credentials file'
# Comments stripped: the header explains at length that it does not refresh,
# and matching that is matching documentation.
uncommented() { grep -v '^[[:space:]]*#' "$1"; }
uncommented "$collector" | grep -qE 'refreshToken|refresh_token|grant_type' \
&& note 'the collector touches the refresh token, which can sign Claude Code out'
uncommented "$collector" | grep -qE '>[[:space:]]*"?\$?\{?CREDENTIALS' \
&& note 'the collector writes to the credentials file'
# ── The reading it produced ─────────────────────────────────────────────────
# The endpoint already reports percentages. Treating them as 0..1 fractions and
# multiplying is how the bar came to read 1500%.
[[ "$(jq -r '.usage.fiveHour.used' "$output")" == "42" ]] \
|| note "the five-hour reading is $(jq -r '.usage.fiveHour.used' "$output") for a reported 42%"
[[ "$(jq -r '.usage.week.used' "$output")" == "71" ]] \
|| note "the weekly reading is $(jq -r '.usage.week.used' "$output") for a reported 71%"
# Nothing the widget shows may fall outside the range a percentage has, whatever
# the endpoint says. A readout that can print 1500% is one you learn to ignore.
cat >"$stub/curl" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' '{"five_hour":{"utilization":1500},"seven_day":{"utilization":-4}}'
STUB
chmod +x "$stub/curl"
run
[[ "$(jq -r '.usage.fiveHour.used' "$output")" == "100" ]] \
|| note 'an out-of-range reading was not clamped to 100'
[[ "$(jq -r '.usage.week.used' "$output")" == "0" ]] \
|| note 'a negative reading was not clamped to 0'
cat >"$stub/curl" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' '{"five_hour":{"utilization":42,"resets_at":"2026-08-22T14:00:00Z"},"seven_day":{"utilization":71,"resets_at":"2026-08-27T00:00:00Z"},"rate_limit_tier":"default_claude_max_5x"}'
STUB
chmod +x "$stub/curl"
# ── It answers a click ──────────────────────────────────────────────────────
#
# Pill's MouseArea is gated on `interactive`, so a widget that sets it false and
# connects onSecondaryActivated has a handler nothing can ever reach.
uncommented "$widget" | grep -q 'interactive: false' \
&& note 'the widget disables Pill''s mouse area, so its click handlers never fire'
uncommented "$widget" | grep -q 'onActivated' \
|| note 'left-clicking the widget does nothing'
# An endpoint that answers with something else must degrade, not crash.
cat >"$stub/curl" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' '{"error":{"message":"nope"}}'
STUB
chmod +x "$stub/curl"
run
[[ "$(jq -r .status "$output")" == "unavailable" ]] \
|| note 'an error response was not reported as unavailable'
# ── 5. The widget hides itself ──────────────────────────────────────────────
grep -q 'Settings.showAgentUsage && AgentUsage.available' "$widget" \
|| note 'the widget does not gate on both the preference and having real numbers'
grep -q 'key: "showAgentUsage"' "$schema" || note 'there is no showAgentUsage preference'
grep -A2 'key: "showAgentUsage"' "$schema" | grep -q 'def: false' \
|| note 'the usage widget is on by default; it is a coding-tool readout, not general-purpose desktop furniture'
grep -q 'showAgentUsage' "$aliases" \
|| note 'Settings.qml does not alias showAgentUsage, so the binding reads undefined'
# The collector runs on a timer measured in minutes, never on a repaint.
python3 - "$service" <<'PY' || note 'the collector is not run on a minute-scale timer'
import re, sys
text = open(sys.argv[1], encoding="utf-8").read()
match = re.search(r"interval:\s*(\d+)\s*\*\s*60\s*\*\s*1000", text)
if not match or int(match.group(1)) < 1:
raise SystemExit(1)
PY
if (( ${#findings[@]} > 0 )); then
printf 'agent usage contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'agent usage contract: PASS\n'