release: v0.14.0-rc2 - #809
Open
bussyjd wants to merge 51 commits into
Open
Conversation
The upstream ethereum-node chart's reth subchart defaults to a tcp liveness probe on p2p (30303) with failureThreshold 3 (~12 min grace). Reth keeps that port closed while running post-crash check_consistency recovery, which can take far longer on a large datadir — so the kubelet SIGKILLs it mid-recovery, each kill adds more drift to heal on the next start, and the node crash-loops permanently (observed: 637 restarts, ~129k blocks of StoragesHistory drift). Override the probe with failureThreshold 10000 so it effectively never fires; a genuinely dead reth exits the container on its own. Scoped to reth only — geth/nethermind/besu probe http-rpc and erigon probes metrics, none of which close during recovery. Claude-Session: https://claude.ai/code/session_01VLQSsnH9WdAsnVYGTd2omr
buildResourceURL keyed the scheme off X-Forwarded-Proto alone, so behind a TLS-terminating tunnel (edge https -> plaintext to Traefik -> verifier) any route without an explicit X-Forwarded-Proto:https RequestHeaderModifier produced http:// resource URLs in 402 challenges. The controller's host-bound so-<name>-host routes carry that filter but the shared-origin so-<name> routes do not — and being more path-specific for /services/<name>, they win the match even on dedicated-hostname origins, so challenges on those origins advertised http:// resources on an https endpoint (strict v2 payment clients and discovery crawlers see a scheme mismatch; observed live on 5 of 8 host-bound offers, #679). resolveSiteURL already solved exactly this for 402-page links: default https, downgrade only for hosts the stack serves locally over plain HTTP (obol.stack, loopback, *.localhost/*.local), explicit signals always win. Extract that resolution into resolveScheme and use it in both places, so challenge resource URLs and page links can never disagree again. Refs #679 Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
Reported by a teammate during v0.14.0-rc0 field testing. https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
Reported by a teammate during v0.14.0-rc0 field testing. https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
… openapi for inference/agent Reported by a teammate during v0.14.0-rc0 field testing. https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
Reported by a teammate during v0.14.0-rc0 field testing. https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
Reported by a teammate during v0.14.0-rc0 field testing. https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
Inline first-message fee capture for gate:auth offers: the SIWX session is minted by settling an x402 auth-capture charge (EIP-3009 single-shot, autoCapture) instead of a plain signature; subsequent requests ride the session cookie free. The escrow enforces the client-signed fee split (feeBps -> feeRecipient) on-chain at charge() time. - internal/x402/authcapture.go: auth-capture requirement builder + signed payload validation (validateSignedUnlockRequirement pins every economic field of the client-signed requirement against config) - internal/x402/unlockgate.go: unlock flow — 402 (x402Version 2) -> verify+settle against the signed payload -> mint SIWX cookie - internal/x402/metrics.go: obol_x402_verifier_fee_revenue_atomic_total + settled_volume_atomic_total (network/asset/fee_recipient), excluded from route-pruning - config: global authCaptureUnlock block (offerPrefix, price, payTo, feeRecipient, min/maxFeeBps, captureAuthorizer) Config-gated, off by default. Settlement must use the client's signed 'accepted' requirement verbatim (PaymentInfo hash commits server-issued deadlines; rebuilding drifts them and breaks the signature). Proven end-to-end on Base Sepolia and Base mainnet (on-chain fee split, cookie mint, free-ride, metrics materialization). Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
… integration/v0.14.0-rc1
…-api-1.x' into integration/v0.14.0-rc1
… into integration/v0.14.0-rc1
… into integration/v0.14.0-rc1
…' into integration/v0.14.0-rc1
…' into integration/v0.14.0-rc1
…into integration/v0.14.0-rc1
…to integration/v0.14.0-rc1
…dates' into integration/v0.14.0-rc1
…integration/v0.14.0-rc1
…dates' into integration/v0.14.0-rc1
…tegration/v0.14.0-rc1
…nto integration/v0.14.0-rc1
An Obol Agent (type=agent) runs its own model, skills, and memory — the buyer
never selects one, and Hermes ignores the chat-completions `model` field
(resolved from its own config). Surfacing the underlying model in the 402 page
is noise and exposes an implementation detail, and pay-agent's required
`--model` flag had no effect on agent calls.
- paymentrequired.go agentCopy: remove '(running <model>)', the '"model":' line
in the example body, and '--model <id>' from the pay-agent example. The agent
copy is now model-free.
- buy-x402 buy.py: pay-agent no longer accepts/sends --model (synthesised body
is just {messages, stream}); usage strings updated.
- buy-x402 SKILL.md: pay-agent documented without --model.
go build + x402/buyer/embed tests green.
Claude-Session: https://claude.ai/code/session_01XgUndZjSoxr2jNNGG5sVYD
(cherry picked from commit e605f55)
mergeAgentExtras no longer adds extra.agentModel — an Obol Agent runs its own model and the buyer never selects one, so the model id is an internal detail, not buyer-facing info (it also rendered in the HTML 402 page's raw-JSON card). agentSkills/agentRuntime still surface so clients can tell it's an agent. Claude-Session: https://claude.ai/code/session_01XgUndZjSoxr2jNNGG5sVYD (cherry picked from commit 92716d6)
…t offers For agent offers the buyer never selects a model — the agent runs its own and ignores the chat-completions `model` field — so the bazaar discovery example now seeds the neutral 'your-model-id' placeholder instead of the real upstream id (which also rendered in the HTML 402 page's embedded raw-JSON card). Inference offers are unchanged: there the model IS buyer-selectable, so the real id stays. Claude-Session: https://claude.ai/code/session_01XgUndZjSoxr2jNNGG5sVYD (cherry picked from commit 0f86017)
…for agent offers The /skill.md catalog (and its Service Details section) showed the agent's underlying model in the Model column / **Model** bullet. An agent runs its own model and ignores the request `model` field, so the id is an internal detail — agent rows now render '—' and omit the **Model** bullet. Inference offers keep their model (there the buyer selects it). Mirrors the 402 page/extra/bazaar model-strip in internal/x402. The /api/services.json feed still resolves the agent model on purpose (drives the storefront UI) — left untouched. Claude-Session: https://claude.ai/code/session_01XgUndZjSoxr2jNNGG5sVYD (cherry picked from commit 2a88c3f)
…json for agent offers The catalog JSON builder set the entry model from spec.model / the resolved agent model, bypassing catalogModelName -- so /api/services.json still leaked the internal model id for type=agent offers even though skill.md, the 402 page/extra, and the bazaar example already strip it (#673). Route the JSON builder through catalogModelName so all discovery surfaces agree: agent offers omit the model (they run their own and ignore the request `model`), inference offers keep it. Invert the test that pinned the old leaky behaviour. Also folds in pre-existing #673 housekeeping that was still uncommitted: - buy-x402 SKILL.md + buy.py: drop stale --model residue - agentcrd contract test: fix stale lifetime_seconds 90 -> 180 drift Claude-Session: https://claude.ai/code/session_01XgUndZjSoxr2jNNGG5sVYD (cherry picked from commit f8c0d8e)
…mojibake The skill-catalog busybox httpd served .md/.html with a bare text/* Content-Type (no charset), so clients fell back to Latin-1/CP1252 and rendered UTF-8 em dashes (the catalog's '—' placeholders, accented operator descriptions) as '—'. Add charset=utf-8 to the text MIME mappings. JSON stays clean (always UTF-8 per RFC 8259). Claude-Session: https://claude.ai/code/session_01XgUndZjSoxr2jNNGG5sVYD (cherry picked from commit 0229b8f)
This reverts commit 78c78a8.
handlePaidUnlock discarded the settle response when facilitatorSettle errored, losing the tx hash the facilitator returns when it submits the settle tx on-chain and then fails on the receipt path. A charged buyer got a bare settle_failed with no cookie, no answer, and no way to reconcile the on-chain debit. Mirror the per-request paid path (HandleProxy): surface settleResp.Transaction via X-PAYMENT-RESPONSE + a 'you may pay twice' hint, and log it, on both the transport-error and !Success branches. A failed settle still mints no session. Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
…-EOA gap Two review fixes on the session-wallet widget: - Per-turn payment policy capped at the price shown at page load (bounded by the session balance), not the whole balance. A seller that drifts the price up mid-session can no longer silently drain a funded session in one message; an over-price turn is refused with an honest error, and a missing/zero amount is dropped rather than treated as free. A legit price change is picked up on reload. - Document the MPC/threshold-ECDSA residual: such wallets are code-less EOAs that pass the contract-wallet guard but sign non-deterministically, stranding session funds. No on-chain detection without a second signature popup this flow avoids; noted at the guard and in assets/README.md limitations. Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
…tegration/v0.14.0-rc1
Fold the agent chat widget into rc1 for the v0.14.0-rc1 pre-release. Widget files (chat.html, chat-vendor.js, chatwidget.go, offerbundle.go) are identical to the deploy/rc1-fixes tree validated live on silvernuc3 (serviceoffer-controller:rc1fix-c2dd5df). render.go merges the widget's /chat route + CSP with rc1's UTF-8 + agent model-strip changes. Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
Render session-only branding drafts through the real public storefront so operators can preview safely without duplicating buyer UI. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep operator live previews on storefront-preview.obol.stack (never the public tunnel), retain that renderer when the public catch-all is torn down, and use server-side apply so large inline logos survive kubectl's 256 KiB last-applied annotation limit.
Publish AgentCash/x402scan discovery fallback on the shared storefront, surface AgentCash/Bankr buy prompts, and gate the path with flow-22 (generic x402 SDK + agent chat-completions) in release-smoke. Co-authored-by: Cursor <cursoragent@cursor.com>
Feat/storefront live preview
Applied directly rather than merging the PR branches: all four are cut from main while this branch carries newer intermediate versions from rc1's own renovate merges, so every one of them conflicts on every file it touches. Hand-resolving those conflicts is how a dependency silently gets downgraded, so the target versions are applied in one commit instead — the same shape renovate would emit if it rebased onto rc2. obolup.sh (#797): - kubectl 1.36.2 -> 1.36.3, helmfile 1.7.1 -> 1.7.2, helm-diff 3.15.10 -> 3.15.11, ollama 0.32.1 -> 0.32.5 ethereum clients (#796): - geth v1.17.4 -> v1.17.5, nethermind 1.39.1 -> 1.39.2, besu 26.7.0 -> 26.7.1, erigon v3.5.2 -> v3.5.4, lighthouse v8.2.0 -> v8.2.1, prysm v7.1.7 -> v7.1.8, lodestar v1.44.0 -> v1.45.0 (reth and teku were already at target; nimbus tracks a different renovate datasource and had no PR, so it is left alone) scalar (#795): - @scalar/api-reference 1.62.9 -> 1.64.0 with its matching SRI hash, which must move in lockstep or the browser blocks the script cloudflared (#794): - 2026.7.2 -> 2026.7.3 with its matching image digest
Multi-arch index digest (linux/amd64 + linux/arm64) for the front-end v0.1.28-rc5 release, which carries the /storefront branding editor that this RC's live-preview renderer serves. rc5 rather than rc4 because a second dependency bundle landed on the front-end main after rc4 was tagged; Docker Hub enforces tag immutability on that repo, so republishing rc4 was not an option.
Skip settle on client disconnect/write errors, classify facilitator rejections, and teach HTTP buyers Bankr chat auto-pay while agent/inference use wallet-sign plus long curl after live timeout and voucher failures. Co-authored-by: Cursor <cursoragent@cursor.com>
Publish type-specific Poncho chat prompts alongside AgentCash/Bankr and surface them in the storefront pill selector after a successful live pay test. Co-authored-by: Cursor <cursoragent@cursor.com>
Record that a Bankr App with pay:x402 + allowedHosts still hit rpc timeout on Base mainnet agent offers; drop the Sepolia digression and clarify HandleProxy settle-skip vs ForwardAuth verify-only. Co-authored-by: Cursor <cursoragent@cursor.com>
…njection) CodeQL flagged four new go/log-injection alerts introduced by this PR in internal/x402/forwardauth.go: the buyer's payment payload and the facilitator's reject reason reach log.Printf unescaped, so a crafted value containing CRLF can forge additional lines in the operator's log (e.g. a fake "payment settled successfully" entry). Fix at the producers rather than at each log call, so future call sites inherit it: paymentPayloadSummary, facilitatorRejectDetail and the normalize note now return CR/LF-stripped strings, and truncateForLog sanitizes too (it carries the raw facilitator /verify body). Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
…-rc2
Brings the aggregate /.well-known/x402 + external buyer-tool compat work
along with its CodeQL go/log-injection fix.
Conflict resolutions (both additive — kept BOTH sides, not one over the
other):
* catalog.go — the static-site match and content hash now fold in both
the widget's embedded chat-vendor.js (rc2) and the new x402.json
(#806). Dropping either side would silently pin a stale copy of that
asset across controller upgrades via the skip-when-unchanged path.
* forwardauth_test.go — kept rc2's TestBuildResourceURL_Scheme
alongside #806's brokenPipeWriter settle-skip tests.
Also updates four widget-era test call sites in catalog_test.go and
hostoffer_test.go for #806's new wellKnownX402JSON parameter (they pass
"" — those tests do not exercise the aggregate x402 document). Without
this the package does not build its tests.
Replaces the unsigned a128bdf/dd9c5a1d/cdee3a22 with a single signed
merge; the resulting tree is byte-identical to cdee3a2.
Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
bussyjd
force-pushed
the
integration/v0.14.0-rc2
branch
from
August 6, 2026 12:51
cdee3a2 to
b874a19
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
Release integration branch for v0.14.0-rc2. Tag
v0.14.0-rc2points at the tip of this branch (cdee3a22).Everything on
mainis already contained here (git log main --not integration/v0.14.0-rc2is empty), so this merges forward cleanly with no risk of reverting main.What rc2 adds on top of main
Folded-in work, by source branch:
feat/chat-widget-relandfeat/authcapture-unlockfix/x402-challenge-url-schemeresource.urldefaults to https on public hosts (closed as superseded)fix/reth-liveness-recoveryfeat/storefront-live-previewfeat/external-buyer-tool-compatfix/{agent-resume-strip-metadata,discovery-x402-resource-path,init-force-own-cluster-ports,noninteractive-prompt-guard,tunnel-hostname-idempotent}renovate/*× 5Plus the agent-offer model-strip series (
fix(x402): drop the model from agent 402 copy,…stop surfacing agentModel in the 402 extra,…drop internal model from /api/services.json,…from /skill.md), the UTF-8 catalog fix, and the widget per-turn spend cap.Verification done on this branch
go build ./...and the fullgo test ./...are green atcdee3a22.mainhas zero commits absent from this branch.CLOSEDones (fix(network): stop reth liveness probe from killing consistency recovery #750, fix(x402): default 402 challenge resource.url to https on public hosts #751, feat(x402): auth-capture unlock gate + fee revenue metrics #798, feat(storefront): re-land the agent chat widget (revert of the rc0 carve-out) #801) were closed as superseded by this release line, and their content is verified present here — none is an abandoned change being smuggled in.cloudflared,scalar-api-reference,gateway-apiandethereum-clientsare content-identical.Known deltas (deliberate, not oversights)
obolup.shpins are two patch versions behind open PR chore(deps): update obolup.sh dependency updates #797 — this branch hasHELMFILE_VERSION=1.7.2/OLLAMA_VERSION=0.32.5; chore(deps): update obolup.sh dependency updates #797 has since moved to1.7.3/0.32.6. Not pulled in, because chore(deps): update obolup.sh dependency updates #797 is unmerged against main and an RC is the wrong place to absorb a moving renovate branch. Trivial to bump if wanted before final.#806 is merged here but is still
REVIEW_REQUIREDagainst main. An independent review of its content surfaced three items worth a maintainer's judgement before v0.14.0 final. None is a build or test failure — all tests pass.SSE settlement receipt is now trailer-only. For
text/event-stream, settlement is deferred tofinalize(), which runs after the response headers are committed — soX-PAYMENT-RESPONSEcan only be delivered as an HTTP trailer (forwardauth.go:1251). Traefik, cloudflared and most HTTP clients do not surface trailers, including this PR's own generic buyer example (x402-generic-buyer.go:130readsresp.Header). Streaming buyers will silently stop seeing their receipt. The deferral itself is correct and fixes a real zombie-settlement bug — the delivery mechanism is the open question. No test covers a trailer surviving a proxy hop.Default advertised method for
type: httpoffers flips POST → GET.defaultPaidMethodis new in feat: add aggregate /.well-known/x402 and external buyer-tool compat #806 (openapi.go:263) and does not exist on main. Offers with no explicitMethodsnow publishGETinopenapi.json,skill.mdand.well-known/x402— a public-contract change for any JSON-body offer, currently ungated and unversioned.facilitator_errorreturns HTTP 503 withretriable:false(forwardauth.go:701). 503 tells generic clients and proxies to retry, contradicting the body. 402/400/502 would be more accurate.Lower-severity items also noted:
signatureVBytehas no length guard (a 64-byte EIP-2098 compact signature ending0x00/0x01gets itssbyte rewritten, failing both verify and settle);normalizePaymentPayloadForVerifyround-trips throughmap[string]any, so a numericvalue/nonceabove 2^53 loses integer fidelity;legacyCompatRequirementsdoublesaccepts[]while the aggregate/.well-known/x402emits CAIP-2 only, so discovery and challenge disagree;interceptor.finalizeis not deferred, soErrAbortHandlerskips its failure metric; andflow-22-external-buyer-compat.sh:227runsnpx -y @agentcash/discovery@latestunpinned inside release smoke.Security fix included
CodeQL raised four new
go/log-injectionalerts against #806 ininternal/x402/forwardauth.go(lines 672/688/695/704) — the buyer's payment payload and the facilitator's reject reason reachedlog.Printfunescaped, letting a crafted CRLF value forge log lines (for example a fake "payment settled successfully" entry). Fixed at the producers rather than at each call site, so future log statements inherit it:paymentPayloadSummary,facilitatorRejectDetailand the normalize note now return CR/LF-stripped strings, andtruncateForLogsanitizes too (it carries the raw facilitator/verifybody). Regression test:TestLogFieldsAreSingleLine. The fix was pushed to #806's own branch so that PR's alerts clear, then merged here.Note on #800
#800 (the rc1 integration PR) is fully contained in this branch and should be closed as superseded once this PR is open, not before — so the widget and auth-capture work never lacks an open path to main.
https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v