Skip to content

integration: v0.14.0-rc0#756

Merged
bussyjd merged 91 commits into
mainfrom
integration/v0.14.0-rc0
Jul 17, 2026
Merged

integration: v0.14.0-rc0#756
bussyjd merged 91 commits into
mainfrom
integration/v0.14.0-rc0

Conversation

@bussyjd

@bussyjd bussyjd commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Integration branch for v0.14.0-rc0, collapsing open PRs onto main:

PR Title Role
#753 chore(storefront): page-surface naming cleanup + template convention Controller/render rename + embedded offer landing template
#754 fix(hermes): edge-redirect dashboard root to password-login Dashboard HTTPRoute //auth/password-login; operator guidance
#755 docs: align install quickstart with v0.13.0 product behavior Hosts resume, OBOL_NONINTERACTIVE, dormant tunnel, obol.stack:8080 Host header
#757 fix(serviceoffer): namespace-disambiguate ReferenceGrant names Cross-namespace offer name collision
#758 fix(serviceoffer): require 2xx for UpstreamHealthy probes False UpstreamHealthy on 404
#759 fix(serviceoffer): split inFlightReq + rateLimit Middlewares Invalid combined Traefik middleware
#760 fix(x402): public path + https in 402 resource.URL Dedicated-host / tunnel challenge URL rewrite
#761 fix(sell): enable registration on offers after CLI ERC-8004 register Inconsistent ERC-8004 activation/status
#752 feat(storefront): built-in browser chat widget for agent-type offers Agent offer chat widget (base of SIWE stack)
#762 feat(chat): session-wallet security — SIWE, withdraw, contract-wallet guard Session-wallet hardening stacked on #752

Also resolves a README merge conflict between #754 and #755 (keeps both dashboard login details and install accuracy).

Review follow-up (Oisin)

Review notes

Excluded (still open, not in this train): #750, #751.

Smoke (spark1)

  • Host: spark1
  • Branch HEAD (docs + hermes + storefront naming + field fixes pre-chat): 919502e1
  • Worktree: obol-stack-qa-20260715-212834-v014rc0-919502e1
  • Command: RELEASE_SMOKE_INCLUDE_OBOL=true RELEASE_SMOKE_INCLUDE_OBOL_FORK=true bash flows/release-smoke.sh
  • LLM: http://127.0.0.1:8000/v1 model qwen36-nvfp4
  • Result: PASS (__RELEASE_SMOKE_DONE_RC__=0, FAIL count 0, PASS count 18)

Report: $QA/.tmp/release-smoke-20260715-212850/RELEASE_REPORT.md

Current HEAD: 77a6e0d1 (includes #752/#762 chat stack + dud-file delete + #757#761). Recommend optional re-smoke before tag if you want live coverage of chat widget + controller/x402 fixes.

Test plan

bussyjd added 20 commits July 15, 2026 17:19
skillCatalog* named a busybox httpd after the first file it ever held.
It now serves skill.md, services.json (the storefront's own backend via
SERVICES_URL), openapi.json, the API docs, and one bundle per
hostname-bound offer -- including every offer landing page. "Skill
catalog" described one of those.

Two concepts were sharing the name, so this splits them:

  skillCatalog*          -> staticSite*      (the httpd + its ConfigMap)
  buildSkillCatalogMarkdown -> buildSkillMarkdown   (builds skill.md)
  skillCatalog{HowToPay,TryIt,RouteLines} -> skillMarkdown*  (its sections)
  catalogMu              -> staticSiteMu     (guards the static-site reconcile)

Identifiers only. Every k8s wire name is untouched -- obol-skill-md,
obol-skill-md-route, obol-catalog-headers and namespace x402 are all
byte-identical, verified by diffing the string literals removed against
those re-added (net zero). Renaming the objects would be a live-cluster
migration and they are referenced from internal/tunnel, cmd/obol,
internal/stackbackup, the embedded x402.yaml and next.config.ts; a
comment at the const block now explains the deliberate mismatch.

ServiceCatalog* (the /api/services.json wire types) keeps its name -- that
one is genuinely a catalog.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
…emplate

The landing page was a backtick literal buried mid-offerbundle.go while all
three x402 pages (payment_required, siwx_challenge, error_page) follow one
convention: templates/<name>.html + //go:embed + <name>HTMLSrc + <name>Tmpl.
It is the surface users actually hit and it was the only one you could not
open as HTML.

Adopts the existing x402 convention rather than inventing a second one:

  internal/serviceoffercontroller/templates/offer_landing.html
  //go:embed templates/offer_landing.html
  var offerLandingHTMLSrc string

Content is byte-identical to the previous literal (verified by extracting
the literal from origin/main and diffing against the new file), so the
rendered page is unchanged.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
… gone

DESIGN.md told the next engineer to hand-mirror the token palette across
three files and gave a drift check to enforce it. Both were stale:
payment_required.html contains zero hex values -- theming moved to
storefront.ResolveTheme(...).CSSVars() via {{.Branding.ThemeCSS}}. The
documented check diffed hex out of that template against globals.css, so
its left side was empty and it compared an empty set forever. Following
the doc would have re-introduced exactly the drift it existed to prevent.

- SS 2: describes the real source (theme.go is the single owner) and says
  not to paste hex back in.
- SS 5: points at the one hand-mirror that does survive -- theme.ts's
  LIGHT_THEME_VARS, a fallback that rots silently because a healthy page
  takes tokens from the feed. The new check compares theme.go's ThemeLight
  against it; verified it passes today (13 pairs each side) and verified it
  FAILS on injected drift, which the old one could not do.
- SS 0: adds the five public surfaces and their data-obol="page-*" markers.
  Nothing else in the repo lists them. Records the two things that are easy
  to get backwards: page-402 renders only on a paid path with Accept:
  text/html (so a root-priced offer's browser visitors get page-landing,
  never page-402), and data-obol="checkout" is a mount div on two pages
  rather than a name for the 402 page.
- Corrects "four surfaces" -> five.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
Every hostname-bound offer with spec.type=agent now serves a free,
self-contained chat client at /chat on its dedicated origin, embedded on
the offer landing page via the reserved checkout mount. One shared static
copy (catalog httpd root) serves every agent offer on any stack: the page
derives the agent name from the hostname and price/model/payment
network/asset from the live 402 challenge on POST /v1/chat/completions
(eip155:8453 and eip155:84532 supported).

Sign-in with Ethereum, without a SIWE server: the visitor connects an
injected wallet, signs one fixed domain-bound message whose keccak256
deterministically derives a local session key, funds that address with a
small USDC transfer, and every chat turn is then paid silently via x402
(EIP-3009, gasless for the payer, wrapFetchWithPayment). The key never
leaves the page and re-signing recovers it; the page refuses challenges
above a $0.05 safety cap.

Delivery: chat.html + a single esbuild vendor bundle (viem 2.21.25 +
@x402/fetch + @x402/evm 2.18.0 — the pair validated live against the
verifier with real settlements) are embedded in the controller binary,
projected into the catalog ConfigMap once, and exposed per-agent-offer via
two Exact rules on the so-<name>-host route so they beat the PathPrefix
payment gate exactly like the discovery paths. httpd.conf gains the .js
MIME mapping ES module loading requires. Non-agent offers are unchanged
(route topology pinned by tests).

Refs #671.
Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
Intermediaries cache .js aggressively (Cloudflare's default cacheable
extensions include .js), so a rebuilt vendor bundle behind the same URL
can strand browsers on a stale copy whose exports no longer match
chat.html. Pin the import with a content-derived ?v= query and document
the bump step in the assets README.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
… chat

The widget only re-read the session wallet's balance at sign-in, after
its own Fund button, and after each message — funds sent from any other
wallet (or arriving after sign-in) left the composer disabled on a stale
zero. Poll every 8s once signed in, make the balance readout
click-to-refresh, and stop refreshBalance from throwing on transient RPC
errors (keep the last rendered state instead).

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
Hermes's API server CORS middleware returns 403 for any request carrying
an Origin header whose origin is not allowlisted — and its default
allowlist is empty. Browsers attach Origin to every POST, including
same-origin ones, so the /chat widget's paid requests were rejected by
the upstream after payment verification (settlement correctly skipped —
buyers were not charged, but chat was unusable from any browser).

Set API_SERVER_CORS_ORIGINS="*" on the rendered hermes deployment: the
API server is only reachable through the x402 verifier, payment is the
gate, and no ambient credentials exist, so the wildcard is safe.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
The skip-when-unchanged fast path compared only skill.md, the aggregate
docs, and the per-offer bundles — a controller upgrade that changed the
embedded chat widget computed the same hash, skipped the ConfigMap
apply, and pinned the old widget assets forever. Fold chat.html +
chat-vendor.js into both the content hash and the deployed-ConfigMap
match, with a regression test.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
…er origin-strip

Unsigned WIP checkpoint — matches the images deployed on silvernuc3
(serviceoffer-controller:chatwidget-compact5, obol-x402-verifier:originstrip1).
To be re-committed signed onto feat/agent-chat-widget for PR #752.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
Connect button chains straight into the session-key signature (still
user-triggered — same click gesture), so the two-stage onboarding is one
click; the separate sign-in stage remains only as the rejection-retry
path. Removes the reveal-key affordance: the session key is deterministic
(re-derivable by signing in again with the same wallet), so surfacing a
raw private key bought nothing but risk. Slimmer empty-state copy.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
The iframe'd widget duplicated the landing page around it: agent title,
price, network. A head inline script adds .embed when framed (before
first paint, so no header flash) and CSS hides the header; the price
still shows in the composer placeholder. The landing chat card loses its
heading and padding — it is just the widget now — with a small
full-page link below the card. Full-page /chat is unchanged.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
The textarea placeholder injected the full registration name, wrapped to
two lines and clipped in the one-row box. Short placeholder (the page
already names the agent), a touch more padding and gap in the strip and
composer, muted placeholder color.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
The label span was inline while every sibling row (Endpoint, Model,
Network) breaks after its label, so the card rendered
"API docsAPI docs ↗ · openapi.json ↗". block on the label matches the
siblings.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
stream:true with a line-buffered SSE reader appending deltas via
textContent (JSON fallback kept for non-streaming upstreams) — first
tokens at ~3.5s instead of the full ~15s wait, verified end-to-end with
a settled paid probe before the widget change.

Payment policy: the fixed $0.05 MAX_PRICE cap is gone (it also blocked
legitimately pricier agents); instead an x402Client registerPolicy
filter refuses any single payment above the CURRENT session balance.
The session balance is the budget the user explicitly parked, so no
authorization can ever exceed it, and a mid-session price hike beyond
it is refused with a clear message instead of silently paid. The
startup probe price check was advisory only; this enforces on every
payment.

Streaming implemented by grok-4.5 lane from spec; balance-ceiling
follow-up from the forked side-review. Diff reviewed, tests re-run.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
Hermes basic-auth is password-only, so bare / redirects into
/auth/login?provider=basic and 500s. Print the pretty dashboard host,
edge-redirect Exact / → /auth/password-login on the dashboard HTTPRoute,
document credentials, and smoke-check root + password-login in flow-04.
Hackathon setup feedback: pin OBOL_RELEASE to a current tag, document
hosts resume + OBOL_NONINTERACTIVE, Ollama-skip skipping Hermes, dormant
tunnel, and that Traefik needs Host: obol.stack (localhost:8080 is 404).
Include a GitBook gap list for docs.obol.org.
Resolve README conflict by keeping v0.13 install/hosts guidance and the
Hermes dashboard password-login section.
@bussyjd
bussyjd requested a review from OisinKyne July 15, 2026 15:16

@OisinKyne OisinKyne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a dud file in one of the sub PRs that we probably should delete before merging this

bussyjd added 8 commits July 15, 2026 21:06
Two ServiceOffers with the same name in different namespaces both wrote
to x402/so-<name>-backend-grant, overwriting each other and flapping
backendRefs. Include the offer namespace in the grant name.
Previously any status <500 counted as healthy, so a 404 healthPath left
offers Ready while paid traffic failed. Match real liveness: 2xx only.
…iddlewares

Combining --max-in-flight and --rps produced one Middleware CR with both
types; Traefik rejected it while the offer still reported Ready. Emit one
CR per type, attach both ExtensionRefs, and delete the legacy -limits CR.
Dedicated-origin routes rewrite /path into /services/<name>/path before
the verifier; challenges still advertised the internal path. Also default
public hosts to https so TLS-terminated tunnels do not leak http://
resource URLs that break x402scan discovery.
obol sell register left ServiceOffers created with --no-register stuck
at registration.enabled=false / Registered=Disabled while the on-chain
AgentIdentity already had an agentId. After a successful register, patch
those offers enabled=true and treat a known agentId as Registered=True
even when RegistrationRequest phase is empty.
bussyjd added a commit that referenced this pull request Jul 16, 2026
The two findings tables enumerated still-open vulnerabilities with exact file
paths and attack scenarios. Merging to public main would publish a ranked
worklist against production operators before fixes ship. Replaced both tables
with severity/count summaries and abstract class labels; per-finding locations
and repro steps now live only in the private security tracker. Methodology
(invariants, four layers, how to run) is unchanged.

Blocker #1 from the PR #756 Fable 5 review.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
docs/testing/proactive-bug-finding.md, .claude/workflows/obol-audit.js, and
hack/canary/run-canary.sh were internal bug-finding methodology and agent
tooling merged via #770. They do not belong in the shipped product repo (and
the doc's findings tables risked disclosing open issues on public main). The
methodology and results are retained privately. The invariant regression test
(name_injectivity_test.go) stays — it guards a real fix.

Supersedes the redaction in #770; addresses blocker #1 of the #756 review.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
bussyjd added 8 commits July 16, 2026 20:44
…gin on free routes

BuildV2RequirementWithAsset only rejected parse errors and negative
amounts in decimalToAtomic's result, so a "0" price — or a sub-atomic
price like "0.0000001" at 6 decimals, which rounds to 0 — produced a
payment requirement with Amount "0", contradicting its own doc comment
promising it never returns a $0 requirement. resolvePaidRoute now skips
(and, with no other options, fails the whole route closed at 403)
instead of advertising a free accepts[] entry for a nominally paid
route. Free/gate:auth routes never build a paid requirement, so they're
unaffected.

buildUpstreamProxy also unconditionally stripped Origin/Sec-Fetch-* on
every route class. That's correct on paid/gate:auth routes, where the
verifier is the browser-facing auth boundary and must not let raw
fetch-context headers leak upstream after it already re-issued the
request under its own authority. But gate:free routes have no verifier
auth to protect — stripping there instead broke an upstream's own
Origin-based CSRF defense. Only strip on !rule.IsFree() now.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
The #775 injection fix wrapped imported values with yamlScalar() but
missed the two sites that interpolate names sourced from
listOllamaModels() (which JSON-decodes the local Ollama HTTP endpoint
with no charset validation): generateOverlayValues's model-list block
and patchOverlayModelList's overlay-update path both spliced the raw
model id/display-name into hand-built YAML lines. A model name
containing a newline plus a YAML key (e.g. "x\nimage:\n  repository:
evil") could inject new top-level keys into values-obol.yaml, later
applied to the cluster via helmfile sync.

Route both id and name through the existing yamlScalar() encoder at
both call sites. Added regression tests mirroring the #775
TestGenerateOverlayValues_RejectsYAMLInjection pattern for each
site, plus an injection subtest under TestPatchOverlayModelList; both
fail on the pre-fix code and pass after. Updated the three pre-existing
assertions that expected unquoted 'id: <model>' output, since
yamlScalar always double-quotes.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
…ft, and withdraw race

- Serve /chat with Content-Security-Policy: frame-ancestors 'self' (new
  addResponseHeader merges it into the existing ResponseHeaderModifier
  filter, since Gateway API disallows repeating a core filter type per
  rule). The page holds a hot session key and signs USDC transfers, so a
  cross-origin iframe could previously clickjack Connect/Fund/Withdraw/Send.
- Add TestChatVendorVersionMatchesBundle: computes sha256 of the embedded
  chat-vendor.js and asserts chat.html's hardcoded ?v= prefix matches, so a
  vendor rebuild without a version bump now fails CI instead of silently
  serving returning visitors the old payment-signing bundle for up to a
  year (the bundle is cached 1-year immutable).
- withdraw() now awaits the transfer receipt (like fund() already does)
  before declaring success and refreshing, and the withdraw/send guards are
  now mutual: withdraw() also checks `busy`, and the composer submit
  handler also checks `withdrawing`, closing the race where a withdrawal
  and a paid message could spend the same session balance concurrently.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
sell.go's --health-path defaulting only special-cased litellm, so the
documented "obol sell http --upstream ollama" flow (and flow-06's default
ollama branch, which hardcoded /health/readiness) never passed
UpstreamHealthy: ollama only returns 2xx at "/". Extract the defaulting into
defaultHealthPathForUpstream and add an ollama case defaulting to "/";
mirror the fix in flow-06 (ollama -> "/", litellm keeps /health/readiness)
and document it in monetize-inference.md. Emit a u.Dim note when either
default fires, and document both special cases on the --health-path flag.

Also, three low-severity hardening fixes flagged alongside it:
- normalizeAgentOrigin now rejects scheme-less ("//host") and non-HTTP
  ("ftp://host") origins instead of minting a malformed on-chain agentURI.
- enableRegistrationOnOffers honors a new
  "obol.stack/no-auto-register: true" annotation so a deliberately
  --no-register offer can stay unlisted even after a later
  "obol sell register" bulk-enables everything else on its network.
- isTransientRegistrationError now classifies "already known", "nonce too
  low", and "replacement transaction underpriced" as transient:
  registerWithRecovery pins and reuses the same nonce across retries, so a
  broadcast-timeout retry hitting one of these means no new tx was accepted
  (no double-mint risk) -- treating them as transient gives
  recoverRegistrationByOwnerAndURI more time to see the original broadcast
  land instead of misreporting a landed mint as failed.

flow-04-agent.sh's dashboard probe now asserts dash_login is exactly 200 or
401 and dash_root is exactly 302, instead of merely rejecting 500/000.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
#772 eRPC operator overlay hardening:

- SetERPC/parseERPCOverlay now rejects network entries that would get an
  empty networkMergeKey (no evm.chainId or alias) and catches cheap type
  errors (evm not a map, chainId not numeric, alias not a string), so a bad
  overlay fails `erpc set` instead of persisting and duplicating/crash-
  looping eRPC on every merge.

- ResetERPC/stripERPCOverlay now tracks provenance (erpc-overlay-provenance.yaml,
  written by captureERPCProvenance before the first merge of each
  overlay-owned key) so reset RESTORES chart-base/recorded entries an
  overlay replaced instead of deleting them outright, while still dropping
  entries the overlay purely added. Success message now reflects this.

- StatusERPC best-effort reads the erpc-overlay-hash ConfigMap annotation
  and reports ClusterSync (in-sync/drifted/not-applied) so drift between
  the on-disk overlay and the live cluster is visible; `erpc status` CLI
  output surfaces it.

- Fixed the RateLimiters doc comment (budgets merged by id, other keys
  replaced) to match mergeRateLimiters' actual shallow-replace behavior.

Tests: reject-no-merge-key-network, reject-bad-chainId-type, reset-restores-
replaced-base-entry (also verified it fails against the old drop-only
strip), and drift-status table test.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
… pricing, ERC-8004)

#777's upstream-OpenAPI discovery feature had six issues:

- getJSONMap accepted up to 8MiB and re-marshaled it into the shared
  "obol-skill-md" ConfigMap; anything over k8s's ~1MiB ConfigMap limit
  bricked static-site publishing for every offer. Cap fetch + re-marshal
  at 200KiB and fall back to buildOfferScopedOpenAPI when exceeded.

- tryUpstreamOpenAPI ran inline (up to 3x3s GETs, uncached) inside
  buildOfferBundles, itself called under staticSiteMu on every offer's
  reconcile. A slow/flapping upstream serialized reconciles and
  flip-flopped the content hash, rolling the shared discovery pod on
  every flap. Added upstreamOpenAPICache, keyed by offer UID+generation:
  refresh() runs from each offer's own reconcile outside staticSiteMu,
  at most once per generation; buildOfferBundles now takes a lookup func
  and only ever reads the cache.

- fetchUpstreamOpenAPI followed redirects and let Upstream.Namespace
  (offer-author-controlled) pick an arbitrary namespace, with none of
  the ReferenceGrant checks the Gateway API data path requires for
  cross-namespace targets. Disabled redirect following, pinned the
  probe to the offer's own namespace, and rejected non-simple
  openapiPath overrides (traversal / embedded scheme).

- buildOfferWellKnownX402FromOpenAPI priced every paid op at the
  offer's default, ignoring spec.routes[] price overrides the
  non-upstream path already honors. Added routePriceOverride, matching
  the verifier's own pattern semantics.

- buildOfferAgentRegistration never set registrations[], which ERC-8004
  requires once an offer is on-chain; buyers using --expected-agent-id
  failed closed. Populated from status.AgentID + the resolved payment
  network's registry.

- Services[].Endpoint origin scoping used HasPrefix(ep, origin), which
  admits an evil-suffix host like origin+".evil.tld". Fixed to exact
  match or origin+"/".

- Deleted an unreachable duplicate security-empty check in
  openAPIOpIsPaid.

Every fix has a test that fails without it (verified by reverting each
change in isolation and re-running its test).

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
…checksum-tamper fallback, DNS-label overflow, safety-decline exit code)

1. monetizeapi.RemoveAgentIdentityRegistration: allocate a fresh slice
   instead of filtering status.Registrations in place, so a caller passing
   an informer-cached object can't have its backing array silently
   corrupted.

2. tunnel.RefreshStorefront: no-op quietly when storefrontHostnames has
   nothing to report (no persistent tunnel/hostname state yet), instead of
   calling CreateStorefront with zero hostnames and surfacing its
   "requires at least one hostname" error as a confusing warning on a
   normal first `obol sell ... --hostname X --no-register`.
   EnsureTunnelForSell reconciles the storefront once the tunnel exists.

3. obolup.sh: verify_release_checksum now returns exit code 2 for a
   verified checksum MISMATCH (tamper), distinct from exit code 1 for
   "couldn't verify" (SHA256SUMS unpublished / no sha tool / no checksum
   entry). download_release propagates the distinction, and
   install_obol_binary hard-aborts on a mismatch instead of falling back to
   `git clone` over the same (potentially compromised) channel. Also fixes
   the misleading "Release not found" message on a mismatch.

4. agentruntime.MaxIDLength + openclaw/hermes Onboard: bound the deployment
   id to (63 - the runtime's DNS-label prefix/suffix) at the two Onboard
   call sites, so "openclaw-<id>" / "hermes-<id>" / "hermes-<id>-ui" can't
   exceed the 63-char DNS label limit and fail later with an opaque k8s
   error. validate.Name itself is untouched for other callers.

5. stack.destroyOldBackendIfSwitching: a declined backend-switch prompt
   during `stack init --force` now returns nil (clean exit 0), matching
   Down/Purge's behavior for the same ConfirmRunningServicesLoss decline.
   Previously it returned errSafetyAborted, whose doc comment claimed
   cmd/obol/stack.go handled it specially — it never did. Comment fixed to
   reflect that nothing consumes the sentinel today.

Deviation: no automated test for (5)'s behavior change — reaching the
declined-prompt branch requires ui.UI.IsTTY()==true, which has no
test-injectable seam (isTTY is unexported, set from a real
isatty.IsTerminal check, no pty dependency in the repo). Verified by
inspection + go build/vet/test instead; the change is a single-line
return-value swap in an already-covered function.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
…tion

httpRouteAccepted ignored status.parents[].conditions[].observedGeneration,
so the #767 route-acceptance gate (apply -> Get -> httpRouteAccepted before
RoutePublished=True) was bypassed for updates to an already-accepted
HTTPRoute: the Get right after applyObject could return the PREVIOUS
generation's Accepted=True/ResolvedRefs=True, flipping RoutePublished True
even though Traefik hasn't reconciled the new spec yet (and may go on to
reject it).

httpRouteAccepted now requires each trusted condition's observedGeneration
to equal the route's metadata.generation; a stale condition is skipped, so
the parent falls through to not-accepted and the existing 5s requeue
re-checks. Both the main and host route accept-gates (controller.go
~852-874) go through this same function.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
bussyjd and others added 10 commits July 16, 2026 21:10
chore: remove internal audit tooling/docs from production repo
fix(discovery): harden upstream-OpenAPI discovery (size cap, cache, SSRF, pricing, ERC-8004)
fix(sell): default ollama health-path to / and harden registration edges
…dgen

fix(serviceoffer): gate httpRouteAccepted on observedGeneration, fail closed
fix(network): validate + preserve eRPC overlay entries on set/reset
fix(openclaw): yaml-escape Ollama model names in overlay generation
fix(x402): fail closed on zero/sub-atomic priced routes, preserve Origin on free routes
fix(chat-widget): clickjacking, stale-bundle, and withdraw-race hardening
chore: fix assorted PR #756 review nits
Remove the browser chat widget stack (#752 agent-chat-widget, #762
chat-siwe-session, #785 chat-widget-hardening) from the release candidate.
The per-message widget + its browser session wallet are being reworked on
top of x402 batch-settlement, so they ship in a later RC rather than rc0.

Deletes chat.html / chat-vendor.js / chatwidget.go and the /chat routes, the
shared vendor-bundle serving, and the widget tests. Keeps the hostRouteRules
refactor (later fixes depend on it) and the verifier paid-route Origin-strip.

go build ./... and go test ./... green (39 pkgs ok, 0 fail).

NEEDS-RESIGN: committed unsigned due to YubiKey 'invalid format'; re-sign before release.

Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
@bussyjd
bussyjd merged commit 606ba95 into main Jul 17, 2026
10 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.

2 participants