No error is a dead end: crash, click, and your agent is already looking
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -1,47 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# How much of the Claude subscription is gone, in the bar.
|
||||
# How much of each agent 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:
|
||||
# The Claude collector 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
|
||||
# 1. The token never reaches argv. A credential 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.
|
||||
# which is the same rule the password and MOK paths already follow. The
|
||||
# collector makes its request in-process, so there is no child to leak it
|
||||
# through -- and this proves no child is spawned.
|
||||
# 2. The token never reaches the output. Neither the panel nor anyone reading
|
||||
# the state directory has any business seeing a credential, and neither
|
||||
# does the collector's own cache.
|
||||
# 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.
|
||||
# 4. An expired token is reported honestly 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.
|
||||
#
|
||||
# And, since the collectors became a fan-out over a directory of records:
|
||||
#
|
||||
# 6. A collector the user switched off does not run, and its stale record
|
||||
# does not linger.
|
||||
# 7. A record is replaced only by a whole, parseable one.
|
||||
# 8. The replacement is atomic -- mktemp and mv, never a redirect into the
|
||||
# file a reader is watching.
|
||||
#
|
||||
# Hermetic throughout. The usage endpoint is a local HTTP fixture, the codex
|
||||
# app-server is a stub, and the fan-out runs stub collectors: nothing here
|
||||
# reaches Anthropic or starts a real agent.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
collector="$repo_dir/config/dot/quickshell/scripts/panama-agent-usage"
|
||||
scripts="$repo_dir/config/dot/quickshell/scripts"
|
||||
claude="$scripts/panama-agent-usage-claude"
|
||||
codex="$scripts/panama-agent-usage-codex"
|
||||
updater="$scripts/panama-agent-usage-update"
|
||||
service="$repo_dir/config/dot/quickshell/services/AgentUsage.qml"
|
||||
widget="$repo_dir/config/dot/quickshell/modules/bar/AgentUsageWidget.qml"
|
||||
panel="$repo_dir/config/dot/quickshell/modules/bar/AgentUsagePanel.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; }
|
||||
for required in "$claude" "$codex" "$updater"; do
|
||||
[[ -x "$required" ]] || {
|
||||
printf 'agent usage contract: %s is not executable\n' "$required" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
[[ -f "$panel" ]] || { printf 'agent usage contract: missing %s\n' "$panel" >&2; exit 1; }
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
server_pid=""
|
||||
cleanup() {
|
||||
[[ -n "$server_pid" ]] && kill "$server_pid" 2>/dev/null
|
||||
rm -rf "$work"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
stub="$work/bin"
|
||||
mkdir -p "$stub"
|
||||
export XDG_STATE_HOME="$work/state"
|
||||
output="$XDG_STATE_HOME/panama/agent-usage.json"
|
||||
mkdir -p "$stub" "$work/claude" "$work/cache" "$work/collectors" "$work/usage"
|
||||
|
||||
readonly SECRET='sk-fixture-token-must-never-appear'
|
||||
|
||||
# ── The endpoint, served locally ────────────────────────────────────────────
|
||||
#
|
||||
# A real request to a real socket, so what the collector puts in the header is
|
||||
# observable -- and so nothing in this file can accidentally reach Anthropic.
|
||||
|
||||
cat >"$work/server.py" <<'PY'
|
||||
import http.server
|
||||
import os
|
||||
import sys
|
||||
|
||||
work = sys.argv[1]
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
with open(os.path.join(work, "headers.txt"), "a") as handle:
|
||||
handle.write((self.headers.get("Authorization") or "") + "\n")
|
||||
try:
|
||||
status = int(open(os.path.join(work, "status")).read().strip())
|
||||
except Exception:
|
||||
status = 200
|
||||
body = open(os.path.join(work, "payload.json"), "rb").read()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
if status == 429:
|
||||
self.send_header("Retry-After", "30")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
|
||||
with open(os.path.join(work, "port"), "w") as handle:
|
||||
handle.write(str(server.server_address[1]))
|
||||
server.serve_forever()
|
||||
PY
|
||||
|
||||
printf '200\n' >"$work/status"
|
||||
: >"$work/headers.txt"
|
||||
cat >"$work/payload.json" <<'JSON'
|
||||
{
|
||||
"five_hour": {"utilization": 42, "resets_at": "2099-08-22T14:00:00Z"},
|
||||
"seven_day": {"utilization": 71, "resets_at": "2099-08-27T00:00:00Z"},
|
||||
"rate_limit_tier": "default_claude_max_5x",
|
||||
"limits": [
|
||||
{"kind": "weekly_scoped", "percent": 31, "resets_at": "2099-08-27T00:00:00Z",
|
||||
"scope": {"model": {"display_name": "Fable 5", "id": "claude-fable-5"}}}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
python3 "$work/server.py" "$work" &
|
||||
server_pid=$!
|
||||
for _ in $(seq 1 50); do
|
||||
[[ -s "$work/port" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
[[ -s "$work/port" ]] || { printf 'agent usage contract: the endpoint fixture did not start\n' >&2; exit 1; }
|
||||
endpoint="http://127.0.0.1:$(cat "$work/port")/usage"
|
||||
|
||||
# A recording curl. Nothing should ever invoke it: the collector makes its
|
||||
# request in-process. If it is called, the argv it was called with is evidence.
|
||||
cat >"$stub/curl" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "\$*" >>"$work/curl-argv"
|
||||
exit 1
|
||||
STUB
|
||||
chmod +x "$stub/curl"
|
||||
: >"$work/curl-argv"
|
||||
|
||||
credentials() {
|
||||
local expires="$1"
|
||||
cat >"$work/credentials.json" <<CREDS
|
||||
@@ -50,128 +152,387 @@ credentials() {
|
||||
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
|
||||
# The collector prints its record; the fan-out is what writes files. Both are
|
||||
# exercised, so both are seamed.
|
||||
run_claude() {
|
||||
PATH="$stub:$PATH" \
|
||||
CLAUDE_CONFIG_DIR="$work/claude" \
|
||||
PANAMA_AGENT_CREDENTIALS="$work/credentials.json" \
|
||||
PANAMA_AGENT_USAGE_ENDPOINT="${FIXTURE_ENDPOINT:-$endpoint}" \
|
||||
PANAMA_AGENT_USAGE_CACHE="$work/cache" \
|
||||
"$claude" "$@" 2>"$work/claude-stderr"
|
||||
}
|
||||
|
||||
# ── 4. An expired token waits rather than failing ───────────────────────────
|
||||
future_ms=$(( ($(date +%s) + 3600) * 1000 ))
|
||||
|
||||
# ── 4. An expired token is honest, and makes no request ─────────────────────
|
||||
|
||||
: >"$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" ]] \
|
||||
record="$(run_claude --force)"
|
||||
[[ "$(jq -r .usageStatusText <<<"$record")" == "Sign-in expired" ]] \
|
||||
|| note "an expired token did not report as expired (got: $(jq -r .usageStatusText <<<"$record"))"
|
||||
[[ -s "$work/headers.txt" ]] \
|
||||
&& note 'a request was made with an expired token'
|
||||
|
||||
# ── 1 & 2. The token stays out of argv and out of the output ────────────────
|
||||
# A credential file that is not there at all is a different sentence.
|
||||
mv "$work/credentials.json" "$work/credentials.away"
|
||||
record="$(run_claude --force)"
|
||||
[[ "$(jq -r .usageStatusText <<<"$record")" == "Waiting for auth" ]] \
|
||||
|| note "a missing sign-in did not report as waiting (got: $(jq -r .usageStatusText <<<"$record"))"
|
||||
[[ -s "$work/headers.txt" ]] \
|
||||
&& note 'a request was made with no token at all'
|
||||
mv "$work/credentials.away" "$work/credentials.json"
|
||||
|
||||
: >"$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 ────────────────────────────────────────
|
||||
# ── 1, 2 & 3. What happens to the token ─────────────────────────────────────
|
||||
|
||||
credentials "$future_ms"
|
||||
before="$(sha256sum "$work/credentials.json" | cut -d' ' -f1)"
|
||||
run
|
||||
record="$(run_claude --force)"
|
||||
|
||||
[[ "$(jq -r .ready <<<"$record")" == "true" ]] \
|
||||
|| note "a valid token did not produce a reading (status: $(jq -r .usageStatusText <<<"$record"))"
|
||||
|
||||
grep -qF "Bearer $SECRET" "$work/headers.txt" \
|
||||
|| note 'the token did not reach the Authorization header, so the request was not authenticated'
|
||||
|
||||
[[ -s "$work/curl-argv" ]] \
|
||||
&& note 'the collector shelled out to curl, where /proc makes an argument world-readable'
|
||||
|
||||
grep -qF "$SECRET" <<<"$record" \
|
||||
&& note 'the token appears in the record the panel reads'
|
||||
grep -qrF "$SECRET" "$work/cache" \
|
||||
&& note "the token appears in the collector's own cache"
|
||||
grep -qF "$SECRET" "$work/claude-stderr" \
|
||||
&& note 'the token appears on stderr, where the journal keeps it'
|
||||
|
||||
[[ "$(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' \
|
||||
uncommented() { python3 - "$1" <<'PY'
|
||||
import re, sys
|
||||
text = open(sys.argv[1], encoding="utf-8").read()
|
||||
# Module docstring first, then whole-line comments.
|
||||
text = re.sub(r'^\s*(?:"""|\'\'\')(?:.|\n)*?(?:"""|\'\'\')', "", text, count=1)
|
||||
print("\n".join(line for line in text.split("\n") if not line.lstrip().startswith("#")))
|
||||
PY
|
||||
}
|
||||
|
||||
uncommented "$claude" | 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' \
|
||||
uncommented "$claude" | grep -qE 'credentials.*\.write_text|open\([^)]*credentials[^)]*"w"' \
|
||||
&& 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'
|
||||
# The endpoint reports percentages already. Treating them as 0..1 fractions and
|
||||
# multiplying is how the bar once came to read 1500%.
|
||||
|
||||
# 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"}}'
|
||||
# Whole percents: jq prints 1 and 1.0 for the same number, and a contract that
|
||||
# fails on the spelling of a float teaches nothing.
|
||||
percent_for() { jq -r --arg label "$1" '.limits[] | select(.label == $label) | .percent * 100 | round' <<<"$record"; }
|
||||
|
||||
[[ "$(percent_for 'Session (5-hour)')" == "42" ]] \
|
||||
|| note "the five-hour reading is $(percent_for 'Session (5-hour)')% for a reported 42%"
|
||||
[[ "$(percent_for 'Weekly (7-day)')" == "71" ]] \
|
||||
|| note "the weekly reading is $(percent_for 'Weekly (7-day)')% for a reported 71%"
|
||||
|
||||
# The model-scoped window lives only in the `limits` array. A collector reading
|
||||
# the flat buckets alone drops a limit the account is really spending against.
|
||||
[[ "$(percent_for 'Fable 5 Weekly')" == "31" ]] \
|
||||
|| note 'the model-scoped limit from the limits array was not read'
|
||||
|
||||
[[ "$(jq -r .tierLabel <<<"$record")" == "Max 5x" ]] \
|
||||
|| note "the plan label is $(jq -r .tierLabel <<<"$record"), not the Max 5x the credential names"
|
||||
|
||||
# Nothing the panel shows may fall outside the range a percentage has, whatever
|
||||
# the endpoint says.
|
||||
cat >"$work/payload.json" <<'JSON'
|
||||
{"five_hour": {"utilization": 1500}, "seven_day": {"utilization": -4}}
|
||||
JSON
|
||||
rm -f "$work/cache/claude-limits.json"
|
||||
record="$(run_claude --force)"
|
||||
[[ "$(percent_for 'Session (5-hour)')" == "100" ]] \
|
||||
|| note "an out-of-range reading was not clamped (got: $(percent_for 'Session (5-hour)'))"
|
||||
[[ -z "$(percent_for 'Weekly (7-day)')" ]] \
|
||||
|| note 'a negative reading was reported as a percentage rather than dropped'
|
||||
|
||||
# A payload that speaks the older fraction convention is read as fractions.
|
||||
cat >"$work/payload.json" <<'JSON'
|
||||
{"five_hour": {"utilization": 0.42}, "seven_day": {"utilization": 0.71}}
|
||||
JSON
|
||||
rm -f "$work/cache/claude-limits.json"
|
||||
record="$(run_claude --force)"
|
||||
[[ "$(percent_for 'Session (5-hour)')" == "42" ]] \
|
||||
|| note "a fraction-scaled payload was misread as $(percent_for 'Session (5-hour)')%"
|
||||
|
||||
# ── A cached limit outlives its probe, but not its window ───────────────────
|
||||
#
|
||||
# Once a window has reset, a cached figure describes a period that is over. A
|
||||
# stale 78% on a fresh week is a lie the panel must not tell.
|
||||
|
||||
dead_endpoint="http://127.0.0.1:1/usage"
|
||||
|
||||
cat >"$work/cache/claude-limits.json" <<'JSON'
|
||||
{"fetchedAtMs":0,"limits":[{"label":"Weekly (7-day)","percent":0.78,"resetsAt":"2099-01-01T00:00:00+00:00"}]}
|
||||
JSON
|
||||
record="$(FIXTURE_ENDPOINT="$dead_endpoint" run_claude --force)"
|
||||
[[ "$(percent_for 'Weekly (7-day)')" == "78" ]] \
|
||||
|| note 'a cached limit whose window is still open was thrown away'
|
||||
|
||||
cat >"$work/cache/claude-limits.json" <<'JSON'
|
||||
{"fetchedAtMs":0,"limits":[{"label":"Weekly (7-day)","percent":0.78,"resetsAt":"2000-01-01T00:00:00+00:00"}]}
|
||||
JSON
|
||||
record="$(FIXTURE_ENDPOINT="$dead_endpoint" run_claude --force)"
|
||||
[[ -z "$(percent_for 'Weekly (7-day)')" ]] \
|
||||
&& [[ "$(jq -r .usageStatusText <<<"$record")" == "Claude limits unavailable" ]] \
|
||||
|| note 'a cached limit was reported after its window had already reset'
|
||||
|
||||
# ── A transport failure is not an answer ────────────────────────────────────
|
||||
#
|
||||
# Nothing reached a server: retry sooner than the interval. An HTTP status IS a
|
||||
# server, and pestering it is how a rate limit becomes a ban.
|
||||
|
||||
rm -f "$work/cache/claude-limits.json"
|
||||
record="$(FIXTURE_ENDPOINT="$dead_endpoint" run_claude --force)"
|
||||
[[ "$(jq -r '.retryAdvised // false' <<<"$record")" == "true" ]] \
|
||||
|| note 'a transport failure did not ask the shell to retry sooner'
|
||||
|
||||
printf '500\n' >"$work/status"
|
||||
record="$(run_claude --force)"
|
||||
[[ "$(jq -r '.retryAdvised // false' <<<"$record")" == "false" ]] \
|
||||
|| note 'an HTTP error asked for a fast retry, which turns a bad status into a hammer'
|
||||
|
||||
printf '429\n' >"$work/status"
|
||||
record="$(run_claude --force)"
|
||||
grep -q 'retry after 30s' <<<"$(jq -r .authHelpText <<<"$record")" \
|
||||
|| note "a 429 did not carry its retry-after into the help text (got: $(jq -r .authHelpText <<<"$record"))"
|
||||
printf '200\n' >"$work/status"
|
||||
|
||||
# ── The Codex collector asks read-only, and reads the right field ───────────
|
||||
|
||||
cat >"$stub/codex" <<'STUB'
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
with open(os.environ["CODEX_STUB_ARGV"], "w") as handle:
|
||||
handle.write(" ".join(sys.argv[1:]))
|
||||
|
||||
RESULTS = {
|
||||
"initialize": {"userAgent": "stub"},
|
||||
"account/read": {"account": {"type": "chatgpt", "planType": "prolite"}},
|
||||
"account/rateLimits/read": {
|
||||
"rateLimits": {
|
||||
"planType": "prolite",
|
||||
"primary": {"usedPercent": 55, "windowDurationMins": 10080, "resetsAt": 4102444800},
|
||||
"secondary": None,
|
||||
},
|
||||
"rateLimitsByLimitId": {
|
||||
"codex": {"limitName": None, "primary": {"usedPercent": 55, "windowDurationMins": 10080}},
|
||||
"codex_spark": {
|
||||
"limitName": "Spark",
|
||||
"primary": {"usedPercent": 12, "windowDurationMins": 300, "resetsAt": 4102444800},
|
||||
"secondary": None,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
message = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if "id" not in message:
|
||||
continue
|
||||
print(json.dumps({"id": message["id"], "result": RESULTS.get(message.get("method"), {})}), flush=True)
|
||||
STUB
|
||||
chmod +x "$stub/curl"
|
||||
run
|
||||
[[ "$(jq -r .status "$output")" == "unavailable" ]] \
|
||||
|| note 'an error response was not reported as unavailable'
|
||||
chmod +x "$stub/codex"
|
||||
|
||||
# ── 5. The widget hides itself ──────────────────────────────────────────────
|
||||
mkdir -p "$work/codex/sessions/2026/08/25"
|
||||
cat >"$work/codex/sessions/2026/08/25/session.jsonl" <<'JSONL'
|
||||
{"type":"turn_context","payload":{"model":"gpt-5.6-sol"}}
|
||||
{"type":"token_count","timestamp":"2026-08-25T10:00:00Z","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":999999,"output_tokens":999999},"last_token_usage":{"input_tokens":1200,"cached_input_tokens":1000,"output_tokens":300}}}}
|
||||
{"type":"token_count","timestamp":"2026-08-25T10:05:00Z","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":1999999,"output_tokens":1999999},"last_token_usage":{"input_tokens":1200,"cached_input_tokens":1000,"output_tokens":300}}}}
|
||||
JSONL
|
||||
touch "$work/codex/sessions/2026/08/25/session.jsonl"
|
||||
|
||||
codex_record="$(
|
||||
PATH="$stub:$PATH" \
|
||||
CODEX_HOME="$work/codex" \
|
||||
CODEX_STUB_ARGV="$work/codex-argv" \
|
||||
PANAMA_AGENT_CODEX_BIN="$stub/codex" \
|
||||
PANAMA_AGENT_USAGE_CACHE="$work/cache" \
|
||||
"$codex" --force 2>/dev/null
|
||||
)"
|
||||
|
||||
grep -q -- '-s read-only' "$work/codex-argv" \
|
||||
|| note "the codex app-server was not asked for read-only ($(cat "$work/codex-argv" 2>/dev/null))"
|
||||
grep -q -- 'app-server' "$work/codex-argv" \
|
||||
|| note 'the codex collector did not start the app-server'
|
||||
|
||||
[[ "$(jq -r '.limits[] | select(.label == "Weekly (7-day)") | .percent * 100 | round' <<<"$codex_record")" == "55" ]] \
|
||||
|| note 'the codex weekly limit was not read from the app-server'
|
||||
[[ "$(jq -r '.limits[] | select(.label == "Spark Session (5-hour)") | .percent * 100 | round' <<<"$codex_record")" == "12" ]] \
|
||||
|| note 'the codex model-scoped limit was not read'
|
||||
[[ "$(jq -r .tierLabel <<<"$codex_record")" == "prolite" ]] \
|
||||
|| note 'the codex plan was not read'
|
||||
|
||||
# total_token_usage is cumulative for the session. Counting it instead of
|
||||
# last_token_usage makes usage grow quadratically: these two snapshots are
|
||||
# 1500 tokens each, not four million.
|
||||
[[ "$(jq -r .todayTotalTokens <<<"$codex_record")" == "3000" ]] \
|
||||
|| note "the codex scan counted $(jq -r .todayTotalTokens <<<"$codex_record") tokens where the last-turn rule gives 3000"
|
||||
|
||||
# ── 6, 7 & 8. The fan-out ───────────────────────────────────────────────────
|
||||
|
||||
cat >"$work/collectors/panama-agent-usage-claude" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
printf 'claude\n' >>"$work/ran"
|
||||
printf '%s\n' '{"schemaVersion":1,"id":"claude","name":"Claude Code","ready":true,"limits":[]}'
|
||||
STUB
|
||||
cat >"$work/collectors/panama-agent-usage-codex" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
printf 'codex\n' >>"$work/ran"
|
||||
printf '%s\n' '{"schemaVersion":1,"id":"codex","name":"Codex","ready":true,"limits":[]}'
|
||||
STUB
|
||||
chmod +x "$work/collectors"/panama-agent-usage-*
|
||||
|
||||
run_update() {
|
||||
PANAMA_AGENT_USAGE_COLLECTORS="$work/collectors" \
|
||||
PANAMA_AGENT_USAGE_DIR="$work/usage" \
|
||||
PANAMA_SETTINGS="$work/settings.json" \
|
||||
"$updater" "$@" 2>"$work/update-stderr"
|
||||
}
|
||||
|
||||
# Both on: both records appear.
|
||||
printf '%s\n' '{"agentUsageClaude":true,"agentUsageCodex":true}' >"$work/settings.json"
|
||||
: >"$work/ran"
|
||||
run_update
|
||||
[[ -s "$work/usage/claude.json" && -s "$work/usage/codex.json" ]] \
|
||||
|| note 'the fan-out did not write a record for every enabled collector'
|
||||
|
||||
# One off: it must not run at all. "Off" has to mean "no process" -- the
|
||||
# collector is the thing that reads a credential and makes a request.
|
||||
printf '%s\n' '{"agentUsageClaude":true,"agentUsageCodex":false}' >"$work/settings.json"
|
||||
: >"$work/ran"
|
||||
run_update
|
||||
grep -qx 'codex' "$work/ran" \
|
||||
&& note 'a collector the user switched off still ran'
|
||||
[[ -e "$work/usage/codex.json" ]] \
|
||||
&& note "a disabled collector's stale record was left behind for the panel to show"
|
||||
|
||||
# Absent means default, and the default is on. `.key // true` would read false
|
||||
# as absent, which is how a switched-off collector comes back to life.
|
||||
printf '%s\n' '{}' >"$work/settings.json"
|
||||
: >"$work/ran"
|
||||
run_update
|
||||
grep -qx 'codex' "$work/ran" \
|
||||
|| note 'an unset per-agent preference was not treated as its default of on'
|
||||
|
||||
# 7. A record is replaced only by a whole, parseable one.
|
||||
printf '%s\n' '{"agentUsageClaude":true,"agentUsageCodex":true}' >"$work/settings.json"
|
||||
run_update
|
||||
good="$(sha256sum "$work/usage/codex.json" | cut -d' ' -f1)"
|
||||
cat >"$work/collectors/panama-agent-usage-codex" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' 'Traceback (most recent call last):'
|
||||
exit 1
|
||||
STUB
|
||||
chmod +x "$work/collectors/panama-agent-usage-codex"
|
||||
run_update
|
||||
[[ "$(sha256sum "$work/usage/codex.json" | cut -d' ' -f1)" == "$good" ]] \
|
||||
|| note 'a collector that died mid-run replaced a good record with rubbish'
|
||||
jq -e . >/dev/null 2>&1 <"$work/usage/codex.json" \
|
||||
|| note 'the usage directory holds something that is not JSON'
|
||||
|
||||
# 8. Atomic, and tidy: no temp file survives a failure for the panel to read.
|
||||
compgen -G "$work/usage/.*.??????" >/dev/null \
|
||||
&& note 'the fan-out left a temporary record behind in the directory the panel watches'
|
||||
grep -qE '>[[:space:]]*"\$USAGE_DIR/\$agent\.json"' "$updater" \
|
||||
&& note 'the fan-out redirects straight into the file a reader is watching'
|
||||
grep -q 'mktemp' "$updater" && grep -q 'mv "\$tmp"' "$updater" \
|
||||
|| note 'the fan-out does not write through mktemp and mv, so a reader can catch a half-written record'
|
||||
|
||||
# ── 5. The widget hides itself, and the panel is what it opens ──────────────
|
||||
|
||||
qml_uncommented() { grep -v '^[[:space:]]*//' "$1"; }
|
||||
|
||||
grep -q 'Settings.showAgentUsage && AgentUsage.available' "$widget" \
|
||||
|| note 'the widget does not gate on both the preference and having real numbers'
|
||||
qml_uncommented "$widget" | grep -q 'interactive: false' \
|
||||
&& note "the widget disables Pill's mouse area, so its click handlers never fire"
|
||||
qml_uncommented "$widget" | grep -q 'onActivated' \
|
||||
|| note 'left-clicking the widget does nothing'
|
||||
qml_uncommented "$widget" | grep -q 'onSecondaryActivated: ShellState.openSettings' \
|
||||
|| note 'right-clicking the widget no longer opens the settings that govern it'
|
||||
qml_uncommented "$widget" | grep -q 'AgentUsagePanel' \
|
||||
|| note 'the widget does not open the usage panel'
|
||||
|
||||
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'
|
||||
for key in showAgentUsage agentUsageClaude agentUsageCodex agentUsageRefreshMinutes; do
|
||||
grep -q "\"$key\"" "$aliases" \
|
||||
|| note "Settings.qml does not alias $key, so the binding reads undefined"
|
||||
done
|
||||
|
||||
# The two-tier colouring is better than one threshold and is not to be lost in
|
||||
# a refactor: amber before it hurts, red when it is about to.
|
||||
qml_uncommented "$widget" | grep -q 'AgentUsage.headline >= 90' \
|
||||
|| note 'the widget lost its red tier at 90%'
|
||||
qml_uncommented "$widget" | grep -q 'AgentUsage.headline >= 75' \
|
||||
|| note 'the widget lost its amber tier at 75%'
|
||||
|
||||
# The collectors run on a timer measured in minutes, never on a repaint, and
|
||||
# only while the readout was asked for.
|
||||
python3 - "$service" <<'PY' || note 'the collectors are not run on a minute-scale timer driven by the preference'
|
||||
import re
|
||||
import sys
|
||||
|
||||
# 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:
|
||||
if not re.search(r"interval:\s*root\.refreshMinutes\s*\*\s*60\s*\*\s*1000", text):
|
||||
raise SystemExit(1)
|
||||
if "Settings.agentUsageRefreshMinutes" not in text:
|
||||
raise SystemExit(1)
|
||||
if not re.search(r"running:\s*Settings\.showAgentUsage", text):
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
|
||||
# The panel and the service read records; neither has any business knowing what
|
||||
# a credential looks like.
|
||||
for file in "$service" "$panel" "$widget"; do
|
||||
grep -qE 'credentials|accessToken|Authorization' "$file" \
|
||||
&& note "$(basename "$file") mentions a credential; only the collector may"
|
||||
done
|
||||
|
||||
# Nothing in the panel repaints on a clock. The one timer it owns advances a
|
||||
# displayed time and only runs while the panel is open.
|
||||
python3 - "$panel" <<'PY' || note 'the panel animates or ticks while it is closed'
|
||||
import re
|
||||
import sys
|
||||
|
||||
text = open(sys.argv[1], encoding="utf-8").read()
|
||||
for block in re.findall(r"Timer\s*\{(.*?)\n \}", text, re.S):
|
||||
if "running: root.visible" not in block:
|
||||
raise SystemExit(1)
|
||||
if re.search(r"\b(SequentialAnimation|loops:\s*Animation\.Infinite)\b", text):
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
|
||||
# ── The retired collector stays retired ─────────────────────────────────────
|
||||
|
||||
[[ -e "$scripts/panama-agent-usage" ]] \
|
||||
&& note 'the single-agent collector is still on disk; its consumers moved to the fan-out'
|
||||
grep -rqF 'scripts/panama-agent-usage"' "$repo_dir/config" "$repo_dir/bin" 2>/dev/null \
|
||||
&& note 'something still calls the retired single-agent collector'
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
printf 'agent usage contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
|
||||
Reference in New Issue
Block a user