integration: v0.14.0-rc0#756
Merged
Merged
Conversation
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.
OisinKyne
approved these changes
Jul 15, 2026
OisinKyne
left a comment
Contributor
There was a problem hiding this comment.
There's a dud file in one of the sub PRs that we probably should delete before merging this
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
…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
This was referenced Jul 16, 2026
Merged
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
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
Integration branch for v0.14.0-rc0, collapsing open PRs onto
main:/→/auth/password-login; operator guidanceOBOL_NONINTERACTIVE, dormant tunnel,obol.stack:8080Host headerAlso resolves a README merge conflict between #754 and #755 (keeps both dashboard login details and install accuracy).
Review follow-up (Oisin)
docs/guides/docs-obol-org-hackathon-gaps.mdwas a temporary GitBook gap checklist from early docs: align install quickstart with v0.13.0 product behavior #755. Product copy lives inREADME.md+docs/getting-started.mdonly. Deleted on docs: align install quickstart with v0.13.0 product behavior #755 (518e467a) and merged into this branch (fe6d4ca4).Review notes
/RequestRedirect). Flow-04 asserts root 302 + password-login 200.Excluded (still open, not in this train): #750, #751.
Smoke (spark1)
919502e1obol-stack-qa-20260715-212834-v014rc0-919502e1RELEASE_SMOKE_INCLUDE_OBOL=true RELEASE_SMOKE_INCLUDE_OBOL_FORK=true bash flows/release-smoke.shhttp://127.0.0.1:8000/v1modelqwen36-nvfp4__RELEASE_SMOKE_DONE_RC__=0, FAIL count 0, PASS count 18)Report:
$QA/.tmp/release-smoke-20260715-212850/RELEASE_REPORT.mdCurrent 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
go test ./internal/hermes/ ./internal/serviceoffercontroller/ ./internal/x402/919502e1)docs/guides/docs-obol-org-hackathon-gaps.mdfrom integration trainv0.14.0-rc0on main when ready