The last of Section F, and the only thing in Panama that reads an authentication token, so most of the design is about that rather than about the number. It never refreshes the token and never writes to the credentials file. That token expires roughly hourly and Claude Code refreshes it on demand; if this refreshed it too, two processes would be rotating one credential, and a rotation invalidates the other holder's copy. The failure mode is being silently signed out of Claude Code by a status widget, which no bar indicator is worth. So it reads the token, uses it while valid, and reports "waiting for Claude Code to refresh" when not -- which covers the case that matters, because while you are using Claude Code the token is fresh, and while you are not there is nothing to watch. The token never reaches argv either: curl takes the Authorization header on stdin through --config, because a header passed as an argument sits in /proc/<pid>/cmdline for the length of the request. Same rule the password and MOK paths already follow. And it never reaches the output: the record carries percentages and timestamps and nothing else. Both are pinned, and both were checked by sabotaging the collector to pass -H and watching the contract name it. Off by default. It is a coding-tool readout, not something a general-purpose desktop shows without being asked, and it hides unless the collector has real numbers rather than displaying "unknown".
152 lines
6.6 KiB
Bash
Executable File
152 lines
6.6 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":0.42,"resets_at":"2026-08-22T14:00:00Z"},"seven_day":{"utilization":0.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 ─────────────────────────────────────────────────
|
|
|
|
[[ "$(jq -r '.usage.fiveHour.used' "$output")" == "42" ]] \
|
|
|| note 'the five-hour utilisation was not converted to a percentage'
|
|
[[ "$(jq -r '.usage.week.used' "$output")" == "71" ]] \
|
|
|| note 'the weekly utilisation was not converted to a percentage'
|
|
|
|
# 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'
|