feat: agent purchase type within hermes#614
Closed
OisinKyne wants to merge 6 commits into
Closed
Conversation
…ardening, doc alignment Synthesis of a multi-agent review of this PR (every finding adversarially verified by two independent checkers; build+786 unit tests green throughout). Release-blocking: - flow-16 ended with undefined `summary` → exit 127 under set -euo pipefail, so every release-smoke run would report FAIL the moment this PR wires flow-16 into the unconditional flows array. Replaced with emit_metrics (the lib.sh idiom every other flow ends with). - Image pins at 04bebbc are this PR's own merge base: the shipped controller still renders sub-agents with Hermes v2026.5.28 (compiled-in default), the verifier lacks the tx-hash + maxTimeoutSeconds changes, the buyer lacks the settled-but-failed ConfirmSpend branch. Cannot be fixed pre-merge (no image containing this PR exists yet) — documented the mandatory post-merge rebuild+repin in embed_image_pin_test.go. Dev clusters mask this via OBOL_DEVELOPMENT rebuilds. Money-path hardening: - buyer proxy: fire OnConfirmSpendFailure (confirm_spend_failure_total) when the settled-but-failed branch fails to persist the consumed nonce — the alert purpose-built for "chain debited but consumed.json missed it"; the 2xx path already did this for the identical error. - buy.py: paid requests (pay, pay-agent) no longer follow redirects. urllib converts a redirected POST into a body-less GET and re-sends every header — including the signed X-PAYMENT voucher — to the Location target. - buy.py: mid-stream read failures in pay-agent print an actionable warning (payment already accepted; do not blindly retry) instead of a traceback. - buy.py: the settled-on-chain warning fires on any failed status (>=400) with a tx-hash settle header, matching the Go buyer condition; non-dict settle JSON no longer raises AttributeError. - buy.py: auth-TTL floor raised 300s → 600s to track the new default settle window (x402.DefaultMaxTimeoutSeconds). Consistency: - docs/observability.md + release-smoke-debugging.md: corrected the "5xx" claims to the actual conditions (buyer: >=400; verifier: non-200 /settle). - Replaced all 9 references to never-committed plans/rc13report.md (docs, code comments, test comments, buy.py runtime output) with pointers to docs/observability.md ("Verify settlement against the chain"). - renovate.json: Hermes pre-merge gate referenced nonexistent flow-17-agent-bundled-skills.sh → flow-16-sell-agent.sh §1.1.1/§1.3.1. - openapi.go: /api/services.json now advertises the clamped maxTimeoutSeconds the 402 wire actually enforces. - SKILL.md: corrected "nothing settles on-chain until that one request succeeds" (disproven by the rc13 incident this PR handles) and the stale 300s TTL-floor rationale. - discover.go: the pay-agent pointer also fires for storefront-base probes of agent-only sellers, not just exact /services/<name> URLs. - flow-16: pod gate on the Ready condition (not phase Running) so the exec-based bundled-skills assertions cannot false-pass mid-boot. Intentionally skipped: buy.py wire-echo fallback maxTimeoutSeconds=60 for sellers that omit the field (changing the echo risks requirement matching); PaymentEventUnsettled naming conflation (cosmetic); verifier-side metric for settle-error-with-tx (follow-up). Note the "default 600s" applies to static routes + the standalone gateway — ServiceOffer paths pin 300 via the CLI --max-timeout default and the CRD schema default.
Removed plans whose work landed and whose durable learnings already live in
the skill references:
- obol-sell-demo.md — implemented (`obol sell demo` ships in the CLI)
- post-490-integration-20260513.md — integration landed in May
- release-smoke-hardening-20260513.md — session retro; learnings folded into
obol-stack-dev/references/release-smoke-debugging.md
- inference-v1337-{buy-report,followup}-20260514.md — QA reports; the WAF/UA
findings live in release-smoke-debugging.md §10 (c2dddc1)
Kept: sell-agent-perf.md (in progress), openapi-402-followups.md (pending
walk-through), openapi-redoc-storefront.md (phase 2 deferred),
storefront-buy-inference-cta-handoff.md (active frontend handoff),
volume-permission-hardening.md (PR #615 owns its rewrite).
bussyjd
added a commit
that referenced
this pull request
Jun 9, 2026
…with #614 Applies the verified findings from the cross-review against PR #614 (every item adversarially confirmed against the refs; union merge-tree clean). PVC / upgrade path: - llm.yaml: restore container-level runAsUser/runAsGroup 1000 on x402-buyer. Clusters upgraded in place from <= rc12 keep hostPath-typed PVs where kubelet skips fsGroup; their /state dir is 1000:1000 with consumed.json written 0600 by UID 1000 — a 65532 sidecar cannot read it, Fatalf's on `load state`, and takes every paid/<model> route down. On fresh local-type PVs the explicit UID is harmless (fsGroup 65532 grants group access). embed_buyer_state_test.go updated to pin the new contract. - plans/volume-permission-hardening.md: new "Upgrading from <= v0.10.0-rc12" section — supported path is cluster recreation (wallet backup/restore), with a documented k3d chown escape hatch. troubleshooting.md gets the symptom->fix entry. The Hermes half of the legacy-PV breakage cannot be patched at runtime without reintroducing the chown machinery this PR removes, so it is a documented breaking change instead. Paid-route availability: - llm.yaml: Reloader annotation narrowed to litellm-config only. The buyer ConfigMaps (x402-buyer-config/x402-buyer-auths) are rewritten by the controller on every buy, top-up, auto-refill, and tombstone cleanup; with strategy Recreate + 1 replica the previous annotation bounced the entire inference gateway (all Hermes traffic, in-flight SSE streams) on every purchase event, inverting CLAUDE.md pitfall 7 (restart is fallback, not the default buy path). The buyer hot-reloads via /admin/reload. stack_test.go updated to pin litellm-config-only. Flow alignment with #614: - lib.sh: `stack down` -> `stack down --yes` in reset_flow_workspace. #614's flow-16 (now last in the single-stack array) intentionally leaves a live agent offer; without --yes the non-TTY ConfirmRunningServicesLoss gate refuses, graceful down is silently skipped (`|| true`), and teardown degrades to the raw k3d-delete fallback on every release-smoke run. - flow-11: post-register Ready poll 120s -> 300s to match flow-14's identical live-Base-Sepolia chain-watch path (pitfall 13 free-tier RPC throttling). Known follow-ups (not in this commit): flow-08 buy-retry top-up vs exactly-N assertions on rare partial failures; flow-11 lacks flow-14's remote-signer rolled guard; aztec PVC has no permission story (runs as root today); post-merge controller repin so released sub-agents pick up this PR's UID-1000 render (tracked in #614's pin-test note).
bussyjd
approved these changes
Jun 10, 2026
This was referenced Jun 10, 2026
Contributor
|
Superseded by the v0.10.0-rc14 release train: #616 — this PR's branch is merged there verbatim (merge commit preserved, full union build + test green). Closing in favor of the consolidated release PR. |
bussyjd
added a commit
that referenced
this pull request
Jun 10, 2026
CLAUDE.md (+6 lines net): `sell mcp` in the command tree + one-line description; `pay`/`pay-agent` added to the buy.py command table; pitfall 7 notes the rc14 Reloader behavior (litellm-config restarts, buyer CMs stay hot-reload); new pitfalls 15 (settled-but-failed tx-hash semantics) and 16 (<= rc12 hostPath-PV upgrade breakage). plans/: dropped sell-agent-perf.md (max_turns 30 + reasoning_effort low shipped in agent_render.go; streaming shipped in #614); trimmed openapi-redoc-storefront.md to the deferred phase-2 design (phase 1 — Scalar UI + /openapi.json — is live); openapi-402-followups.md intro updated: its trigger (oisin/openapi landing) has fired, items are now actionable.
OisinKyne
pushed a commit
that referenced
this pull request
Jun 10, 2026
…with #614 Applies the verified findings from the cross-review against PR #614 (every item adversarially confirmed against the refs; union merge-tree clean). PVC / upgrade path: - llm.yaml: restore container-level runAsUser/runAsGroup 1000 on x402-buyer. Clusters upgraded in place from <= rc12 keep hostPath-typed PVs where kubelet skips fsGroup; their /state dir is 1000:1000 with consumed.json written 0600 by UID 1000 — a 65532 sidecar cannot read it, Fatalf's on `load state`, and takes every paid/<model> route down. On fresh local-type PVs the explicit UID is harmless (fsGroup 65532 grants group access). embed_buyer_state_test.go updated to pin the new contract. - plans/volume-permission-hardening.md: new "Upgrading from <= v0.10.0-rc12" section — supported path is cluster recreation (wallet backup/restore), with a documented k3d chown escape hatch. troubleshooting.md gets the symptom->fix entry. The Hermes half of the legacy-PV breakage cannot be patched at runtime without reintroducing the chown machinery this PR removes, so it is a documented breaking change instead. Paid-route availability: - llm.yaml: Reloader annotation narrowed to litellm-config only. The buyer ConfigMaps (x402-buyer-config/x402-buyer-auths) are rewritten by the controller on every buy, top-up, auto-refill, and tombstone cleanup; with strategy Recreate + 1 replica the previous annotation bounced the entire inference gateway (all Hermes traffic, in-flight SSE streams) on every purchase event, inverting CLAUDE.md pitfall 7 (restart is fallback, not the default buy path). The buyer hot-reloads via /admin/reload. stack_test.go updated to pin litellm-config-only. Flow alignment with #614: - lib.sh: `stack down` -> `stack down --yes` in reset_flow_workspace. #614's flow-16 (now last in the single-stack array) intentionally leaves a live agent offer; without --yes the non-TTY ConfirmRunningServicesLoss gate refuses, graceful down is silently skipped (`|| true`), and teardown degrades to the raw k3d-delete fallback on every release-smoke run. - flow-11: post-register Ready poll 120s -> 300s to match flow-14's identical live-Base-Sepolia chain-watch path (pitfall 13 free-tier RPC throttling). Known follow-ups (not in this commit): flow-08 buy-retry top-up vs exactly-N assertions on rare partial failures; flow-11 lacks flow-14's remote-signer rolled guard; aztec PVC has no permission story (runs as root today); post-merge controller repin so released sub-agents pick up this PR's UID-1000 render (tracked in #614's pin-test note).
OisinKyne
pushed a commit
that referenced
this pull request
Jun 10, 2026
CLAUDE.md (+6 lines net): `sell mcp` in the command tree + one-line description; `pay`/`pay-agent` added to the buy.py command table; pitfall 7 notes the rc14 Reloader behavior (litellm-config restarts, buyer CMs stay hot-reload); new pitfalls 15 (settled-but-failed tx-hash semantics) and 16 (<= rc12 hostPath-PV upgrade breakage). plans/: dropped sell-agent-perf.md (max_turns 30 + reasoning_effort low shipped in agent_render.go; streaming shipped in #614); trimmed openapi-redoc-storefront.md to the deferred phase-2 design (phase 1 — Scalar UI + /openapi.json — is live); openapi-402-followups.md intro updated: its trigger (oisin/openapi landing) has fired, items are now actionable.
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
Hardens the x402 paid-flow stack after rc13 QA caught two distinct issues: stale image
pins that bypassed merged source fixes, and a silent-money-loss path where on-chain
settlement landed but the buyer reported "0 spent." Also adds the missing buyer side for
type:agentServiceOffers (streaming SSE), plumbsspec.payment.maxTimeoutSecondsthrough to the 402 response (previously dropped on the floor), and bumps the Hermes
agent image to pick up the upstream
sync_skills()marker-honoring fix.Driven by
plans/rc13report.md(Kody, 2026-06-09); learnings folded intodocs/observability.mdandobol-stack-dev/references/release-smoke-debugging.mdsothe next debugging session doesn't relearn this from a cold start.
Changes
1. Image pins →
04bebbc(currentmainHEAD as of branching)internal/embed/infrastructure/base/templates/{x402,llm}.yamlab71481(suppress per-requestverifyOnly=falselog spam on the in-processsettle path) and
86b8c9f(buyer drops expired pre-signed auths before signing). Bothshipped in source for weeks but were not reaching the deployed cluster because the
embedded pins lagged.
embed_image_pin_test.go,embed_crd_test.go) updated.2.
maxTimeoutSecondsplumbed end-to-endServiceOffer.spec.payment.maxTimeoutSeconds→RouteRule.MaxTimeoutSeconds→BuildV2RequirementWithAsset→ the 402 wire response.DefaultMaxTimeoutSeconds); operatorscan set any value up to 86400s (24h) via
MaxMaxTimeoutSecondscap.because the auth is nonce-bound and replay-safe — the cap exists only to guard against
operator typos (e.g. ms→s).
TestRouteRuleFromOffer_PlumbsMaxTimeoutSeconds,TestClampMaxTimeoutSeconds,TestBuildV2RequirementWithAsset_HonorsMaxTimeoutSeconds.3. New buy.py command:
pay-agent(streaming SSE fortype:agentoffers)pay, deliberately not an alias of--type inference.inferencepushes a
paid/<model>alias behind LiteLLM;agentresponses are first-class data thecalling agent wants to consume itself (memory, tool-call traces, partial results), so
no LiteLLM wire-up.
"stream": true, sendsAccept: text/event-stream, reads responseline-by-line, flushes each SSE event to stdout so a calling Hermes/OpenClaw can re-emit
the stream to its own user (telegram,
obol hermes chat, REST clients).--timeout.obol buy inferencenow emits a clear pointer atpay-agentinstead of just refusingtype=agentoffers (internal/buy/discover.go).for this. The seller already streams OpenAI-compatible SSE through
HandleProxy; thebuyer just needed to consume it.)
4. rc13 headline fix: surface on-chain tx hash on facilitator
/settle5xxQA caught a 0.001 OBOL mainnet on-chain debit from a request that returned
HTTP 503 "Payment settlement failed"while every signal the stack produced said "nothing waspaid." Permit2 settle tx [
0xb5122d…932307](https://etherscan.io/tx/0xb5122d818a058e8bf529380260fa2584ba3d50bfc800f1e906faca34d3932307) mined; the facilitator's post-submit
step then returned 500.
This PR doesn't fix the facilitator (that's the hosted
x402.gcp.obol.techservice),but closes the client-side robustness gap that turned a facilitator hiccup into
silent money loss:
internal/x402/forwardauth.go):facilitatorSettlenow returns theparsed response alongside the error on non-200, and the settle interceptor writes
X-PAYMENT-RESPONSEwith the tx hash before committing the 503. Without this theon-chain hash was invisible to the buyer.
internal/x402/buyer/proxy.go): an upstream 5xx withX-PAYMENT-RESPONSE.transaction != ""now triggersConfirmSpendon the held auth (itwas spent on-chain), fires
OnPaymentUnsettled, and logs the hash + payer + network.Previously the auth was released back into the pool, leaving the spent counter
permanently off and the next reuse attempt to fail on Permit2 nonce-reuse revert.
internal/embed/skills/buy-x402/scripts/buy.py):_print_paid_request_failuredecodes the settle header on 5xx and prints a loud⚠️ SETTLEMENT MAY HAVE COMPLETED ON-CHAINwarning with the tx hash and the exactbalance --chain <X>command.TestForwardAuth_SettleErrorPreservesTxHashInHeader(verifier) andTestProxy_UpstreamErrorWithTxHash_PersistsConsume(buyer).5. Hermes image bump:
v2026.5.28→v2026.6.5Closes a separate sub-agent context-bloat bug uncovered while triaging an older
sub-agent during the rc13 session:
v2026.5.28'ssync_skills()had no.no-bundled-skillsmarker check (tools/skills_sync.py:419), so every Hermes pod bootcopied ~24 stock skill categories (~1 MB of SKILL.md text, ~100k tokens of prompt-scan
bloat) from the image-baked
/opt/hermes/skillsinto the PVC — regardless of the markerour
SeedHostFileswrites at agent-create time. Affected every CRD sub-agent, notjust stale PVCs.
Upstream fix landed in
v2026.6.5as commit2ed96372a("blank-slate skills"):sync_skills()now early-returns when the marker exists (tools/skills_sync.py:467).Verified by reading both tags' source. No
containerEnvchanges needed — the marker ourcode already writes is now honored end-to-end.
internal/serviceoffercontroller/agent_render.go(sub-agent default)internal/hermes/hermes.go(master agent default)internal/agentcrd/agent_contract_integration_test.godoc comment updated with thev2026.5.28 → v2026.6.5 root cause + upstream commit reference.
Also picks up unrelated fixes between 5.28 and 6.5: gateway
max_tokenspropagationchain, multi-profile remote-dashboard sessions, several smaller items.
6. Debugging-lore additions
docs/observability.md: new § "Verify settlement against the chain, never the sidecarsnapshot" with the operator debugging recipe (
eth_getLogscurl) and the rc13 tx hashas evidence.
.agents/skills/obol-stack-dev/references/release-smoke-debugging.md§ 11: samelesson at the runbook surface so future debugging sessions don't have to rediscover it
from
0 spent / 2 remaining.Follow-ups (NOT in this PR — flagged for separate work)
These are deliberately left for separate PRs because they each have non-trivial scope or
live outside this repo. Tracked here so they don't get lost:
deployed clusters until CI publishes new
x402-verifier/x402-buyerimages taggedwith the merge-commit short SHA. A one-line follow-up to
internal/embed/infrastructure/base/templates/{x402,llm}.yamlwill pin those. Untilthen the cluster sees the
04bebbcpin, which has theab71481and86b8c9ffixes butnot the
maxTimeoutSecondsplumb or the settle-tx-hash forensic fix.2. Wire the integration test into CI.
TestIntegration_AgentContract_HonorsRenderedConstraints(//go:build integration, ininternal/agentcrd/agent_contract_integration_test.go) verifies the no-bundled-skillscontract end-to-end on a live cluster, but isn't referenced in any
.github/workflows/or
flows/release-smoke.sh. Manual-only invocation means rc13's bloat went undetectedfor weeks. Add it to release-smoke (or a Hermes-bump gate).
Renovate annotation on the Hermes pin. TwodefaultImage/defaultHermesImageconstants in this repo currently track the Hermes release cadence by hand. A Renovate
# renovate: ...annotation would surface future bumps automatically — sized as a 2-linefollow-up.
/data/.hermes/skillscleanup on pre-b121f99PVCs. Agents created beforethe streamline work (June 2) have
~/.hermes/skills/already populated; even with thev2026.6.5 marker honoring, that dir is already on disk and
load_skills()reads it.Manual fix per agent:
kubectl exec -n <ns> deploy/hermes -- mv /data/.hermes/skills /data/.hermes/skills.bundled-disabled-<ts>+kubectl rollout restart deploy/hermes -n <ns>. Not worth code in this repo — strictly an upgrade-migration concern for knownexisting deployments.
hash so an operator can reconcile manually. The bigger fix is to query an in-cluster RPC
(eRPC) for the receipt status before deciding 200 vs 5xx — when settle lands on-chain,
the verifier could serve the upstream response and report success. Needs RPC client
plumbed into the verifier with per-network config; sized as its own PR.
reuse reverting on-chain to prevent double-debit — that works but burns gas and surfaces
as cascading 503s. Proper fix: an auth state machine with
PENDING → SETTLEDtransitions guarded by tx receipt confirmation.
/settlereturn 500after a successful on-chain submit while Base Sepolia USDC settles cleanly? Out of scope
(hosted
x402.gcp.obol.tech), but the receipt-tx for the reproducible failure is inplans/rc13report.md.plans/rc13report.md):obol wallet importre-breaks Hermes ownership (fix =rollout restartmaster).--force/ abandon path.purgedata-dir cleanup on headless sudo fails (cleaned manually via rootcontainer).
the OBOL token's
_spendAllowancespecial-casetype(uint256).max? If it doesn't,every settled transfer decrements a "max" allowance until 0, and the fix is in periodic
re-approval, not
cmd_paypre-flights.Test plan
go test ./...clean.TestForwardAuth_SettleErrorPreservesTxHashInHeaderTestProxy_UpstreamErrorWithTxHash_PersistsConsumeTestRouteRuleFromOffer_PlumbsMaxTimeoutSecondsTestClampMaxTimeoutSeconds,TestBuildV2RequirementWithAsset_HonorsMaxTimeoutSecondspython3 buy.py pay-agentshows usage and exits 1;python3 buy.pyhelp textlists the new command.
../NousResearch/hermes-agent):v2026.5.28sync_skillshas no marker check;v2026.6.5early-returns on the marker attools/skills_sync.py:467(commit2ed96372a).(
obol sell inference→ curl withX-PAYMENT) and verify that a forced facilitator 500(if reproducible) now surfaces the tx hash to the buyer and accounts the auth as spent.
maxTimeoutSeconds: 1800in aServiceOffer reaches the 402 wire response (curl the offer endpoint, decode the
accepts[].maxTimeoutSecondsfield).TestIntegration_AgentContract_HonorsRenderedConstraints(go test -tags integration -v -run TestIntegration_AgentContract_HonorsRenderedConstraints ./internal/agentcrd/) ona fresh cluster and confirm
/data/.hermes/skillsis empty after pod ready.obol buy inference <type=agent-offer>returns the new actionable error pointingat
pay-agent.Implemented #2 + #3
#3 — Renovate annotation (3 files):
depName=nousresearch/hermes-agent above defaultImage
defaultHermesImage
hermes-agent:vX.Y.Z strings (verified against both files, doesn't trip on the existing
rawChartVersion helm annotation).
Monday-scheduled PR template that includes a pre-merge gate explicitly reminding the
reviewer to run release-smoke (flow-16 + flow-17 mentions).
historically changed skill layout / marker semantics, exactly the class of regression
that broke v2026.5.28.
#2 — CI wiring (3 files):
have caught the v2026.5.28 bloat).
comment explaining why it's now release-gating.
that flow-16 now provides CI coverage of the same invariants — Go test is white-box
developer surface, flow-16 is the black-box CI gate.
Verification:
currentValue=v2026.6.5, doesn't false-match the existing rawChartVersion = "2.0.2"
annotation.