Skip to content

0.51.1 — round four, second half - #151

Merged
VickyXAI merged 10 commits into
mainfrom
fix/audit-r4b
Sep 15, 2026
Merged

VickyXAI merged 10 commits into
mainfrom
fix/audit-r4b

Conversation

@VickyXAI

Copy link
Copy Markdown
Contributor

Summary

The seven audit-round-4 finders that stalled before 0.51.0 shipped, re-run scoped, plus a regression hunt over 0.51.0's own fixes: 46 findings confirmed by two adversarial verifiers each (2 P0, 5 P1 — three of the P1s introduced by 0.51.0), all fixed with pinning tests.

  • P0 path-safety: the classifier now strips trailing C0/space and reads \ as / like the URL parser — phone/numbers/buy reserved $0.012 for the $5.001 route; sandbox\create priced as $0.003 for a $192 sandbox.
  • P1 path tools use a per-call client (0.51.0's settled-evidence counter was the shared singleton's, so a concurrent call's settlement was booked to a failing call, twice).
  • P1 an edge 408/502/504/52x is a maybe on every rail (0.51.0 read every status as a verdict — a speech 504 from the edge while TTS billed read "try again").
  • P1 publishMintedKey's placeholder claim adopts a peer's key instead of renaming it aside and deleting it.
  • P1 native claude-* path: never-connected errors nested two causes deep book nothing; stall-before-connect is a maybe; partial text kept; Solana frame path refuses an empty stream.
  • Polymarket: sign/post split (only the POST can be unknown), creds retry never re-submits an unknown outcome, reservation before the spend dialog, EOA withdraw double-send guard, relayer poll throw = may still land, unreadable state file = refusal, definite refusals say nothing was placed.
  • Linux: a host with secret-tool but no secrets service can mint again (0.51.0 regression).
  • CI: publish on main only; release step repairable by re-run; registry none/unknown split; brand-sync refreshes an open PR; verify:prices bounds probes and tallies throws as unreachable; version-gate realpaths isMain.
  • Docs: which rails bill at submit, both session files to back up, spend gate covers Polymarket, claude-* Base-only for thinking alone.

Full notes: CHANGELOG.md § 0.51.1.

Test plan

  • npm run typecheck
  • npm test — 1312/1312
  • npm run build
  • npm run verify:prices — exit 0
  • stdio smoke: tools/list = 19 tools
  • after merge: watch the publish run; verify npm, MCP registry, v0.51.1, GitHub release; cold npx

🤖 Generated with Claude Code

VickyXAI and others added 10 commits September 15, 2026 17:46
… a literal backslash as a slash, as the URL parser does

Two P0s of the same class as the tab-in-escape hole. The WHATWG parser strips
trailing C0-control-or-space from the whole URL input — and the slug is the
tail of `${base}${endpoint}` on every rail — so `phone/numbers/buy ` (an
ordinary tokenisation slip) left the machine as /v1/phone/numbers/buy and the
gateway served the $5.001 route while the exact-match price row missed it and
$0.012 was reserved. And for https the parser treats a literal `\` as `/`, so
`sandbox\create` classified as the $0.003 op, skipped the handler's gpu/timeout
normalisation, and the gateway served a sandbox/create of up to $192. Both
guards share the cut, and a `..` last segment with trailing space is traversal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he account rail's unreadable settled body is a charge that stands

Round 4's RawCallSettledError read the SDK's spend counter around the call,
but the counter is per CLIENT, getClient() is a cached singleton per rail, and
tool calls run concurrently — so a concurrent call's settlement landed inside
a failing call's before/after window and was booked to it as "the charge
stands" (and booked again by the call that paid). Every path tool now builds
its own client, as blockrun_chat always has.

The account rail returned a settled 2xx whose body would not parse as a
SUCCESS with data `{}`; it now throws the same typed error, carrying the cost
header, and the catch says the charge stands. RawCallSettledError moves to a
leaf module so both helpers can throw it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…alface say when the charge stands

Round 4 read every numeric status as the gateway's verdict, which undid — for
the media tools alone — the rule chat and the path tools apply to the same
status on the same rails: 408/502/504/52x say only that the origin did not
answer in time, and it may still be running the request and settling it. A
blockrun_speech answered 504 by the edge while TTS finished and billed at the
origin read "failed — try again", i.e. pay twice, where blockrun_exa booked
the same status as a precaution. ORIGIN_DID_NOT_ANSWER is one shared set now;
the tracker remembers that a paid request LEFT even after a response settled
it, and the account helper classifies an edge status on its submit as an
unknown billing. The rail-parity cell splits into gateway-500 (an answer) and
edge-504 (booked as a precaution). realface's enroll throws carry the status.

image and realface get the charge-stands step video, music and speech have:
a settled 2xx with no URL / no asset id, or a temp file that would not write,
says the charge stands instead of "failed" with alt-model advice that runs a
second paid render.

The music and video descriptions say which rails bill at submit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ybe, a stall before connect is not settled, and the native path keeps partial text

The Anthropic SDK wraps undici's TypeError once more (APIConnectionError →
cause TypeError("fetch failed") → cause { code: ENOTFOUND }), and the
classifier read cause.code one level down: a DNS failure on the native path
booked the reconstructed quote and forbade a retry for a request that never
left the machine, while the same outage on the compat path read "none". The
cause chain is walked (bounded), and UND_ERR_CONNECT_TIMEOUT — undici giving
up before a TCP connection existed, the shape of the Solana blockhash RPC
dying inside the SDK's signing step — is never-connected too.

An idle stall BEFORE the native stream connected is a maybe, like the compat
Solana path's own "before the first frame" stall, not "the charge stands";
partial text streamed before a post-acceptance failure rides along on the
native path as it does on the compat one; and the Solana frame path refuses a
stream that ended with no frames (a route that ignored stream:true) instead of
resolving a paid, successful, empty reply.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eting it, and a missing secrets service is absent, not a fault

Round 4's placeholder claim renamed whatever was at the session file's NAME
after a read that saw it empty — so a peer that claimed the same placeholder
and linked its key in the gap had that key renamed aside and deleted, and
signed with a key that was on disk nowhere. The aside is inspected: a key
found there is linked back under the exclusive name and adopted; only an
empty one is discarded, and the loop never falls through to a plain write.

Round 4's Linux fix read every secret-tool stderr as a fault, and a fresh
install on a host with secret-tool but no org.freedesktop.secrets service
(SSH sessions, containers, Fedora/Arch base installs) could never mint a
wallet — every paid tool refused with "your existing one is most likely still
in the keychain". A keychain that does not exist cannot hold a funded key:
D-Bus / no-provider messages read as absent; a locked collection or a
dismissed prompt is still an error.

getBaseUsdcBalance honours BASE_RPC_URL, as the SDK's own getBalance and the
Solana read's SOLANA_RPC_* siblings do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…EOA withdraw gets the double-send guard, and a definite refusal says so

- The SDK's createAndPost* helpers read the network BEFORE they sign (GET
  /version, the market's condition id and tick size, builder fees), and a
  relay 502 or a dropped socket on any of those was wrapped as
  outcome-unknown — a phantom "possibly live" order that never released, with
  a "do NOT re-place" instruction, when nothing had been signed. The tool now
  signs and posts separately; only the POST can be unknown.
- withCredsRetry re-ran the whole trade on any error whose text matched the
  creds-mismatch phrases, and an unknown outcome carries raw transport text
  that can say "unauthorized" — a second signed submit on top of a
  possibly-live order. It never retries an unknown outcome.
- The session-cap check and the reservation were split by the awaited spend
  dialog, so two confirms waiting on it together could overshoot
  POLYMARKET_MAX_SESSION_USD; the reservation is taken before the dialog and
  released on a decline.
- The EOA (sigType 0) withdraw is a plain Polygon transaction with no
  pendingWithdraw guard — a receipt timeout after the broadcast invited a
  second full transfer with a fresh nonce. It records "eoa" before the send,
  "eoa:<hash>" once known, and clears only on a receipt; the guard checks a
  receipt, not the relayer, for it.
- A throw from the relayer's wait() (a transport failure mid-poll) is the
  same "may still land" case as wait() resolving undefined, not a bare
  `{"error":"connection error"}` with no anti-retry wording.
- A state file that exists but cannot be read is a refusal, not an empty
  state: loadState() answered {} for a half-written JSON, which let a second
  authorization be signed inside the guard window and the next save
  overwrote the evidence.
- Every definite refusal from executeTrade ends with the repo's uncharged
  wording and structured outcome:"rejected", so the order card re-arms on it
  instead of locking as "outcome UNKNOWN" for an order the server never
  placed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rable, and a probe that throws is an unreachable row

- publish.yml's workflow_dispatch accepted any ref and every guard was
  ref-agnostic, so a bumped feature branch could have shipped as `latest`,
  tagged at an unmerged commit. The job is gated on main.
- The tag/release step was gated on the tag being ABSENT, so a re-run after
  `gh release create` failed skipped the whole step — the in-step
  `gh release view` guard that makes re-runs safe could never run. Both
  existence checks live in-step; the outer gate is build + npm only.
- The MCP-registry lookup gets the same none/unknown split as npm: a non-2xx
  or an unparsable body is "unknown" and stops the job before mcp-publisher
  runs blind.
- brand-sync: a fallback PR a human already opened is refreshed by the
  force-push and reported green, not "PR creation is unavailable".
- verify:prices: a probe that THROWS (reset, DNS, a gateway that never
  answers) was an uncaught rejection — Node exited 1, the "confirmed
  under-reserve" code, with no verdict printed. It is an unreachable row now,
  bounded by a 30s timeout; an expectRefused row that answers 5xx is
  unreachable too, not "the gateway sells it".
- version-gate.mjs realpaths both sides of its isMain check, as the sibling
  scripts do: through a symlinked checkout it exited 0 on a downgrade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ly; recordSpending credits the tombstone since round 4

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…covers Polymarket, and claude-* is Base-only for `thinking` alone

Ten stale claims rounds 3-4 left behind: "timeout = no charge, retry" was
wrong on Solana music and every account-rail media job; the setup and debug
skills told the user to back up only ~/.blockrun/.session when the funded key
on a new install is .solana-session; spend-confirmation.md said
blockrun_polymarket is not behind the dialog; the README, setup and debug
docs said native claude-* chat is Base-only when only `thinking` is refused
on Solana; the gentech skill said music, speech and realface need Base; the
README's config table said .solana-session is "not created" and that a file's
existence picks the chain; CONTRIBUTING's async pattern said timeouts never
charge and called music the synchronous variant. Schema figures re-measured
after the two description edits (13,044 full, trading 59% less).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@VickyXAI
VickyXAI merged commit c39cc81 into main Sep 15, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant