Skip to content

release: v0.14.0-rc2 - #809

Open
bussyjd wants to merge 51 commits into
mainfrom
integration/v0.14.0-rc2
Open

release: v0.14.0-rc2#809
bussyjd wants to merge 51 commits into
mainfrom
integration/v0.14.0-rc2

Conversation

@bussyjd

@bussyjd bussyjd commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Release integration branch for v0.14.0-rc2. Tag v0.14.0-rc2 points at the tip of this branch (cdee3a22).

Everything on main is already contained here (git log main --not integration/v0.14.0-rc2 is 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:

Branch PR Note
feat/chat-widget-reland #801 Agent chat widget re-land (closed as superseded — content lives here)
feat/authcapture-unlock #798 x402 auth-capture unlock gate + fee revenue metrics (closed as superseded)
fix/x402-challenge-url-scheme #751 402 challenge resource.url defaults to https on public hosts (closed as superseded)
fix/reth-liveness-recovery #750 Stop the reth liveness probe killing consistency recovery (closed as superseded)
feat/storefront-live-preview #805 Merged to main, included here
feat/external-buyer-tool-compat #806 Still open and unreviewed — see below
fix/{agent-resume-strip-metadata,discovery-x402-resource-path,init-force-own-cluster-ports,noninteractive-prompt-guard,tunnel-hostname-idempotent} #790 #791 #792 #789 #793 Already merged to main
renovate/* × 5 #794#797, #787 Dependency bundles

Plus 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

Known deltas (deliberate, not oversights)

⚠️ Review findings on #806 (carried by this branch)

#806 is merged here but is still REVIEW_REQUIRED against 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.

  1. SSE settlement receipt is now trailer-only. For text/event-stream, settlement is deferred to finalize(), which runs after the response headers are committed — so X-PAYMENT-RESPONSE can 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:130 reads resp.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.

  2. Default advertised method for type: http offers flips POST → GET. defaultPaidMethod is 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 explicit Methods now publish GET in openapi.json, skill.md and .well-known/x402 — a public-contract change for any JSON-body offer, currently ungated and unversioned.

  3. facilitator_error returns HTTP 503 with retriable: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: signatureVByte has no length guard (a 64-byte EIP-2098 compact signature ending 0x00/0x01 gets its s byte rewritten, failing both verify and settle); normalizePaymentPayloadForVerify round-trips through map[string]any, so a numeric value/nonce above 2^53 loses integer fidelity; legacyCompatRequirements doubles accepts[] while the aggregate /.well-known/x402 emits CAIP-2 only, so discovery and challenge disagree; interceptor.finalize is not deferred, so ErrAbortHandler skips its failure metric; and flow-22-external-buyer-compat.sh:227 runs npx -y @agentcash/discovery@latest unpinned inside release smoke.

Security fix included

CodeQL raised four new go/log-injection alerts against #806 in internal/x402/forwardauth.go (lines 672/688/695/704) — the buyer's payment payload and the facilitator's reject reason reached log.Printf unescaped, 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, facilitatorRejectDetail and the normalize note now return CR/LF-stripped strings, and truncateForLog sanitizes too (it carries the raw facilitator /verify body). 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

bussyjd and others added 30 commits July 14, 2026 17:06
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
… openapi for inference/agent

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
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)
bussyjd and others added 21 commits July 20, 2026 19:07
…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)
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
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>
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
bussyjd force-pushed the integration/v0.14.0-rc2 branch from cdee3a2 to b874a19 Compare August 6, 2026 12:51
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.

2 participants