Show how much of the subscription is gone, without risking the session
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".
This commit is contained in:
@@ -135,7 +135,7 @@ docs/ Settings reference, and the design specs behind the work
|
||||
|
||||
## Tests
|
||||
|
||||
150 of them, under `tests/`. Run the lot, or a subset by pattern:
|
||||
151 of them, under `tests/`. Run the lot, or a subset by pattern:
|
||||
|
||||
```sh
|
||||
panama test # everything
|
||||
|
||||
@@ -93,6 +93,14 @@ Singleton {
|
||||
detail: "Show the charge level in the bar, on machines that have a battery"
|
||||
},
|
||||
|
||||
// Off by default: this is a coding-tool readout, not something a
|
||||
// general-purpose desktop should show without being asked.
|
||||
{
|
||||
key: "showAgentUsage", type: "bool", def: false, group: "vitals",
|
||||
label: "Claude usage",
|
||||
detail: "Show how much of the Claude subscription has been used, beside the other vitals"
|
||||
},
|
||||
|
||||
// ── Battery ─────────────────────────────────────────────────────────
|
||||
// The two points at which the desktop starts telling you. Low is a
|
||||
// quiet mention; critical is the one that interrupts, so it is
|
||||
|
||||
@@ -39,6 +39,7 @@ Singleton {
|
||||
readonly property bool showMemory: DesktopPreferences.get("showMemory")
|
||||
readonly property bool showGpu: DesktopPreferences.get("showGpu")
|
||||
readonly property bool showBattery: DesktopPreferences.get("showBattery")
|
||||
readonly property bool showAgentUsage: DesktopPreferences.get("showAgentUsage")
|
||||
|
||||
// ── Battery ─────────────────────────────────────────────────────────────
|
||||
readonly property int batteryLowPercent: DesktopPreferences.get("batteryLowPercent")
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// How much of the Claude subscription is gone, beside the other vitals.
|
||||
//
|
||||
// One number: whichever window is closer to its limit, because that is the one
|
||||
// about to interrupt you.
|
||||
//
|
||||
// Hidden unless asked for AND the collector has real numbers. A bar indicator
|
||||
// reading "unknown" is worse than an empty space, and this is off by default:
|
||||
// it is a coding-tool readout, not something a general-purpose desktop shows
|
||||
// without being asked.
|
||||
//
|
||||
// Children go straight into Pill's own Row -- it adopts them through its
|
||||
// default alias, so wrapping them in another Row and centring that is both
|
||||
// redundant and a warning at load.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.widgets
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
|
||||
visible: Settings.showAgentUsage && AgentUsage.available
|
||||
interactive: false
|
||||
|
||||
onSecondaryActivated: ShellState.openSettings("appearance")
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "\u{F1719}" // md-robot-outline
|
||||
color: {
|
||||
if (AgentUsage.headline >= 90) return Theme.danger;
|
||||
if (AgentUsage.headline >= 75) return Theme.warn;
|
||||
return Theme.fgDim;
|
||||
}
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: AgentUsage.headline + "%"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.features: Theme.tabularFigures
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,8 @@ PanelWindow {
|
||||
screen: root.screen
|
||||
}
|
||||
|
||||
AgentUsageWidget {}
|
||||
|
||||
VitalsWidget {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# How much of the Claude subscription this account has used.
|
||||
#
|
||||
# Writes one display-ready record to $XDG_STATE_HOME/panama/agent-usage.json.
|
||||
# The bar widget only ever reads that file, so adding a second agent later is a
|
||||
# collector rather than a change to any QML.
|
||||
#
|
||||
# ── What this deliberately does NOT do ───────────────────────────────────────
|
||||
#
|
||||
# It never refreshes the OAuth token, and it never writes to
|
||||
# ~/.claude/.credentials.json.
|
||||
#
|
||||
# That token expires about hourly and Claude Code refreshes it on demand. If
|
||||
# this refreshed it too, two processes would be rotating one credential: a
|
||||
# refresh that rotates the refresh token invalidates the other holder's copy,
|
||||
# and the failure mode is being silently logged out of Claude Code by a status
|
||||
# widget. No bar indicator is worth that.
|
||||
#
|
||||
# So this reads the token, uses it if it is still valid, and reports
|
||||
# "unavailable" if it is not. In practice that covers the case that matters --
|
||||
# while you are actually using Claude Code the token is fresh, and while you
|
||||
# are not, there is nothing to watch.
|
||||
#
|
||||
# ── The token ────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Never reaches argv. `curl --config -` takes the Authorization header on
|
||||
# stdin, because a header passed as an argument is world-readable in
|
||||
# /proc/<pid>/cmdline for as long as the request takes -- the same rule
|
||||
# panama-pick follows for passwords and panama-sudo for the MOK hash.
|
||||
#
|
||||
# Never reaches the output either. The record below carries percentages and
|
||||
# timestamps and nothing else; the widget has no business seeing a credential
|
||||
# and neither does anyone reading the state file.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
CREDENTIALS="${PANAMA_AGENT_CREDENTIALS:-$HOME/.claude/.credentials.json}"
|
||||
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
|
||||
OUTPUT="$STATE_DIR/agent-usage.json"
|
||||
ENDPOINT="${PANAMA_AGENT_USAGE_ENDPOINT:-https://api.anthropic.com/api/oauth/usage}"
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
# Written whatever happens, so the widget can distinguish "no data yet" from
|
||||
# "collector never ran" and hide itself for the right reason.
|
||||
emit() {
|
||||
local status="$1" detail="${2:-}" body="${3:-null}"
|
||||
local tmp
|
||||
tmp="$(mktemp "$OUTPUT.XXXXXX")"
|
||||
jq -n --arg status "$status" --arg detail "$detail" \
|
||||
--argjson usage "$body" --arg at "$(date -Is)" \
|
||||
'{status: $status, detail: $detail, collectedAt: $at, usage: $usage}' \
|
||||
>"$tmp" 2>/dev/null || printf '{"status":"error","detail":"could not write","usage":null}' >"$tmp"
|
||||
mv "$tmp" "$OUTPUT"
|
||||
}
|
||||
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
[[ -r "$CREDENTIALS" ]] || { emit unavailable "Claude Code is not signed in on this machine."; exit 0; }
|
||||
|
||||
expires="$(jq -r '.claudeAiOauth.expiresAt // 0' "$CREDENTIALS" 2>/dev/null)"
|
||||
[[ "$expires" =~ ^[0-9]+$ ]] || expires=0
|
||||
now="$(( $(date +%s) * 1000 ))"
|
||||
|
||||
# Thirty seconds of headroom: a token about to expire will have expired by the
|
||||
# time the request lands, and a 401 is a worse answer than an honest wait.
|
||||
if (( expires <= now + 30000 )); then
|
||||
emit stale "Waiting for Claude Code to refresh its session."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
config="$(mktemp)"
|
||||
cleanup() { rm -f "$config"; }
|
||||
trap cleanup EXIT
|
||||
chmod 600 "$config"
|
||||
|
||||
jq -r '"header = \"Authorization: Bearer \(.claudeAiOauth.accessToken)\"\nheader = \"anthropic-beta: oauth-2025-04-20\"\nsilent\nshow-error"' \
|
||||
"$CREDENTIALS" >"$config" 2>/dev/null \
|
||||
|| { emit unavailable "Could not read the Claude Code session."; exit 0; }
|
||||
|
||||
response="$(curl --max-time 10 --config "$config" "$ENDPOINT" 2>/dev/null)" || {
|
||||
emit unavailable "Could not reach the usage service."
|
||||
exit 0
|
||||
}
|
||||
rm -f "$config"
|
||||
|
||||
jq -e . >/dev/null 2>&1 <<<"$response" || { emit unavailable "The usage service returned something unreadable."; exit 0; }
|
||||
if jq -e '.error' >/dev/null 2>&1 <<<"$response"; then
|
||||
emit unavailable "$(jq -r '.error.message // "The usage service refused the request."' <<<"$response")"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Reshaped into a small, stable record rather than passed through, so the
|
||||
# widget does not depend on the shape of an endpoint nobody documents. Every
|
||||
# field is optional: an endpoint that stops reporting one should cost that
|
||||
# number, not the whole indicator.
|
||||
usage="$(jq -c '
|
||||
def pct: if type == "number" then (. * 100 | round) else null end;
|
||||
{
|
||||
tier: (.rate_limit_tier // .rateLimitTier // null),
|
||||
subscription: (.subscription_type // .subscriptionType // null),
|
||||
fiveHour: {
|
||||
used: ((.five_hour.utilization // .fiveHour.utilization // null) | pct),
|
||||
resetsAt: (.five_hour.resets_at // .fiveHour.resetsAt // null)
|
||||
},
|
||||
week: {
|
||||
used: ((.seven_day.utilization // .week.utilization // null) | pct),
|
||||
resetsAt: (.seven_day.resets_at // .week.resetsAt // null)
|
||||
}
|
||||
}' <<<"$response" 2>/dev/null)"
|
||||
|
||||
[[ -n "$usage" ]] || { emit unavailable "The usage service returned an unfamiliar shape."; exit 0; }
|
||||
emit ok "" "$usage"
|
||||
@@ -0,0 +1,92 @@
|
||||
pragma Singleton
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// How much of the Claude subscription this account has used.
|
||||
//
|
||||
// The collector writes one display-ready record and this only ever reads it.
|
||||
// That split is the point: adding a second agent later is a collector, not a
|
||||
// change here or in the widget, and nothing in QML ever sees a credential.
|
||||
//
|
||||
// The record carries its own status, so this can tell the three cases apart:
|
||||
// the collector has never run, it ran and the session was stale, or it has
|
||||
// real numbers. The widget hides for the first two, which is right -- a bar
|
||||
// indicator that says "unknown" is worse than an empty space.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// "ok" | "stale" | "unavailable" | "" (never collected)
|
||||
property string status: ""
|
||||
property string detail: ""
|
||||
|
||||
// Percentages, 0-100, or -1 when the endpoint did not report one.
|
||||
property int fiveHourUsed: -1
|
||||
property int weekUsed: -1
|
||||
property string weekResetsAt: ""
|
||||
property string tier: ""
|
||||
|
||||
readonly property bool available: root.status === "ok"
|
||||
&& (root.fiveHourUsed >= 0 || root.weekUsed >= 0)
|
||||
|
||||
// The number worth showing when there is only room for one: whichever
|
||||
// window is closer to its limit is the one about to interrupt you.
|
||||
readonly property int headline: Math.max(root.fiveHourUsed, root.weekUsed)
|
||||
|
||||
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-agent-usage"
|
||||
readonly property string statePath:
|
||||
(Quickshell.env("XDG_STATE_HOME") || `${Quickshell.env("HOME")}/.local/state`)
|
||||
+ "/panama/agent-usage.json"
|
||||
|
||||
// Minutes, never a repaint. Usage moves slowly and the collector makes a
|
||||
// network call; anything faster would be spending someone's battery to
|
||||
// watch a number that changes a few times an hour.
|
||||
Timer {
|
||||
interval: 5 * 60 * 1000
|
||||
running: Settings.showAgentUsage
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: collect.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: collect
|
||||
command: [root.helperPath]
|
||||
onExited: record.reload()
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: record
|
||||
path: root.statePath
|
||||
printErrors: false
|
||||
watchChanges: true
|
||||
onFileChanged: this.reload()
|
||||
onLoaded: {
|
||||
try {
|
||||
const parsed = JSON.parse(this.text());
|
||||
root.status = String(parsed.status ?? "");
|
||||
root.detail = String(parsed.detail ?? "");
|
||||
const usage = parsed.usage;
|
||||
if (usage) {
|
||||
root.fiveHourUsed = Number.isFinite(usage.fiveHour?.used)
|
||||
? usage.fiveHour.used : -1;
|
||||
root.weekUsed = Number.isFinite(usage.week?.used)
|
||||
? usage.week.used : -1;
|
||||
root.weekResetsAt = String(usage.week?.resetsAt ?? "");
|
||||
root.tier = String(usage.tier ?? "");
|
||||
} else {
|
||||
root.fiveHourUsed = -1;
|
||||
root.weekUsed = -1;
|
||||
}
|
||||
} catch (error) {
|
||||
root.status = "";
|
||||
}
|
||||
}
|
||||
onLoadFailed: root.status = "";
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,6 @@
|
||||
# @vicinae.mode silent
|
||||
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||
# @vicinae.description Open Appearance in Settings.
|
||||
# @vicinae.keywords ["settings", "24-hour time", "show seconds", "show weekday", "processor", "memory", "graphics", "battery", "inner gaps", "outer gaps", "border width", "corner radius", "unfocused window opacity"]
|
||||
# @vicinae.keywords ["settings", "24-hour time", "show seconds", "show weekday", "processor", "memory", "graphics", "battery", "claude usage", "inner gaps", "outer gaps", "border width", "corner radius"]
|
||||
|
||||
exec "$HOME/.config/quickshell/scripts/panama-action" settings-page appearance
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@
|
||||
Do not edit this file. Run `quickshell/scripts/panama-settings-docs`
|
||||
after changing the schema; a contract fails when this copy is stale.
|
||||
|
||||
146 settings across 29 groups. 70 of them are applied to the compositor and confirmed by reading the value back.
|
||||
147 settings across 29 groups. 70 of them are applied to the compositor and confirmed by reading the value back.
|
||||
|
||||
## accessibility
|
||||
|
||||
@@ -317,6 +317,7 @@ Found on **Appearance**.
|
||||
| **Memory**<br>`showMemory` | true | Show memory usage beside the workspace indicator |
|
||||
| **Graphics**<br>`showGpu` | true | Show graphics usage beside the workspace indicator |
|
||||
| **Battery**<br>`showBattery` | true | Show the charge level in the bar, on machines that have a battery |
|
||||
| **Claude usage**<br>`showAgentUsage` | false | Show how much of the Claude subscription has been used, beside the other vitals |
|
||||
| **Vitals refresh**<br>`vitalsIntervalMs` | 2000 ms | How often processor, memory, and graphics usage update. Range 500–10000. |
|
||||
|
||||
## wallpaper
|
||||
|
||||
Executable
+151
@@ -0,0 +1,151 @@
|
||||
#!/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'
|
||||
Reference in New Issue
Block a user