0.51.1 — round four, second half - #151
Merged
Merged
Conversation
… 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
force-pushed
the
fix/audit-r4b
branch
from
September 15, 2026 21:46
76270a0 to
38c5109
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
path-safety: the classifier now strips trailing C0/space and reads\as/like the URL parser —phone/numbers/buyreserved $0.012 for the $5.001 route;sandbox\createpriced as $0.003 for a $192 sandbox.publishMintedKey's placeholder claim adopts a peer's key instead of renaming it aside and deleting it.secret-toolbut no secrets service can mint again (0.51.0 regression).mainonly; release step repairable by re-run; registry none/unknown split; brand-sync refreshes an open PR;verify:pricesbounds probes and tallies throws as unreachable;version-gaterealpathsisMain.thinkingalone.Full notes:
CHANGELOG.md§ 0.51.1.Test plan
npm run typechecknpm test— 1312/1312npm run buildnpm run verify:prices— exit 0tools/list= 19 toolsv0.51.1, GitHub release; coldnpx🤖 Generated with Claude Code