Skip to content

v0.5.1 — the cold wipe works

Latest

Choose a tag to compare

@CyberGitJul CyberGitJul released this 30 Jul 07:28
· 41 commits to main since this release
775b0da

Patch release. One bug, found live against a real Gramps Web instance right after
v0.5.0 shipped: gramps_delete_all_objects failed on a cold client, which is
the normal case — a stdio MCP server is spawned per session.

The bug

Gramps Web rate-limits POST /api/token/ to one request per second per source IP
(@limiter.limit("1/second")). The wipe minted two tokens with only a single
metadata GET between them: the lazy login behind the precondition read, then an
unconditional fresh login for the endpoint's FreshProtectedResource. The second mint
came back 429 TOO MANY REQUESTS and the wipe died before the delete POST went out.

The tree was never touched by this failure — the 429 was raised from _login()'s
raise_for_status(), outside the try that guards the destructive POST. Verified
twice: by code order, and by a wire log.

The fix

  • The fresh login before the POST is now conditional on this call not having minted
    a token already. fresh is a boolean JWT claim, not a time window, so a token minted
    milliseconds earlier by the precondition read is already exactly what a second login
    would produce. On a warm session the login still happens and still earns its mint:
    Gramps Web checks is_tree_disabled only at login, never per request, so it is the
    last point at which the deployment can refuse.
  • _login rides out a single 429 and retries once, then raises the new
    TokenRateLimitError. The limiter is keyed by IP, so a second MCP session or a
    browser tab behind the same address can take the budget for the second this wipe needs
    it — dropping our own second mint does not help there, only waiting does. Non-429
    statuses still fail immediately and unretried: a 403 means the credentials are wrong,
    and re-authenticating in a loop is what a brute-force guard counts.
  • TokenRateLimitError subclasses requests.HTTPError, so the existing 502/503/504
    fallthrough and every other HTTPError handler keep working. Its message scopes its
    claim to the login itself rather than asserting nothing happened — it can surface
    during the post-delete counts poll, where the wipe has already landed.
  • _delete_orphaned_notes no longer amplifies the retry. It swallows a per-note
    transport error and carries on, which is right for a hiccup and wrong for a
    client-wide rate limit: 20 orphaned notes meant 40 token POSTs and 22 s of blocking
    sleep on the single-threaded stdio server, to fail anyway. It now breaks out on
    TokenRateLimitError and reports what it managed to delete.

Verification

  • 259 tests (243 → 259), 13 of the first 15 red before the fix — including one that
    pinned the literal two-mint order (['login','request','login']). Mutation probe:
    11 mutations, all red.
  • Adversarial review, 23 agents across 5 lenses: 17 findings, 16 refuted, 3 fixed here
    (the retry wait was pinned by no test and TOKEN_RETRY_WAIT = 0.0 left the suite
    green; the orphan-note amplification above; a TokenRateLimitError message that
    claimed a retry had happened in the branch where none does).
  • Proven live against a real instance: gramps_delete_all_objects as the very
    first call
    of a fresh session, no warm-up — 391 objects → 0 in 7.7 s, no 429; then
    gramps_import_file restored all 391 with identical handles and change timestamps.
    Exactly the case that previously failed reproducibly. The instance uses
    GRAMPSWEB_RATELIMIT_STORAGE_URI (Redis-backed limiter), so the test ran against the
    strict configuration, not a lucky per-worker one.

No tool signatures changed; the tool count is unchanged at 27 + 4 destructive = 31.