Z Usage Monitor 1.1.0 — family hardening: key in the environment, replay #24

Merged
claude merged 5 commits from oss-oo/z-family-hardening into main 2026-10-28 01:00:52 +00:00
Member

The family update for Z Usage Monitor 1.1.0, part 1: what Claude Usage Monitor 1.0.3 and ChatGPT Usage Monitor 1.1.0 learned, checked against Z's data model first.

Security

  • The Z.ai API key setting reaches the helper in its environment (Z_USAGE_API_KEY) instead of --api-key on argv, where every local process could read it through /proc. The helper takes no key on its command line any more; ZAI_API_KEY, the key file and the ZCode app stay the automatic sources (the settings tooltip now names them in their real order).
  • Both requests refuse redirects (urllib would forward the key to the new host); server error text reaches the popup and Cinnamon's log as one line of at most 160 characters, with the key and credential-shaped words replaced by [redacted].

Robustness

  • Cut-off answers and garbled status lines (http.client.HTTPException, also inside the error-body read), unsendable keys and non-UTF-8 history, key, ZCode config or telemetry files no longer end in a traceback; a one-line catch-all closes main().
  • Reset times only as plausible epoch seconds (ms or s, years 2000–3000); GLib null guards in formatTimestamp, formatLocalDate, formatActivityBucketRange.
  • One history sample per moment (any same-timestamp sample is replaced); the history write uses the C JSON encoder; state files are atomic and 0600.
  • The menu signature covers the error line, the sign-in state, the replay flag and the whole history, so an open popup follows them at once.

Replay: a failing monitor API replays the last good snapshot of the same key (last-snapshot.json, 0600, truncated SHA-256 fingerprint, two hours = twice the longest interval, windows past their reset dropped, deleted when that key is rejected, no history sample). The applet adopts a replay only into an empty applet or over a held replay, keeps the error line, skips the green confirmation, ends a held replay on a plain failure, refreshes at the replay's expiry and compares the first fresh snapshot against the first replay (weekly resets, notifications).

Deliberately not ported

  • Request spacing and the answer cache (claude 1.0.1): the live history shows 10,000 samples at one refresh per minute over 7 days with 2 single misses and no logged failure; Claude's endpoint answered 429 at that pace, Z's does not.
  • Claude's trailing-zero percent trim: Z's percentages are exact credit fractions, so the owner's rule stays — x.xx% below 10%, whole percents from 10%, a bare 0% (now pinned by tests).
  • ChatGPT's banked-reset notification: Z's API has no banked resets (live payload: level and CREDIT_LIMIT windows only).

Validation

  • make verify green (83 Python tests, 13 cjs suites, translations, icons, shellcheck, social preview); all suites green under clock shifts of 0, ±30 days, +1 and +5 years.
  • The new tests fail on main (GLib null guards: dateTime is null; transport, replay and launcher tests by construction).
  • Real Gio.SubprocessLauncher probe: the helper's /proc cmdline during an open request carries no key; the fixture received it as the Authorization header.
  • History write on a copy of the live 10,000-sample history: 112 → 25 ms, byte-identical.
  • An audit hook over the whole Python suite records no open of a real ZCode, key, state or applet-settings path.

README screenshots are refreshed in the release PR (they date from 1.0.6; z's CI does not check them).

🤖 Generated with Claude Code

The family update for Z Usage Monitor 1.1.0, part 1: what Claude Usage Monitor 1.0.3 and ChatGPT Usage Monitor 1.1.0 learned, checked against Z's data model first. **Security** - The _Z.ai API key_ setting reaches the helper in its environment (`Z_USAGE_API_KEY`) instead of `--api-key` on argv, where every local process could read it through `/proc`. The helper takes no key on its command line any more; `ZAI_API_KEY`, the key file and the ZCode app stay the automatic sources (the settings tooltip now names them in their real order). - Both requests refuse redirects (urllib would forward the key to the new host); server error text reaches the popup and Cinnamon's log as one line of at most 160 characters, with the key and credential-shaped words replaced by `[redacted]`. **Robustness** - Cut-off answers and garbled status lines (`http.client.HTTPException`, also inside the error-body read), unsendable keys and non-UTF-8 history, key, ZCode config or telemetry files no longer end in a traceback; a one-line catch-all closes `main()`. - Reset times only as plausible epoch seconds (ms or s, years 2000–3000); GLib null guards in `formatTimestamp`, `formatLocalDate`, `formatActivityBucketRange`. - One history sample per moment (any same-timestamp sample is replaced); the history write uses the C JSON encoder; state files are atomic and `0600`. - The menu signature covers the error line, the sign-in state, the replay flag and the whole history, so an open popup follows them at once. **Replay**: a failing monitor API replays the last good snapshot of the same key (`last-snapshot.json`, 0600, truncated SHA-256 fingerprint, two hours = twice the longest interval, windows past their reset dropped, deleted when that key is rejected, no history sample). The applet adopts a replay only into an empty applet or over a held replay, keeps the error line, skips the green confirmation, ends a held replay on a plain failure, refreshes at the replay's expiry and compares the first fresh snapshot against the first replay (weekly resets, notifications). **Deliberately not ported** - Request spacing and the answer cache (claude 1.0.1): the live history shows 10,000 samples at one refresh per minute over 7 days with 2 single misses and no logged failure; Claude's endpoint answered 429 at that pace, Z's does not. - Claude's trailing-zero percent trim: Z's percentages are exact credit fractions, so the owner's rule stays — `x.xx%` below 10%, whole percents from 10%, a bare `0%` (now pinned by tests). - ChatGPT's banked-reset notification: Z's API has no banked resets (live payload: `level` and `CREDIT_LIMIT` windows only). **Validation** - `make verify` green (83 Python tests, 13 cjs suites, translations, icons, shellcheck, social preview); all suites green under clock shifts of 0, ±30 days, +1 and +5 years. - The new tests fail on `main` (GLib null guards: `dateTime is null`; transport, replay and launcher tests by construction). - Real `Gio.SubprocessLauncher` probe: the helper's `/proc` cmdline during an open request carries no key; the fixture received it as the `Authorization` header. - History write on a copy of the live 10,000-sample history: 112 → 25 ms, byte-identical. - An audit hook over the whole Python suite records no open of a real ZCode, key, state or applet-settings path. README screenshots are refreshed in the release PR (they date from 1.0.6; z's CI does not check them). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude agent added 1 commit 2026-10-28 00:09:04 +00:00
Z Usage Monitor 1.1.0 — family hardening: key in the environment, replay
Some checks failed
Checks / Functional checks (pull_request) Has been cancelled
62176c94a8
The family update from the sibling applets, checked against Z's data
model (measured: no rate limit at one refresh per minute; the monitor
API reports only CREDIT_LIMIT windows).

- The API key setting reaches the helper as Z_USAGE_API_KEY in its
  environment instead of --api-key on argv (readable through /proc);
  the helper takes no key on its command line any more.
- Both requests refuse redirects (urllib would forward the key) and
  redact server text to one short line; cut-off answers
  (http.client.HTTPException), unsendable keys and non-UTF-8 state,
  key or ZCode files no longer end in a traceback; a one-line
  catch-all ends main().
- A failing monitor API replays the last good snapshot of the same key
  (last-snapshot.json, 0600, truncated fingerprint, two hours, expired
  windows dropped, deleted on a rejected key, no history sample). The
  applet adopts a replay only into an empty applet or over a held
  replay, keeps the error line, skips the confirmation, ends a held
  replay on a plain failure, refreshes at the replay's expiry and
  compares the first fresh snapshot against the first replay.
- Plausible epoch timestamps only (ms or s, 2000..3000); GLib null
  guards in the date formatters; one history sample per moment; the
  history write uses the C JSON encoder (112 -> 25 ms on 10,000
  samples, identical bytes); state files atomic with mode 0600.
- The menu signature covers the error line, the sign-in state, the
  replay flag and the whole history.
- Tests: private test home (HOME, XDG dirs, ZCODE_HOME, key variables)
  with a guard on every deletion; transport, redaction, replay and
  launcher tests; Z's percent rule pinned (x.xx% below 10%, whole
  percents from 10%, bare 0%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test the replay expiry tick, implausible expiries and the history signature
All checks were successful
Checks / Functional checks (pull_request) Successful in 35s
ce127a757d
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1: steady history signature, replay balance, key change
All checks were successful
Checks / Functional checks (pull_request) Successful in 39s
852a399df3
- The menu signature signs the history without its moving tracking
  start (a full history drops its oldest sample every refresh; the start
  only renders inside the 24-hour charts, so it is clamped there).
- A replay recomputes the credit balance from the monitor windows it
  keeps, so a weekly window whose reset passed leaves no stale balance.
- Error bodies are read up to 64 KiB and redacted whole before the cut;
  a token cut at that bound is dropped, and the Z.ai key pattern also
  matches a key whose secret part was cut. The catch-all line redacts
  the resolved key and plan token too.
- A changed API key empties the applet before its refresh (no old
  numbers under a new key, no comparison against them); the same key
  only refreshes.
- A malformed ZCode plan answer drops only the plan sections; the helper
  stops after twice its timeout plus five seconds (SIGALRM), and main()
  clears that alarm when it returns.
- Tests: the replay-rewrite trap, a plain-text multi-line error body,
  the body bound, cut keys, the replay balance, the deadline, malformed
  plan answers, the steady signature and the key change; module-path
  runs (python3 -m unittest tests.test_x) import the private home again.
  SECURITY.md and CHANGELOG.md follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2: linear trim, key change mid-refresh, unknown balance
All checks were successful
Checks / Functional checks (pull_request) Successful in 40s
0266778072
- The partial-word trim at the error-body bound runs in linear time (a
  trailing \S* search was quadratic: 13 s for a 67 KB minified body).
- A helper answer launched with another key is dropped (another account,
  perhaps); the refresh queued by the change asks with the new key.
- No known balance reads "unavailable", never "0" (a replay without
  account windows, or a snapshot without credit data).
- The deadline is armed only for finite timeouts and capped; an
  in-process caller gets its alarm and SIGALRM handler back.
- Equally long windows keep the old tie order for the balance.
- Tests: a garbled chunked error body (HTTPException in the bounded
  read), a non-key-shaped key at the 64 KiB bound, the trim's speed,
  absurd timeouts, the caller's alarm, the tie order, the key change
  mid-refresh and the unknown balance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 3: a speed test that catches the slow trim; SIGTERM restored
All checks were successful
Checks / Functional checks (pull_request) Successful in 39s
08a345ce0f
The trim speed test now feeds long tokens followed by more text, the slow
case of a trailing \S* search (it fails on the old regex). main() also
restores an in-process caller's SIGTERM handler, like its alarm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent merged commit 5d6c609338 into main 2026-10-28 01:00:52 +00:00
claude agent deleted branch oss-oo/z-family-hardening 2026-10-28 01:00:52 +00:00
ClaudiuSchuster referenced this pull request from a commit 2026-10-28 01:35:58 +00:00
claude agent referenced this pull request from a commit 2026-10-28 01:54:07 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
OSS-Singularity/cinnamon-z-usage!24
No description provided.