fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report - #583

Merged
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers
Jun 3, 2026
Merged

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report#583
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

What changed: Fixes the v0.10.0-rc9 upgrade-report issues. Each was validated against rc9
source (adversarially re-checked) before fixing.

  • Bring obolup code to this repo #1 (GA blocker) — repin serviceoffer-controller from the f5d94fc side-branch build
    (which predated the Secret-create-only reconciler change) to 503016b@sha256:bec62ea0
    (rc9 commit 503016bf, image 0.10.0-rc9). The old pin Updates per-agent Secrets, which
    the tightened RBAC (no secrets:update/patch) 403s → per-agent provisioning never converges.
    Added a tripwire test.
  • User facing ingress #3x402-buyerHoldSign now drops expired pre-signed auths before signing
    (Permit2 deadline / ERC-3009 validBefore), ending the 503 invalid_payment_expired cascade.
  • WIP: Working towards a high quality rpc. #4buy.py status/list count is expiry-aware (valid vs expired auths).
  • Specify flag passing to obolup (or obol) #5reconcileDeletingPurchase finalizes the delete-drain on the not found in sidecar status signal instead of requeueing forever (stranded Terminating).
  • P1 — suppress the per-request verifyOnly=false warning on the in-process settle path
    (HandleProxy / obol sell inference); the Traefik ForwardAuth path still warns.
  • P2obol agent new --model X validates X against the LiteLLM registry; fails fast.

Issue #2 (master-Hermes PVC ownership on k3d local-path) is already fixed on main
(eb985bd/671c8ac root-chown init container); this branch inherits it. Per-agent Hermes was
confirmed not a residual (it seeds via Secret, not host PVC writes).

Why it matters: #1 and #2 are GA blockers from the rc9 report; the rest are buy-side
correctness / UX fixes on the monetize path.

Risk level: low — the controller fix is a pin bump to an already-published rc9 image; the rest
are narrow, regression-tested behaviour changes. No RBAC widened, no security surface added.

Commit under test: 8cfd0ca1 (live-chain evidence captured at b90118fd; 8cfd0ca1
adds only the dual-stack flows retry, no runtime behaviour change)

Base branch: main

Scope

  • Code
  • Charts / manifests
  • Flows / QA scripts
  • Docs / skills
  • Images / dependencies
  • Other:

Validation

CI checks:

CheckStatusLink
(added on push)pending

Unit tests:

go test ./... → ok (33 packages), gofmt + go vet clean
new regression tests (all PASS, #3/P1 fail without the fix):
internal/x402/buyer TestPreSignedSigner_DropsExpiredAuths, TestAuthDeadlineUnix (#3)
internal/serviceoffercontroller TestIsSidecarUpstreamGone (#5)
internal/x402 TestForwardAuth_SettlesInProcess_SuppressesWarning (P1)
cmd/obol TestIsModelConfigured (P2)
internal/embed TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix (#1)
+ dev-rewrite/production-pin guards updated for the new pin
commit: b90118fd

Integration tests:

n/a — covered by the release smoke below.

Flow tests (best result per flow across 3 full smoke runs on local darwin/arm64, k3d):

FlowNetworkResultEvidence
flow-01..10 (single stack incl. buy+lifecycle)base-sepolia (anvil)PASSrun5 + run6
flow-11 dual-stack (USDC)base-sepolia (anvil)PASSrun6
flow-13 dual-stack-obol (anvil fork)base-sepolia (anvil fork)PASSrun5
flow-14 live-obolbase-sepolia (LIVE)PASSrun5 + run6 — on-chain receipts below

Release smoke:

RELEASE_SMOKE_INCLUDE_OBOL=true RELEASE_SMOKE_INCLUDE_OBOL_FORK=true \
OBOL_LLM_ENDPOINT=http://<spark>:8000/v1 OBOL_LLM_MODEL=qwen36-deep \
bash flows/release-smoke.sh
LLM routed through vLLM qwen36-deep (DGX Spark over tailscale).
Every flow passed; no single run reached 13/13 because this macOS/Docker-Desktop +
cloudflare-quick-tunnel box hit a DIFFERENT environment-side transient each pass
(none code-related):
run5: 12/13 — flow-11 Docker Desktop gRPC-FUSE mount race on Alice cluster create
run6: 12/13 — flow-13 same mount race on Bob cluster create
run7: — flow-07/08 cloudflare quick-tunnel failed to establish (local 402 gate PASS)
The mount race now has a retry (commit 8cfd0ca1). The tunnel flake is external (trycloudflare).
#1 pin bump validated separately on a live cluster: deploying 503016b@sha256:bec62ea0 made a
per-agent `obol agent new` reach Ready with no 403; the prior f5d94fc image reproduces the 403.
Dev-mode rebuilds controller/buyer/verifier from this branch's source, so the smoke exercises
the source fixes directly.

Live Chain Evidence

Network: Base Sepolia (eip155:84532)

RPC/provider: paid drpc load-balancer (redacted)

Facilitator: https://x402.gcp.obol.tech (prometheus-overlay)

Contracts and tokens:

NameAddressVersion / notes
OBOL token0x0a09371a8b011d5110656ceBCc70603e53FD2c78Obol Network / OBOL / 18 dp, Permit2
ERC-8004 Identity Registry0x8004a818bfb912233c491871b3d84c89a494bd9emint = agentId

Wallet roles:

RoleAddressSource
Alice / seller / register0xC0De030F6C37f490594F93fB99e2756703c4297Eseller payTo + funded EOA
Bob / buyer / payer0x57b0eF875DeB5A37301F1640E469a2129Da9490Edeterministic 2nd-derived from REMOTE_SIGNER_PRIVATE_KEY; bobSigner == BOB_WALLET ✓

Balances:

TokenAddressBeforeAfterExpected deltaActual delta
OBOL (Bob)0x0a09371a…4949000000000000000 wei4948000000000000000 wei-1000000000000000 (0.001 OBOL)-1000000000000000 ✓ exact
OBOL (Alice)0x0a09371a…+1000000000000000 (0.001 OBOL)+1000000000000000 ✓ exact

Transaction receipts:

PurposeTx hashFromToAmount / eventStatus
ERC-8004 registration0xff4cdbbdeea75e578728f097eb35ba230c42cc2410eb67fb2ce910782d2c2863AliceIdentity Registry 0x8004a818…mint agentId 67240x1
Metadata / service offer0x19055e9680e6f6072e0783310364e80c72f89c9836e4700ad78f841894a010c4AliceIdentity RegistrysetMetadata0x1
Settlement transfer0x81f86c63992089802beba5fad18525f5bcd2509bcdc95913958c310df400f455Bob 0x57b0eF…Alice 0xC0De03…OBOL 0.001 (Permit2)0x1

Runtime Evidence

QA environment:

ItemValue
OS / archmacOS (darwin) / arm64
Backendk3d (rancher/k3s v1.35.1-k3s1)
Tool versionskubectl 1.35.3, helm 3.20.1, helmfile 1.4.3, k3d 5.8.3
QA agent/modelHermes via LiteLLM → vLLM qwen36-deep (27B-class)

Images:

ComponentImageTag / digestSource
serviceoffer-controller (release pin)ghcr.io/obolnetwork/serviceoffer-controller503016b@sha256:bec62ea0…121957rc9 (this PR's repin)
serviceoffer-controller/buyer/verifier (smoke)ghcr.io/obolnetwork/…:latestbuilt from this branch (dev mode)

Kubernetes / stack:

ItemValue
Stack IDsper-run default + alice/bob (petnames)
Namespaceshermes-obol-agent, llm, x402, erpc, traefik, agent-*
Pod readinessall core pods Running (per flow checks)
Cleanup resultstacks torn down by release-smoke cleanup trap

Model and routing:

ItemValue
Agent/model usedqwen36-deep (vLLM, enable_thinking=false)
LiteLLM routecustom endpoint → host-reachable vLLM; paid/* → x402-buyer sidecar
Paid endpoint statuspaid/qwen3.5:9b Ready (5 auths loaded)
Auth token sourceobol agent auth (LiteLLM master key for upstream)

Artifacts and logs:

ArtifactLocation / linkNotes
Release report.tmp/release-smoke-20260603-130613/RELEASE_REPORT.mdrun6 per-flow table
flow-14 receipts.tmp/release-smoke-20260603-130613/flow-14-receiptsrun6 registration + settlement JSON

Demo readiness:

ItemStatusNotes
Seller visible / registeredERC-8004 agentId on Base Sepolia
Buyer discovery works402 → probe → pre-sign → PurchaseRequest
Paid route workspaid/* → 200
Settlement visible on-chainOBOL Transfer, status 0x1

Review Notes

Known gaps:

Follow-ups:

Reviewer focus:

  • internal/embed/infrastructure/base/templates/x402.yaml controller pin + embed_crd_test.go
    tripwire (no RBAC widened).
  • internal/x402/buyer/signer.go expiry filter (USDC validBefore=2106 never dropped).
  • internal/serviceoffercontroller/purchase.go not-found drain case (transient errors still requeue).

bussyjd added 7 commits June 3, 2026 10:24
…et create-only
The pinned serviceoffer-controller image (f5d94fc) was a side-branch build that
predated the change making Secret create-only in the reconciler. The tightened
ClusterRole grants no secrets update/patch verb, so the deployed binary 403s
when it Updates the per-agent hermes-api-server / remote-signer-keystore Secrets
on re-reconcile, and per-agent provisioning never converges.
Repin to 503016b@sha256:bec62ea0 (rc9 commit 503016b, image 0.10.0-rc9), whose
reconciler treats Secret as create-only and matches the shipped RBAC. Add a
tripwire test mirroring the x402-verifier one so a future downgrade can't
silently re-ship the bug. The short-SHA tag keeps the dev-mode :latest rewrite
and production pin invariants intact.
HoldSign popped s.auths[0] with no deadline check. A pre-signed Permit2 (OBOL)
batch shares one ~5-min deadline, so once expired the buyer served the whole
batch auth-by-auth, each returning 503 invalid_payment_expired from the
verifier before reaching a fresh auth. Add authDeadlineUnix (covering the
Permit2 deadline, nested ERC-3009 validBefore, and legacy flat field) and skip
expired auths at pick time. USDC vouchers use a year-2106 validBefore and are
never dropped.
… is gone
reconcileDeletingPurchase routed the 'not found in sidecar status' error into
the Configured&&Remaining>0 branch, which kept Remaining>0 and requeued every
5s forever, stranding the PurchaseRequest in Terminating until its finalizer was
force-removed. That signal means the sidecar has nothing left to drain. Add a
case (via isSidecarUpstreamGone) that collapses Remaining to 0 so cleanup and
finalizer removal proceed, consistent with the terminal not-found check already
present later in the function. Transient errors still requeue.
buy.py status/list showed the raw sidecar 'remaining' count, so an all-expired
Permit2 auth pool read as ready to spend. Add _auth_deadline / _count_valid_auths
and surface expired auths in both commands so an operator or agent tops up
instead of burning expired vouchers into 503s.
… path
HandleProxy (and the standalone inference gateway) rebuild the ForwardAuth
middleware per request with VerifyOnly=false by design — they proxy to the real
upstream and settle only after a <400 response — so the verifyOnly=false warning
fired on every paid request telling operators to 'fix' correct config. Add a
SettlesInProcess flag that suppresses the warning on those paths while leaving
the genuinely-dangerous Traefik ForwardAuth path loud.
obol agent new --model X provisioned cleanly for an unknown model, then every
chat call failed with 'no healthy deployments for this model'. Add a preflight
in createCRDAgent that checks a non-empty --model against the LiteLLM registry
and fails fast with the available models. A transient list error warns and
continues; an empty model still lets the controller auto-pin.
…races
Docker Desktop on macOS intermittently fails to create the gRPC-FUSE mount
source for a k3d node's workspace data dir under sustained cluster-churn
("error while creating mount source path ...: no such file or directory"),
so the k3s node never reports ready and k3d rolls the cluster back. The host
dir exists; it's a daemon-side file-sharing race. The dual-stack stack-up loop
already retries port-bind and image/Helm transients — extend it to retry this
mount race (a fresh cluster on retry clears it) so the release smoke isn't
flaked by an environment-side Docker hiccup.
@OisinKyne
OisinKyne merged commit a2742da into mainJun 3, 2026
9 checks passed
@OisinKyne
OisinKyne deleted the fix/rc9-ga-blockers branch June 3, 2026 15:01
Sign up for freeto 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

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report - #583

Merged
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers
Jun 3, 2026
Merged

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report#583
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

What changed: Fixes the v0.10.0-rc9 upgrade-report issues. Each was validated against rc9
source (adversarially re-checked) before fixing.

  • Bring obolup code to this repo #1 (GA blocker) — repin serviceoffer-controller from the f5d94fc side-branch build
    (which predated the Secret-create-only reconciler change) to 503016b@sha256:bec62ea0
    (rc9 commit 503016bf, image 0.10.0-rc9). The old pin Updates per-agent Secrets, which
    the tightened RBAC (no secrets:update/patch) 403s → per-agent provisioning never converges.
    Added a tripwire test.
  • User facing ingress #3x402-buyerHoldSign now drops expired pre-signed auths before signing
    (Permit2 deadline / ERC-3009 validBefore), ending the 503 invalid_payment_expired cascade.
  • WIP: Working towards a high quality rpc. #4buy.py status/list count is expiry-aware (valid vs expired auths).
  • Specify flag passing to obolup (or obol) #5reconcileDeletingPurchase finalizes the delete-drain on the not found in sidecar status signal instead of requeueing forever (stranded Terminating).
  • P1 — suppress the per-request verifyOnly=false warning on the in-process settle path
    (HandleProxy / obol sell inference); the Traefik ForwardAuth path still warns.
  • P2obol agent new --model X validates X against the LiteLLM registry; fails fast.

Issue #2 (master-Hermes PVC ownership on k3d local-path) is already fixed on main
(eb985bd/671c8ac root-chown init container); this branch inherits it. Per-agent Hermes was
confirmed not a residual (it seeds via Secret, not host PVC writes).

Why it matters: #1 and #2 are GA blockers from the rc9 report; the rest are buy-side
correctness / UX fixes on the monetize path.

Risk level: low — the controller fix is a pin bump to an already-published rc9 image; the rest
are narrow, regression-tested behaviour changes. No RBAC widened, no security surface added.

Commit under test: 8cfd0ca1 (live-chain evidence captured at b90118fd; 8cfd0ca1
adds only the dual-stack flows retry, no runtime behaviour change)

Base branch: main

Scope

  • Code
  • Charts / manifests
  • Flows / QA scripts
  • Docs / skills
  • Images / dependencies
  • Other:

Validation

CI checks:

CheckStatusLink
(added on push)pending

Unit tests:

go test ./... → ok (33 packages), gofmt + go vet clean
new regression tests (all PASS, #3/P1 fail without the fix):
internal/x402/buyer TestPreSignedSigner_DropsExpiredAuths, TestAuthDeadlineUnix (#3)
internal/serviceoffercontroller TestIsSidecarUpstreamGone (#5)
internal/x402 TestForwardAuth_SettlesInProcess_SuppressesWarning (P1)
cmd/obol TestIsModelConfigured (P2)
internal/embed TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix (#1)
+ dev-rewrite/production-pin guards updated for the new pin
commit: b90118fd

Integration tests:

n/a — covered by the release smoke below.

Flow tests (best result per flow across 3 full smoke runs on local darwin/arm64, k3d):

FlowNetworkResultEvidence
flow-01..10 (single stack incl. buy+lifecycle)base-sepolia (anvil)PASSrun5 + run6
flow-11 dual-stack (USDC)base-sepolia (anvil)PASSrun6
flow-13 dual-stack-obol (anvil fork)base-sepolia (anvil fork)PASSrun5
flow-14 live-obolbase-sepolia (LIVE)PASSrun5 + run6 — on-chain receipts below

Release smoke:

RELEASE_SMOKE_INCLUDE_OBOL=true RELEASE_SMOKE_INCLUDE_OBOL_FORK=true \
OBOL_LLM_ENDPOINT=http://<spark>:8000/v1 OBOL_LLM_MODEL=qwen36-deep \
bash flows/release-smoke.sh
LLM routed through vLLM qwen36-deep (DGX Spark over tailscale).
Every flow passed; no single run reached 13/13 because this macOS/Docker-Desktop +
cloudflare-quick-tunnel box hit a DIFFERENT environment-side transient each pass
(none code-related):
run5: 12/13 — flow-11 Docker Desktop gRPC-FUSE mount race on Alice cluster create
run6: 12/13 — flow-13 same mount race on Bob cluster create
run7: — flow-07/08 cloudflare quick-tunnel failed to establish (local 402 gate PASS)
The mount race now has a retry (commit 8cfd0ca1). The tunnel flake is external (trycloudflare).
#1 pin bump validated separately on a live cluster: deploying 503016b@sha256:bec62ea0 made a
per-agent `obol agent new` reach Ready with no 403; the prior f5d94fc image reproduces the 403.
Dev-mode rebuilds controller/buyer/verifier from this branch's source, so the smoke exercises
the source fixes directly.

Live Chain Evidence

Network: Base Sepolia (eip155:84532)

RPC/provider: paid drpc load-balancer (redacted)

Facilitator: https://x402.gcp.obol.tech (prometheus-overlay)

Contracts and tokens:

NameAddressVersion / notes
OBOL token0x0a09371a8b011d5110656ceBCc70603e53FD2c78Obol Network / OBOL / 18 dp, Permit2
ERC-8004 Identity Registry0x8004a818bfb912233c491871b3d84c89a494bd9emint = agentId

Wallet roles:

RoleAddressSource
Alice / seller / register0xC0De030F6C37f490594F93fB99e2756703c4297Eseller payTo + funded EOA
Bob / buyer / payer0x57b0eF875DeB5A37301F1640E469a2129Da9490Edeterministic 2nd-derived from REMOTE_SIGNER_PRIVATE_KEY; bobSigner == BOB_WALLET ✓

Balances:

TokenAddressBeforeAfterExpected deltaActual delta
OBOL (Bob)0x0a09371a…4949000000000000000 wei4948000000000000000 wei-1000000000000000 (0.001 OBOL)-1000000000000000 ✓ exact
OBOL (Alice)0x0a09371a…+1000000000000000 (0.001 OBOL)+1000000000000000 ✓ exact

Transaction receipts:

PurposeTx hashFromToAmount / eventStatus
ERC-8004 registration0xff4cdbbdeea75e578728f097eb35ba230c42cc2410eb67fb2ce910782d2c2863AliceIdentity Registry 0x8004a818…mint agentId 67240x1
Metadata / service offer0x19055e9680e6f6072e0783310364e80c72f89c9836e4700ad78f841894a010c4AliceIdentity RegistrysetMetadata0x1
Settlement transfer0x81f86c63992089802beba5fad18525f5bcd2509bcdc95913958c310df400f455Bob 0x57b0eF…Alice 0xC0De03…OBOL 0.001 (Permit2)0x1

Runtime Evidence

QA environment:

ItemValue
OS / archmacOS (darwin) / arm64
Backendk3d (rancher/k3s v1.35.1-k3s1)
Tool versionskubectl 1.35.3, helm 3.20.1, helmfile 1.4.3, k3d 5.8.3
QA agent/modelHermes via LiteLLM → vLLM qwen36-deep (27B-class)

Images:

ComponentImageTag / digestSource
serviceoffer-controller (release pin)ghcr.io/obolnetwork/serviceoffer-controller503016b@sha256:bec62ea0…121957rc9 (this PR's repin)
serviceoffer-controller/buyer/verifier (smoke)ghcr.io/obolnetwork/…:latestbuilt from this branch (dev mode)

Kubernetes / stack:

ItemValue
Stack IDsper-run default + alice/bob (petnames)
Namespaceshermes-obol-agent, llm, x402, erpc, traefik, agent-*
Pod readinessall core pods Running (per flow checks)
Cleanup resultstacks torn down by release-smoke cleanup trap

Model and routing:

ItemValue
Agent/model usedqwen36-deep (vLLM, enable_thinking=false)
LiteLLM routecustom endpoint → host-reachable vLLM; paid/* → x402-buyer sidecar
Paid endpoint statuspaid/qwen3.5:9b Ready (5 auths loaded)
Auth token sourceobol agent auth (LiteLLM master key for upstream)

Artifacts and logs:

ArtifactLocation / linkNotes
Release report.tmp/release-smoke-20260603-130613/RELEASE_REPORT.mdrun6 per-flow table
flow-14 receipts.tmp/release-smoke-20260603-130613/flow-14-receiptsrun6 registration + settlement JSON

Demo readiness:

ItemStatusNotes
Seller visible / registeredERC-8004 agentId on Base Sepolia
Buyer discovery works402 → probe → pre-sign → PurchaseRequest
Paid route workspaid/* → 200
Settlement visible on-chainOBOL Transfer, status 0x1

Review Notes

Known gaps:

Follow-ups:

Reviewer focus:

  • internal/embed/infrastructure/base/templates/x402.yaml controller pin + embed_crd_test.go
    tripwire (no RBAC widened).
  • internal/x402/buyer/signer.go expiry filter (USDC validBefore=2106 never dropped).
  • internal/serviceoffercontroller/purchase.go not-found drain case (transient errors still requeue).

bussyjd added 7 commits June 3, 2026 10:24
…et create-only
The pinned serviceoffer-controller image (f5d94fc) was a side-branch build that
predated the change making Secret create-only in the reconciler. The tightened
ClusterRole grants no secrets update/patch verb, so the deployed binary 403s
when it Updates the per-agent hermes-api-server / remote-signer-keystore Secrets
on re-reconcile, and per-agent provisioning never converges.
Repin to 503016b@sha256:bec62ea0 (rc9 commit 503016b, image 0.10.0-rc9), whose
reconciler treats Secret as create-only and matches the shipped RBAC. Add a
tripwire test mirroring the x402-verifier one so a future downgrade can't
silently re-ship the bug. The short-SHA tag keeps the dev-mode :latest rewrite
and production pin invariants intact.
HoldSign popped s.auths[0] with no deadline check. A pre-signed Permit2 (OBOL)
batch shares one ~5-min deadline, so once expired the buyer served the whole
batch auth-by-auth, each returning 503 invalid_payment_expired from the
verifier before reaching a fresh auth. Add authDeadlineUnix (covering the
Permit2 deadline, nested ERC-3009 validBefore, and legacy flat field) and skip
expired auths at pick time. USDC vouchers use a year-2106 validBefore and are
never dropped.
… is gone
reconcileDeletingPurchase routed the 'not found in sidecar status' error into
the Configured&&Remaining>0 branch, which kept Remaining>0 and requeued every
5s forever, stranding the PurchaseRequest in Terminating until its finalizer was
force-removed. That signal means the sidecar has nothing left to drain. Add a
case (via isSidecarUpstreamGone) that collapses Remaining to 0 so cleanup and
finalizer removal proceed, consistent with the terminal not-found check already
present later in the function. Transient errors still requeue.
buy.py status/list showed the raw sidecar 'remaining' count, so an all-expired
Permit2 auth pool read as ready to spend. Add _auth_deadline / _count_valid_auths
and surface expired auths in both commands so an operator or agent tops up
instead of burning expired vouchers into 503s.
… path
HandleProxy (and the standalone inference gateway) rebuild the ForwardAuth
middleware per request with VerifyOnly=false by design — they proxy to the real
upstream and settle only after a <400 response — so the verifyOnly=false warning
fired on every paid request telling operators to 'fix' correct config. Add a
SettlesInProcess flag that suppresses the warning on those paths while leaving
the genuinely-dangerous Traefik ForwardAuth path loud.
obol agent new --model X provisioned cleanly for an unknown model, then every
chat call failed with 'no healthy deployments for this model'. Add a preflight
in createCRDAgent that checks a non-empty --model against the LiteLLM registry
and fails fast with the available models. A transient list error warns and
continues; an empty model still lets the controller auto-pin.
…races
Docker Desktop on macOS intermittently fails to create the gRPC-FUSE mount
source for a k3d node's workspace data dir under sustained cluster-churn
("error while creating mount source path ...: no such file or directory"),
so the k3s node never reports ready and k3d rolls the cluster back. The host
dir exists; it's a daemon-side file-sharing race. The dual-stack stack-up loop
already retries port-bind and image/Helm transients — extend it to retry this
mount race (a fresh cluster on retry clears it) so the release smoke isn't
flaked by an environment-side Docker hiccup.
@OisinKyne
OisinKyne merged commit a2742da into mainJun 3, 2026
9 checks passed
@OisinKyne
OisinKyne deleted the fix/rc9-ga-blockers branch June 3, 2026 15:01
Sign up for freeto 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

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report - #583

Merged
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers
Jun 3, 2026
Merged

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report#583
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

What changed: Fixes the v0.10.0-rc9 upgrade-report issues. Each was validated against rc9
source (adversarially re-checked) before fixing.

  • Bring obolup code to this repo #1 (GA blocker) — repin serviceoffer-controller from the f5d94fc side-branch build
    (which predated the Secret-create-only reconciler change) to 503016b@sha256:bec62ea0
    (rc9 commit 503016bf, image 0.10.0-rc9). The old pin Updates per-agent Secrets, which
    the tightened RBAC (no secrets:update/patch) 403s → per-agent provisioning never converges.
    Added a tripwire test.
  • User facing ingress #3x402-buyerHoldSign now drops expired pre-signed auths before signing
    (Permit2 deadline / ERC-3009 validBefore), ending the 503 invalid_payment_expired cascade.
  • WIP: Working towards a high quality rpc. #4buy.py status/list count is expiry-aware (valid vs expired auths).
  • Specify flag passing to obolup (or obol) #5reconcileDeletingPurchase finalizes the delete-drain on the not found in sidecar status signal instead of requeueing forever (stranded Terminating).
  • P1 — suppress the per-request verifyOnly=false warning on the in-process settle path
    (HandleProxy / obol sell inference); the Traefik ForwardAuth path still warns.
  • P2obol agent new --model X validates X against the LiteLLM registry; fails fast.

Issue #2 (master-Hermes PVC ownership on k3d local-path) is already fixed on main
(eb985bd/671c8ac root-chown init container); this branch inherits it. Per-agent Hermes was
confirmed not a residual (it seeds via Secret, not host PVC writes).

Why it matters: #1 and #2 are GA blockers from the rc9 report; the rest are buy-side
correctness / UX fixes on the monetize path.

Risk level: low — the controller fix is a pin bump to an already-published rc9 image; the rest
are narrow, regression-tested behaviour changes. No RBAC widened, no security surface added.

Commit under test: 8cfd0ca1 (live-chain evidence captured at b90118fd; 8cfd0ca1
adds only the dual-stack flows retry, no runtime behaviour change)

Base branch: main

Scope

  • Code
  • Charts / manifests
  • Flows / QA scripts
  • Docs / skills
  • Images / dependencies
  • Other:

Validation

CI checks:

CheckStatusLink
(added on push)pending

Unit tests:

go test ./... → ok (33 packages), gofmt + go vet clean
new regression tests (all PASS, #3/P1 fail without the fix):
internal/x402/buyer TestPreSignedSigner_DropsExpiredAuths, TestAuthDeadlineUnix (#3)
internal/serviceoffercontroller TestIsSidecarUpstreamGone (#5)
internal/x402 TestForwardAuth_SettlesInProcess_SuppressesWarning (P1)
cmd/obol TestIsModelConfigured (P2)
internal/embed TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix (#1)
+ dev-rewrite/production-pin guards updated for the new pin
commit: b90118fd

Integration tests:

n/a — covered by the release smoke below.

Flow tests (best result per flow across 3 full smoke runs on local darwin/arm64, k3d):

FlowNetworkResultEvidence
flow-01..10 (single stack incl. buy+lifecycle)base-sepolia (anvil)PASSrun5 + run6
flow-11 dual-stack (USDC)base-sepolia (anvil)PASSrun6
flow-13 dual-stack-obol (anvil fork)base-sepolia (anvil fork)PASSrun5
flow-14 live-obolbase-sepolia (LIVE)PASSrun5 + run6 — on-chain receipts below

Release smoke:

RELEASE_SMOKE_INCLUDE_OBOL=true RELEASE_SMOKE_INCLUDE_OBOL_FORK=true \
OBOL_LLM_ENDPOINT=http://<spark>:8000/v1 OBOL_LLM_MODEL=qwen36-deep \
bash flows/release-smoke.sh
LLM routed through vLLM qwen36-deep (DGX Spark over tailscale).
Every flow passed; no single run reached 13/13 because this macOS/Docker-Desktop +
cloudflare-quick-tunnel box hit a DIFFERENT environment-side transient each pass
(none code-related):
run5: 12/13 — flow-11 Docker Desktop gRPC-FUSE mount race on Alice cluster create
run6: 12/13 — flow-13 same mount race on Bob cluster create
run7: — flow-07/08 cloudflare quick-tunnel failed to establish (local 402 gate PASS)
The mount race now has a retry (commit 8cfd0ca1). The tunnel flake is external (trycloudflare).
#1 pin bump validated separately on a live cluster: deploying 503016b@sha256:bec62ea0 made a
per-agent `obol agent new` reach Ready with no 403; the prior f5d94fc image reproduces the 403.
Dev-mode rebuilds controller/buyer/verifier from this branch's source, so the smoke exercises
the source fixes directly.

Live Chain Evidence

Network: Base Sepolia (eip155:84532)

RPC/provider: paid drpc load-balancer (redacted)

Facilitator: https://x402.gcp.obol.tech (prometheus-overlay)

Contracts and tokens:

NameAddressVersion / notes
OBOL token0x0a09371a8b011d5110656ceBCc70603e53FD2c78Obol Network / OBOL / 18 dp, Permit2
ERC-8004 Identity Registry0x8004a818bfb912233c491871b3d84c89a494bd9emint = agentId

Wallet roles:

RoleAddressSource
Alice / seller / register0xC0De030F6C37f490594F93fB99e2756703c4297Eseller payTo + funded EOA
Bob / buyer / payer0x57b0eF875DeB5A37301F1640E469a2129Da9490Edeterministic 2nd-derived from REMOTE_SIGNER_PRIVATE_KEY; bobSigner == BOB_WALLET ✓

Balances:

TokenAddressBeforeAfterExpected deltaActual delta
OBOL (Bob)0x0a09371a…4949000000000000000 wei4948000000000000000 wei-1000000000000000 (0.001 OBOL)-1000000000000000 ✓ exact
OBOL (Alice)0x0a09371a…+1000000000000000 (0.001 OBOL)+1000000000000000 ✓ exact

Transaction receipts:

PurposeTx hashFromToAmount / eventStatus
ERC-8004 registration0xff4cdbbdeea75e578728f097eb35ba230c42cc2410eb67fb2ce910782d2c2863AliceIdentity Registry 0x8004a818…mint agentId 67240x1
Metadata / service offer0x19055e9680e6f6072e0783310364e80c72f89c9836e4700ad78f841894a010c4AliceIdentity RegistrysetMetadata0x1
Settlement transfer0x81f86c63992089802beba5fad18525f5bcd2509bcdc95913958c310df400f455Bob 0x57b0eF…Alice 0xC0De03…OBOL 0.001 (Permit2)0x1

Runtime Evidence

QA environment:

ItemValue
OS / archmacOS (darwin) / arm64
Backendk3d (rancher/k3s v1.35.1-k3s1)
Tool versionskubectl 1.35.3, helm 3.20.1, helmfile 1.4.3, k3d 5.8.3
QA agent/modelHermes via LiteLLM → vLLM qwen36-deep (27B-class)

Images:

ComponentImageTag / digestSource
serviceoffer-controller (release pin)ghcr.io/obolnetwork/serviceoffer-controller503016b@sha256:bec62ea0…121957rc9 (this PR's repin)
serviceoffer-controller/buyer/verifier (smoke)ghcr.io/obolnetwork/…:latestbuilt from this branch (dev mode)

Kubernetes / stack:

ItemValue
Stack IDsper-run default + alice/bob (petnames)
Namespaceshermes-obol-agent, llm, x402, erpc, traefik, agent-*
Pod readinessall core pods Running (per flow checks)
Cleanup resultstacks torn down by release-smoke cleanup trap

Model and routing:

ItemValue
Agent/model usedqwen36-deep (vLLM, enable_thinking=false)
LiteLLM routecustom endpoint → host-reachable vLLM; paid/* → x402-buyer sidecar
Paid endpoint statuspaid/qwen3.5:9b Ready (5 auths loaded)
Auth token sourceobol agent auth (LiteLLM master key for upstream)

Artifacts and logs:

ArtifactLocation / linkNotes
Release report.tmp/release-smoke-20260603-130613/RELEASE_REPORT.mdrun6 per-flow table
flow-14 receipts.tmp/release-smoke-20260603-130613/flow-14-receiptsrun6 registration + settlement JSON

Demo readiness:

ItemStatusNotes
Seller visible / registeredERC-8004 agentId on Base Sepolia
Buyer discovery works402 → probe → pre-sign → PurchaseRequest
Paid route workspaid/* → 200
Settlement visible on-chainOBOL Transfer, status 0x1

Review Notes

Known gaps:

Follow-ups:

Reviewer focus:

  • internal/embed/infrastructure/base/templates/x402.yaml controller pin + embed_crd_test.go
    tripwire (no RBAC widened).
  • internal/x402/buyer/signer.go expiry filter (USDC validBefore=2106 never dropped).
  • internal/serviceoffercontroller/purchase.go not-found drain case (transient errors still requeue).

bussyjd added 7 commits June 3, 2026 10:24
…et create-only
The pinned serviceoffer-controller image (f5d94fc) was a side-branch build that
predated the change making Secret create-only in the reconciler. The tightened
ClusterRole grants no secrets update/patch verb, so the deployed binary 403s
when it Updates the per-agent hermes-api-server / remote-signer-keystore Secrets
on re-reconcile, and per-agent provisioning never converges.
Repin to 503016b@sha256:bec62ea0 (rc9 commit 503016b, image 0.10.0-rc9), whose
reconciler treats Secret as create-only and matches the shipped RBAC. Add a
tripwire test mirroring the x402-verifier one so a future downgrade can't
silently re-ship the bug. The short-SHA tag keeps the dev-mode :latest rewrite
and production pin invariants intact.
HoldSign popped s.auths[0] with no deadline check. A pre-signed Permit2 (OBOL)
batch shares one ~5-min deadline, so once expired the buyer served the whole
batch auth-by-auth, each returning 503 invalid_payment_expired from the
verifier before reaching a fresh auth. Add authDeadlineUnix (covering the
Permit2 deadline, nested ERC-3009 validBefore, and legacy flat field) and skip
expired auths at pick time. USDC vouchers use a year-2106 validBefore and are
never dropped.
… is gone
reconcileDeletingPurchase routed the 'not found in sidecar status' error into
the Configured&&Remaining>0 branch, which kept Remaining>0 and requeued every
5s forever, stranding the PurchaseRequest in Terminating until its finalizer was
force-removed. That signal means the sidecar has nothing left to drain. Add a
case (via isSidecarUpstreamGone) that collapses Remaining to 0 so cleanup and
finalizer removal proceed, consistent with the terminal not-found check already
present later in the function. Transient errors still requeue.
buy.py status/list showed the raw sidecar 'remaining' count, so an all-expired
Permit2 auth pool read as ready to spend. Add _auth_deadline / _count_valid_auths
and surface expired auths in both commands so an operator or agent tops up
instead of burning expired vouchers into 503s.
… path
HandleProxy (and the standalone inference gateway) rebuild the ForwardAuth
middleware per request with VerifyOnly=false by design — they proxy to the real
upstream and settle only after a <400 response — so the verifyOnly=false warning
fired on every paid request telling operators to 'fix' correct config. Add a
SettlesInProcess flag that suppresses the warning on those paths while leaving
the genuinely-dangerous Traefik ForwardAuth path loud.
obol agent new --model X provisioned cleanly for an unknown model, then every
chat call failed with 'no healthy deployments for this model'. Add a preflight
in createCRDAgent that checks a non-empty --model against the LiteLLM registry
and fails fast with the available models. A transient list error warns and
continues; an empty model still lets the controller auto-pin.
…races
Docker Desktop on macOS intermittently fails to create the gRPC-FUSE mount
source for a k3d node's workspace data dir under sustained cluster-churn
("error while creating mount source path ...: no such file or directory"),
so the k3s node never reports ready and k3d rolls the cluster back. The host
dir exists; it's a daemon-side file-sharing race. The dual-stack stack-up loop
already retries port-bind and image/Helm transients — extend it to retry this
mount race (a fresh cluster on retry clears it) so the release smoke isn't
flaked by an environment-side Docker hiccup.
@OisinKyne
OisinKyne merged commit a2742da into mainJun 3, 2026
9 checks passed
@OisinKyne
OisinKyne deleted the fix/rc9-ga-blockers branch June 3, 2026 15:01
Sign up for freeto 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

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report - #583

Merged
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers
Jun 3, 2026
Merged

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report#583
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

What changed: Fixes the v0.10.0-rc9 upgrade-report issues. Each was validated against rc9
source (adversarially re-checked) before fixing.

  • Bring obolup code to this repo #1 (GA blocker) — repin serviceoffer-controller from the f5d94fc side-branch build
    (which predated the Secret-create-only reconciler change) to 503016b@sha256:bec62ea0
    (rc9 commit 503016bf, image 0.10.0-rc9). The old pin Updates per-agent Secrets, which
    the tightened RBAC (no secrets:update/patch) 403s → per-agent provisioning never converges.
    Added a tripwire test.
  • User facing ingress #3x402-buyerHoldSign now drops expired pre-signed auths before signing
    (Permit2 deadline / ERC-3009 validBefore), ending the 503 invalid_payment_expired cascade.
  • WIP: Working towards a high quality rpc. #4buy.py status/list count is expiry-aware (valid vs expired auths).
  • Specify flag passing to obolup (or obol) #5reconcileDeletingPurchase finalizes the delete-drain on the not found in sidecar status signal instead of requeueing forever (stranded Terminating).
  • P1 — suppress the per-request verifyOnly=false warning on the in-process settle path
    (HandleProxy / obol sell inference); the Traefik ForwardAuth path still warns.
  • P2obol agent new --model X validates X against the LiteLLM registry; fails fast.

Issue #2 (master-Hermes PVC ownership on k3d local-path) is already fixed on main
(eb985bd/671c8ac root-chown init container); this branch inherits it. Per-agent Hermes was
confirmed not a residual (it seeds via Secret, not host PVC writes).

Why it matters: #1 and #2 are GA blockers from the rc9 report; the rest are buy-side
correctness / UX fixes on the monetize path.

Risk level: low — the controller fix is a pin bump to an already-published rc9 image; the rest
are narrow, regression-tested behaviour changes. No RBAC widened, no security surface added.

Commit under test: 8cfd0ca1 (live-chain evidence captured at b90118fd; 8cfd0ca1
adds only the dual-stack flows retry, no runtime behaviour change)

Base branch: main

Scope

  • Code
  • Charts / manifests
  • Flows / QA scripts
  • Docs / skills
  • Images / dependencies
  • Other:

Validation

CI checks:

CheckStatusLink
(added on push)pending

Unit tests:

go test ./... → ok (33 packages), gofmt + go vet clean
new regression tests (all PASS, #3/P1 fail without the fix):
internal/x402/buyer TestPreSignedSigner_DropsExpiredAuths, TestAuthDeadlineUnix (#3)
internal/serviceoffercontroller TestIsSidecarUpstreamGone (#5)
internal/x402 TestForwardAuth_SettlesInProcess_SuppressesWarning (P1)
cmd/obol TestIsModelConfigured (P2)
internal/embed TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix (#1)
+ dev-rewrite/production-pin guards updated for the new pin
commit: b90118fd

Integration tests:

n/a — covered by the release smoke below.

Flow tests (best result per flow across 3 full smoke runs on local darwin/arm64, k3d):

FlowNetworkResultEvidence
flow-01..10 (single stack incl. buy+lifecycle)base-sepolia (anvil)PASSrun5 + run6
flow-11 dual-stack (USDC)base-sepolia (anvil)PASSrun6
flow-13 dual-stack-obol (anvil fork)base-sepolia (anvil fork)PASSrun5
flow-14 live-obolbase-sepolia (LIVE)PASSrun5 + run6 — on-chain receipts below

Release smoke:

RELEASE_SMOKE_INCLUDE_OBOL=true RELEASE_SMOKE_INCLUDE_OBOL_FORK=true \
OBOL_LLM_ENDPOINT=http://<spark>:8000/v1 OBOL_LLM_MODEL=qwen36-deep \
bash flows/release-smoke.sh
LLM routed through vLLM qwen36-deep (DGX Spark over tailscale).
Every flow passed; no single run reached 13/13 because this macOS/Docker-Desktop +
cloudflare-quick-tunnel box hit a DIFFERENT environment-side transient each pass
(none code-related):
run5: 12/13 — flow-11 Docker Desktop gRPC-FUSE mount race on Alice cluster create
run6: 12/13 — flow-13 same mount race on Bob cluster create
run7: — flow-07/08 cloudflare quick-tunnel failed to establish (local 402 gate PASS)
The mount race now has a retry (commit 8cfd0ca1). The tunnel flake is external (trycloudflare).
#1 pin bump validated separately on a live cluster: deploying 503016b@sha256:bec62ea0 made a
per-agent `obol agent new` reach Ready with no 403; the prior f5d94fc image reproduces the 403.
Dev-mode rebuilds controller/buyer/verifier from this branch's source, so the smoke exercises
the source fixes directly.

Live Chain Evidence

Network: Base Sepolia (eip155:84532)

RPC/provider: paid drpc load-balancer (redacted)

Facilitator: https://x402.gcp.obol.tech (prometheus-overlay)

Contracts and tokens:

NameAddressVersion / notes
OBOL token0x0a09371a8b011d5110656ceBCc70603e53FD2c78Obol Network / OBOL / 18 dp, Permit2
ERC-8004 Identity Registry0x8004a818bfb912233c491871b3d84c89a494bd9emint = agentId

Wallet roles:

RoleAddressSource
Alice / seller / register0xC0De030F6C37f490594F93fB99e2756703c4297Eseller payTo + funded EOA
Bob / buyer / payer0x57b0eF875DeB5A37301F1640E469a2129Da9490Edeterministic 2nd-derived from REMOTE_SIGNER_PRIVATE_KEY; bobSigner == BOB_WALLET ✓

Balances:

TokenAddressBeforeAfterExpected deltaActual delta
OBOL (Bob)0x0a09371a…4949000000000000000 wei4948000000000000000 wei-1000000000000000 (0.001 OBOL)-1000000000000000 ✓ exact
OBOL (Alice)0x0a09371a…+1000000000000000 (0.001 OBOL)+1000000000000000 ✓ exact

Transaction receipts:

PurposeTx hashFromToAmount / eventStatus
ERC-8004 registration0xff4cdbbdeea75e578728f097eb35ba230c42cc2410eb67fb2ce910782d2c2863AliceIdentity Registry 0x8004a818…mint agentId 67240x1
Metadata / service offer0x19055e9680e6f6072e0783310364e80c72f89c9836e4700ad78f841894a010c4AliceIdentity RegistrysetMetadata0x1
Settlement transfer0x81f86c63992089802beba5fad18525f5bcd2509bcdc95913958c310df400f455Bob 0x57b0eF…Alice 0xC0De03…OBOL 0.001 (Permit2)0x1

Runtime Evidence

QA environment:

ItemValue
OS / archmacOS (darwin) / arm64
Backendk3d (rancher/k3s v1.35.1-k3s1)
Tool versionskubectl 1.35.3, helm 3.20.1, helmfile 1.4.3, k3d 5.8.3
QA agent/modelHermes via LiteLLM → vLLM qwen36-deep (27B-class)

Images:

ComponentImageTag / digestSource
serviceoffer-controller (release pin)ghcr.io/obolnetwork/serviceoffer-controller503016b@sha256:bec62ea0…121957rc9 (this PR's repin)
serviceoffer-controller/buyer/verifier (smoke)ghcr.io/obolnetwork/…:latestbuilt from this branch (dev mode)

Kubernetes / stack:

ItemValue
Stack IDsper-run default + alice/bob (petnames)
Namespaceshermes-obol-agent, llm, x402, erpc, traefik, agent-*
Pod readinessall core pods Running (per flow checks)
Cleanup resultstacks torn down by release-smoke cleanup trap

Model and routing:

ItemValue
Agent/model usedqwen36-deep (vLLM, enable_thinking=false)
LiteLLM routecustom endpoint → host-reachable vLLM; paid/* → x402-buyer sidecar
Paid endpoint statuspaid/qwen3.5:9b Ready (5 auths loaded)
Auth token sourceobol agent auth (LiteLLM master key for upstream)

Artifacts and logs:

ArtifactLocation / linkNotes
Release report.tmp/release-smoke-20260603-130613/RELEASE_REPORT.mdrun6 per-flow table
flow-14 receipts.tmp/release-smoke-20260603-130613/flow-14-receiptsrun6 registration + settlement JSON

Demo readiness:

ItemStatusNotes
Seller visible / registeredERC-8004 agentId on Base Sepolia
Buyer discovery works402 → probe → pre-sign → PurchaseRequest
Paid route workspaid/* → 200
Settlement visible on-chainOBOL Transfer, status 0x1

Review Notes

Known gaps:

Follow-ups:

Reviewer focus:

  • internal/embed/infrastructure/base/templates/x402.yaml controller pin + embed_crd_test.go
    tripwire (no RBAC widened).
  • internal/x402/buyer/signer.go expiry filter (USDC validBefore=2106 never dropped).
  • internal/serviceoffercontroller/purchase.go not-found drain case (transient errors still requeue).

bussyjd added 7 commits June 3, 2026 10:24
…et create-only
The pinned serviceoffer-controller image (f5d94fc) was a side-branch build that
predated the change making Secret create-only in the reconciler. The tightened
ClusterRole grants no secrets update/patch verb, so the deployed binary 403s
when it Updates the per-agent hermes-api-server / remote-signer-keystore Secrets
on re-reconcile, and per-agent provisioning never converges.
Repin to 503016b@sha256:bec62ea0 (rc9 commit 503016b, image 0.10.0-rc9), whose
reconciler treats Secret as create-only and matches the shipped RBAC. Add a
tripwire test mirroring the x402-verifier one so a future downgrade can't
silently re-ship the bug. The short-SHA tag keeps the dev-mode :latest rewrite
and production pin invariants intact.
HoldSign popped s.auths[0] with no deadline check. A pre-signed Permit2 (OBOL)
batch shares one ~5-min deadline, so once expired the buyer served the whole
batch auth-by-auth, each returning 503 invalid_payment_expired from the
verifier before reaching a fresh auth. Add authDeadlineUnix (covering the
Permit2 deadline, nested ERC-3009 validBefore, and legacy flat field) and skip
expired auths at pick time. USDC vouchers use a year-2106 validBefore and are
never dropped.
… is gone
reconcileDeletingPurchase routed the 'not found in sidecar status' error into
the Configured&&Remaining>0 branch, which kept Remaining>0 and requeued every
5s forever, stranding the PurchaseRequest in Terminating until its finalizer was
force-removed. That signal means the sidecar has nothing left to drain. Add a
case (via isSidecarUpstreamGone) that collapses Remaining to 0 so cleanup and
finalizer removal proceed, consistent with the terminal not-found check already
present later in the function. Transient errors still requeue.
buy.py status/list showed the raw sidecar 'remaining' count, so an all-expired
Permit2 auth pool read as ready to spend. Add _auth_deadline / _count_valid_auths
and surface expired auths in both commands so an operator or agent tops up
instead of burning expired vouchers into 503s.
… path
HandleProxy (and the standalone inference gateway) rebuild the ForwardAuth
middleware per request with VerifyOnly=false by design — they proxy to the real
upstream and settle only after a <400 response — so the verifyOnly=false warning
fired on every paid request telling operators to 'fix' correct config. Add a
SettlesInProcess flag that suppresses the warning on those paths while leaving
the genuinely-dangerous Traefik ForwardAuth path loud.
obol agent new --model X provisioned cleanly for an unknown model, then every
chat call failed with 'no healthy deployments for this model'. Add a preflight
in createCRDAgent that checks a non-empty --model against the LiteLLM registry
and fails fast with the available models. A transient list error warns and
continues; an empty model still lets the controller auto-pin.
…races
Docker Desktop on macOS intermittently fails to create the gRPC-FUSE mount
source for a k3d node's workspace data dir under sustained cluster-churn
("error while creating mount source path ...: no such file or directory"),
so the k3s node never reports ready and k3d rolls the cluster back. The host
dir exists; it's a daemon-side file-sharing race. The dual-stack stack-up loop
already retries port-bind and image/Helm transients — extend it to retry this
mount race (a fresh cluster on retry clears it) so the release smoke isn't
flaked by an environment-side Docker hiccup.
@OisinKyne
OisinKyne merged commit a2742da into mainJun 3, 2026
9 checks passed
@OisinKyne
OisinKyne deleted the fix/rc9-ga-blockers branch June 3, 2026 15:01
Sign up for freeto 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

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report - #583

Merged
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers
Jun 3, 2026
Merged

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report#583
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

What changed: Fixes the v0.10.0-rc9 upgrade-report issues. Each was validated against rc9
source (adversarially re-checked) before fixing.

  • Bring obolup code to this repo #1 (GA blocker) — repin serviceoffer-controller from the f5d94fc side-branch build
    (which predated the Secret-create-only reconciler change) to 503016b@sha256:bec62ea0
    (rc9 commit 503016bf, image 0.10.0-rc9). The old pin Updates per-agent Secrets, which
    the tightened RBAC (no secrets:update/patch) 403s → per-agent provisioning never converges.
    Added a tripwire test.
  • User facing ingress #3x402-buyerHoldSign now drops expired pre-signed auths before signing
    (Permit2 deadline / ERC-3009 validBefore), ending the 503 invalid_payment_expired cascade.
  • WIP: Working towards a high quality rpc. #4buy.py status/list count is expiry-aware (valid vs expired auths).
  • Specify flag passing to obolup (or obol) #5reconcileDeletingPurchase finalizes the delete-drain on the not found in sidecar status signal instead of requeueing forever (stranded Terminating).
  • P1 — suppress the per-request verifyOnly=false warning on the in-process settle path
    (HandleProxy / obol sell inference); the Traefik ForwardAuth path still warns.
  • P2obol agent new --model X validates X against the LiteLLM registry; fails fast.

Issue #2 (master-Hermes PVC ownership on k3d local-path) is already fixed on main
(eb985bd/671c8ac root-chown init container); this branch inherits it. Per-agent Hermes was
confirmed not a residual (it seeds via Secret, not host PVC writes).

Why it matters: #1 and #2 are GA blockers from the rc9 report; the rest are buy-side
correctness / UX fixes on the monetize path.

Risk level: low — the controller fix is a pin bump to an already-published rc9 image; the rest
are narrow, regression-tested behaviour changes. No RBAC widened, no security surface added.

Commit under test: 8cfd0ca1 (live-chain evidence captured at b90118fd; 8cfd0ca1
adds only the dual-stack flows retry, no runtime behaviour change)

Base branch: main

Scope

  • Code
  • Charts / manifests
  • Flows / QA scripts
  • Docs / skills
  • Images / dependencies
  • Other:

Validation

CI checks:

CheckStatusLink
(added on push)pending

Unit tests:

go test ./... → ok (33 packages), gofmt + go vet clean
new regression tests (all PASS, #3/P1 fail without the fix):
internal/x402/buyer TestPreSignedSigner_DropsExpiredAuths, TestAuthDeadlineUnix (#3)
internal/serviceoffercontroller TestIsSidecarUpstreamGone (#5)
internal/x402 TestForwardAuth_SettlesInProcess_SuppressesWarning (P1)
cmd/obol TestIsModelConfigured (P2)
internal/embed TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix (#1)
+ dev-rewrite/production-pin guards updated for the new pin
commit: b90118fd

Integration tests:

n/a — covered by the release smoke below.

Flow tests (best result per flow across 3 full smoke runs on local darwin/arm64, k3d):

FlowNetworkResultEvidence
flow-01..10 (single stack incl. buy+lifecycle)base-sepolia (anvil)PASSrun5 + run6
flow-11 dual-stack (USDC)base-sepolia (anvil)PASSrun6
flow-13 dual-stack-obol (anvil fork)base-sepolia (anvil fork)PASSrun5
flow-14 live-obolbase-sepolia (LIVE)PASSrun5 + run6 — on-chain receipts below

Release smoke:

RELEASE_SMOKE_INCLUDE_OBOL=true RELEASE_SMOKE_INCLUDE_OBOL_FORK=true \
OBOL_LLM_ENDPOINT=http://<spark>:8000/v1 OBOL_LLM_MODEL=qwen36-deep \
bash flows/release-smoke.sh
LLM routed through vLLM qwen36-deep (DGX Spark over tailscale).
Every flow passed; no single run reached 13/13 because this macOS/Docker-Desktop +
cloudflare-quick-tunnel box hit a DIFFERENT environment-side transient each pass
(none code-related):
run5: 12/13 — flow-11 Docker Desktop gRPC-FUSE mount race on Alice cluster create
run6: 12/13 — flow-13 same mount race on Bob cluster create
run7: — flow-07/08 cloudflare quick-tunnel failed to establish (local 402 gate PASS)
The mount race now has a retry (commit 8cfd0ca1). The tunnel flake is external (trycloudflare).
#1 pin bump validated separately on a live cluster: deploying 503016b@sha256:bec62ea0 made a
per-agent `obol agent new` reach Ready with no 403; the prior f5d94fc image reproduces the 403.
Dev-mode rebuilds controller/buyer/verifier from this branch's source, so the smoke exercises
the source fixes directly.

Live Chain Evidence

Network: Base Sepolia (eip155:84532)

RPC/provider: paid drpc load-balancer (redacted)

Facilitator: https://x402.gcp.obol.tech (prometheus-overlay)

Contracts and tokens:

NameAddressVersion / notes
OBOL token0x0a09371a8b011d5110656ceBCc70603e53FD2c78Obol Network / OBOL / 18 dp, Permit2
ERC-8004 Identity Registry0x8004a818bfb912233c491871b3d84c89a494bd9emint = agentId

Wallet roles:

RoleAddressSource
Alice / seller / register0xC0De030F6C37f490594F93fB99e2756703c4297Eseller payTo + funded EOA
Bob / buyer / payer0x57b0eF875DeB5A37301F1640E469a2129Da9490Edeterministic 2nd-derived from REMOTE_SIGNER_PRIVATE_KEY; bobSigner == BOB_WALLET ✓

Balances:

TokenAddressBeforeAfterExpected deltaActual delta
OBOL (Bob)0x0a09371a…4949000000000000000 wei4948000000000000000 wei-1000000000000000 (0.001 OBOL)-1000000000000000 ✓ exact
OBOL (Alice)0x0a09371a…+1000000000000000 (0.001 OBOL)+1000000000000000 ✓ exact

Transaction receipts:

PurposeTx hashFromToAmount / eventStatus
ERC-8004 registration0xff4cdbbdeea75e578728f097eb35ba230c42cc2410eb67fb2ce910782d2c2863AliceIdentity Registry 0x8004a818…mint agentId 67240x1
Metadata / service offer0x19055e9680e6f6072e0783310364e80c72f89c9836e4700ad78f841894a010c4AliceIdentity RegistrysetMetadata0x1
Settlement transfer0x81f86c63992089802beba5fad18525f5bcd2509bcdc95913958c310df400f455Bob 0x57b0eF…Alice 0xC0De03…OBOL 0.001 (Permit2)0x1

Runtime Evidence

QA environment:

ItemValue
OS / archmacOS (darwin) / arm64
Backendk3d (rancher/k3s v1.35.1-k3s1)
Tool versionskubectl 1.35.3, helm 3.20.1, helmfile 1.4.3, k3d 5.8.3
QA agent/modelHermes via LiteLLM → vLLM qwen36-deep (27B-class)

Images:

ComponentImageTag / digestSource
serviceoffer-controller (release pin)ghcr.io/obolnetwork/serviceoffer-controller503016b@sha256:bec62ea0…121957rc9 (this PR's repin)
serviceoffer-controller/buyer/verifier (smoke)ghcr.io/obolnetwork/…:latestbuilt from this branch (dev mode)

Kubernetes / stack:

ItemValue
Stack IDsper-run default + alice/bob (petnames)
Namespaceshermes-obol-agent, llm, x402, erpc, traefik, agent-*
Pod readinessall core pods Running (per flow checks)
Cleanup resultstacks torn down by release-smoke cleanup trap

Model and routing:

ItemValue
Agent/model usedqwen36-deep (vLLM, enable_thinking=false)
LiteLLM routecustom endpoint → host-reachable vLLM; paid/* → x402-buyer sidecar
Paid endpoint statuspaid/qwen3.5:9b Ready (5 auths loaded)
Auth token sourceobol agent auth (LiteLLM master key for upstream)

Artifacts and logs:

ArtifactLocation / linkNotes
Release report.tmp/release-smoke-20260603-130613/RELEASE_REPORT.mdrun6 per-flow table
flow-14 receipts.tmp/release-smoke-20260603-130613/flow-14-receiptsrun6 registration + settlement JSON

Demo readiness:

ItemStatusNotes
Seller visible / registeredERC-8004 agentId on Base Sepolia
Buyer discovery works402 → probe → pre-sign → PurchaseRequest
Paid route workspaid/* → 200
Settlement visible on-chainOBOL Transfer, status 0x1

Review Notes

Known gaps:

Follow-ups:

Reviewer focus:

  • internal/embed/infrastructure/base/templates/x402.yaml controller pin + embed_crd_test.go
    tripwire (no RBAC widened).
  • internal/x402/buyer/signer.go expiry filter (USDC validBefore=2106 never dropped).
  • internal/serviceoffercontroller/purchase.go not-found drain case (transient errors still requeue).

bussyjd added 7 commits June 3, 2026 10:24
…et create-only
The pinned serviceoffer-controller image (f5d94fc) was a side-branch build that
predated the change making Secret create-only in the reconciler. The tightened
ClusterRole grants no secrets update/patch verb, so the deployed binary 403s
when it Updates the per-agent hermes-api-server / remote-signer-keystore Secrets
on re-reconcile, and per-agent provisioning never converges.
Repin to 503016b@sha256:bec62ea0 (rc9 commit 503016b, image 0.10.0-rc9), whose
reconciler treats Secret as create-only and matches the shipped RBAC. Add a
tripwire test mirroring the x402-verifier one so a future downgrade can't
silently re-ship the bug. The short-SHA tag keeps the dev-mode :latest rewrite
and production pin invariants intact.
HoldSign popped s.auths[0] with no deadline check. A pre-signed Permit2 (OBOL)
batch shares one ~5-min deadline, so once expired the buyer served the whole
batch auth-by-auth, each returning 503 invalid_payment_expired from the
verifier before reaching a fresh auth. Add authDeadlineUnix (covering the
Permit2 deadline, nested ERC-3009 validBefore, and legacy flat field) and skip
expired auths at pick time. USDC vouchers use a year-2106 validBefore and are
never dropped.
… is gone
reconcileDeletingPurchase routed the 'not found in sidecar status' error into
the Configured&&Remaining>0 branch, which kept Remaining>0 and requeued every
5s forever, stranding the PurchaseRequest in Terminating until its finalizer was
force-removed. That signal means the sidecar has nothing left to drain. Add a
case (via isSidecarUpstreamGone) that collapses Remaining to 0 so cleanup and
finalizer removal proceed, consistent with the terminal not-found check already
present later in the function. Transient errors still requeue.
buy.py status/list showed the raw sidecar 'remaining' count, so an all-expired
Permit2 auth pool read as ready to spend. Add _auth_deadline / _count_valid_auths
and surface expired auths in both commands so an operator or agent tops up
instead of burning expired vouchers into 503s.
… path
HandleProxy (and the standalone inference gateway) rebuild the ForwardAuth
middleware per request with VerifyOnly=false by design — they proxy to the real
upstream and settle only after a <400 response — so the verifyOnly=false warning
fired on every paid request telling operators to 'fix' correct config. Add a
SettlesInProcess flag that suppresses the warning on those paths while leaving
the genuinely-dangerous Traefik ForwardAuth path loud.
obol agent new --model X provisioned cleanly for an unknown model, then every
chat call failed with 'no healthy deployments for this model'. Add a preflight
in createCRDAgent that checks a non-empty --model against the LiteLLM registry
and fails fast with the available models. A transient list error warns and
continues; an empty model still lets the controller auto-pin.
…races
Docker Desktop on macOS intermittently fails to create the gRPC-FUSE mount
source for a k3d node's workspace data dir under sustained cluster-churn
("error while creating mount source path ...: no such file or directory"),
so the k3s node never reports ready and k3d rolls the cluster back. The host
dir exists; it's a daemon-side file-sharing race. The dual-stack stack-up loop
already retries port-bind and image/Helm transients — extend it to retry this
mount race (a fresh cluster on retry clears it) so the release smoke isn't
flaked by an environment-side Docker hiccup.
@OisinKyne
OisinKyne merged commit a2742da into mainJun 3, 2026
9 checks passed
@OisinKyne
OisinKyne deleted the fix/rc9-ga-blockers branch June 3, 2026 15:01
Sign up for freeto 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

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report - #583

Merged
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers
Jun 3, 2026
Merged

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report#583
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

What changed: Fixes the v0.10.0-rc9 upgrade-report issues. Each was validated against rc9
source (adversarially re-checked) before fixing.

  • Bring obolup code to this repo #1 (GA blocker) — repin serviceoffer-controller from the f5d94fc side-branch build
    (which predated the Secret-create-only reconciler change) to 503016b@sha256:bec62ea0
    (rc9 commit 503016bf, image 0.10.0-rc9). The old pin Updates per-agent Secrets, which
    the tightened RBAC (no secrets:update/patch) 403s → per-agent provisioning never converges.
    Added a tripwire test.
  • User facing ingress #3x402-buyerHoldSign now drops expired pre-signed auths before signing
    (Permit2 deadline / ERC-3009 validBefore), ending the 503 invalid_payment_expired cascade.
  • WIP: Working towards a high quality rpc. #4buy.py status/list count is expiry-aware (valid vs expired auths).
  • Specify flag passing to obolup (or obol) #5reconcileDeletingPurchase finalizes the delete-drain on the not found in sidecar status signal instead of requeueing forever (stranded Terminating).
  • P1 — suppress the per-request verifyOnly=false warning on the in-process settle path
    (HandleProxy / obol sell inference); the Traefik ForwardAuth path still warns.
  • P2obol agent new --model X validates X against the LiteLLM registry; fails fast.

Issue #2 (master-Hermes PVC ownership on k3d local-path) is already fixed on main
(eb985bd/671c8ac root-chown init container); this branch inherits it. Per-agent Hermes was
confirmed not a residual (it seeds via Secret, not host PVC writes).

Why it matters: #1 and #2 are GA blockers from the rc9 report; the rest are buy-side
correctness / UX fixes on the monetize path.

Risk level: low — the controller fix is a pin bump to an already-published rc9 image; the rest
are narrow, regression-tested behaviour changes. No RBAC widened, no security surface added.

Commit under test: 8cfd0ca1 (live-chain evidence captured at b90118fd; 8cfd0ca1
adds only the dual-stack flows retry, no runtime behaviour change)

Base branch: main

Scope

  • Code
  • Charts / manifests
  • Flows / QA scripts
  • Docs / skills
  • Images / dependencies
  • Other:

Validation

CI checks:

CheckStatusLink
(added on push)pending

Unit tests:

go test ./... → ok (33 packages), gofmt + go vet clean
new regression tests (all PASS, #3/P1 fail without the fix):
internal/x402/buyer TestPreSignedSigner_DropsExpiredAuths, TestAuthDeadlineUnix (#3)
internal/serviceoffercontroller TestIsSidecarUpstreamGone (#5)
internal/x402 TestForwardAuth_SettlesInProcess_SuppressesWarning (P1)
cmd/obol TestIsModelConfigured (P2)
internal/embed TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix (#1)
+ dev-rewrite/production-pin guards updated for the new pin
commit: b90118fd

Integration tests:

n/a — covered by the release smoke below.

Flow tests (best result per flow across 3 full smoke runs on local darwin/arm64, k3d):

FlowNetworkResultEvidence
flow-01..10 (single stack incl. buy+lifecycle)base-sepolia (anvil)PASSrun5 + run6
flow-11 dual-stack (USDC)base-sepolia (anvil)PASSrun6
flow-13 dual-stack-obol (anvil fork)base-sepolia (anvil fork)PASSrun5
flow-14 live-obolbase-sepolia (LIVE)PASSrun5 + run6 — on-chain receipts below

Release smoke:

RELEASE_SMOKE_INCLUDE_OBOL=true RELEASE_SMOKE_INCLUDE_OBOL_FORK=true \
OBOL_LLM_ENDPOINT=http://<spark>:8000/v1 OBOL_LLM_MODEL=qwen36-deep \
bash flows/release-smoke.sh
LLM routed through vLLM qwen36-deep (DGX Spark over tailscale).
Every flow passed; no single run reached 13/13 because this macOS/Docker-Desktop +
cloudflare-quick-tunnel box hit a DIFFERENT environment-side transient each pass
(none code-related):
run5: 12/13 — flow-11 Docker Desktop gRPC-FUSE mount race on Alice cluster create
run6: 12/13 — flow-13 same mount race on Bob cluster create
run7: — flow-07/08 cloudflare quick-tunnel failed to establish (local 402 gate PASS)
The mount race now has a retry (commit 8cfd0ca1). The tunnel flake is external (trycloudflare).
#1 pin bump validated separately on a live cluster: deploying 503016b@sha256:bec62ea0 made a
per-agent `obol agent new` reach Ready with no 403; the prior f5d94fc image reproduces the 403.
Dev-mode rebuilds controller/buyer/verifier from this branch's source, so the smoke exercises
the source fixes directly.

Live Chain Evidence

Network: Base Sepolia (eip155:84532)

RPC/provider: paid drpc load-balancer (redacted)

Facilitator: https://x402.gcp.obol.tech (prometheus-overlay)

Contracts and tokens:

NameAddressVersion / notes
OBOL token0x0a09371a8b011d5110656ceBCc70603e53FD2c78Obol Network / OBOL / 18 dp, Permit2
ERC-8004 Identity Registry0x8004a818bfb912233c491871b3d84c89a494bd9emint = agentId

Wallet roles:

RoleAddressSource
Alice / seller / register0xC0De030F6C37f490594F93fB99e2756703c4297Eseller payTo + funded EOA
Bob / buyer / payer0x57b0eF875DeB5A37301F1640E469a2129Da9490Edeterministic 2nd-derived from REMOTE_SIGNER_PRIVATE_KEY; bobSigner == BOB_WALLET ✓

Balances:

TokenAddressBeforeAfterExpected deltaActual delta
OBOL (Bob)0x0a09371a…4949000000000000000 wei4948000000000000000 wei-1000000000000000 (0.001 OBOL)-1000000000000000 ✓ exact
OBOL (Alice)0x0a09371a…+1000000000000000 (0.001 OBOL)+1000000000000000 ✓ exact

Transaction receipts:

PurposeTx hashFromToAmount / eventStatus
ERC-8004 registration0xff4cdbbdeea75e578728f097eb35ba230c42cc2410eb67fb2ce910782d2c2863AliceIdentity Registry 0x8004a818…mint agentId 67240x1
Metadata / service offer0x19055e9680e6f6072e0783310364e80c72f89c9836e4700ad78f841894a010c4AliceIdentity RegistrysetMetadata0x1
Settlement transfer0x81f86c63992089802beba5fad18525f5bcd2509bcdc95913958c310df400f455Bob 0x57b0eF…Alice 0xC0De03…OBOL 0.001 (Permit2)0x1

Runtime Evidence

QA environment:

ItemValue
OS / archmacOS (darwin) / arm64
Backendk3d (rancher/k3s v1.35.1-k3s1)
Tool versionskubectl 1.35.3, helm 3.20.1, helmfile 1.4.3, k3d 5.8.3
QA agent/modelHermes via LiteLLM → vLLM qwen36-deep (27B-class)

Images:

ComponentImageTag / digestSource
serviceoffer-controller (release pin)ghcr.io/obolnetwork/serviceoffer-controller503016b@sha256:bec62ea0…121957rc9 (this PR's repin)
serviceoffer-controller/buyer/verifier (smoke)ghcr.io/obolnetwork/…:latestbuilt from this branch (dev mode)

Kubernetes / stack:

ItemValue
Stack IDsper-run default + alice/bob (petnames)
Namespaceshermes-obol-agent, llm, x402, erpc, traefik, agent-*
Pod readinessall core pods Running (per flow checks)
Cleanup resultstacks torn down by release-smoke cleanup trap

Model and routing:

ItemValue
Agent/model usedqwen36-deep (vLLM, enable_thinking=false)
LiteLLM routecustom endpoint → host-reachable vLLM; paid/* → x402-buyer sidecar
Paid endpoint statuspaid/qwen3.5:9b Ready (5 auths loaded)
Auth token sourceobol agent auth (LiteLLM master key for upstream)

Artifacts and logs:

ArtifactLocation / linkNotes
Release report.tmp/release-smoke-20260603-130613/RELEASE_REPORT.mdrun6 per-flow table
flow-14 receipts.tmp/release-smoke-20260603-130613/flow-14-receiptsrun6 registration + settlement JSON

Demo readiness:

ItemStatusNotes
Seller visible / registeredERC-8004 agentId on Base Sepolia
Buyer discovery works402 → probe → pre-sign → PurchaseRequest
Paid route workspaid/* → 200
Settlement visible on-chainOBOL Transfer, status 0x1

Review Notes

Known gaps:

Follow-ups:

Reviewer focus:

  • internal/embed/infrastructure/base/templates/x402.yaml controller pin + embed_crd_test.go
    tripwire (no RBAC widened).
  • internal/x402/buyer/signer.go expiry filter (USDC validBefore=2106 never dropped).
  • internal/serviceoffercontroller/purchase.go not-found drain case (transient errors still requeue).

bussyjd added 7 commits June 3, 2026 10:24
…et create-only
The pinned serviceoffer-controller image (f5d94fc) was a side-branch build that
predated the change making Secret create-only in the reconciler. The tightened
ClusterRole grants no secrets update/patch verb, so the deployed binary 403s
when it Updates the per-agent hermes-api-server / remote-signer-keystore Secrets
on re-reconcile, and per-agent provisioning never converges.
Repin to 503016b@sha256:bec62ea0 (rc9 commit 503016b, image 0.10.0-rc9), whose
reconciler treats Secret as create-only and matches the shipped RBAC. Add a
tripwire test mirroring the x402-verifier one so a future downgrade can't
silently re-ship the bug. The short-SHA tag keeps the dev-mode :latest rewrite
and production pin invariants intact.
HoldSign popped s.auths[0] with no deadline check. A pre-signed Permit2 (OBOL)
batch shares one ~5-min deadline, so once expired the buyer served the whole
batch auth-by-auth, each returning 503 invalid_payment_expired from the
verifier before reaching a fresh auth. Add authDeadlineUnix (covering the
Permit2 deadline, nested ERC-3009 validBefore, and legacy flat field) and skip
expired auths at pick time. USDC vouchers use a year-2106 validBefore and are
never dropped.
… is gone
reconcileDeletingPurchase routed the 'not found in sidecar status' error into
the Configured&&Remaining>0 branch, which kept Remaining>0 and requeued every
5s forever, stranding the PurchaseRequest in Terminating until its finalizer was
force-removed. That signal means the sidecar has nothing left to drain. Add a
case (via isSidecarUpstreamGone) that collapses Remaining to 0 so cleanup and
finalizer removal proceed, consistent with the terminal not-found check already
present later in the function. Transient errors still requeue.
buy.py status/list showed the raw sidecar 'remaining' count, so an all-expired
Permit2 auth pool read as ready to spend. Add _auth_deadline / _count_valid_auths
and surface expired auths in both commands so an operator or agent tops up
instead of burning expired vouchers into 503s.
… path
HandleProxy (and the standalone inference gateway) rebuild the ForwardAuth
middleware per request with VerifyOnly=false by design — they proxy to the real
upstream and settle only after a <400 response — so the verifyOnly=false warning
fired on every paid request telling operators to 'fix' correct config. Add a
SettlesInProcess flag that suppresses the warning on those paths while leaving
the genuinely-dangerous Traefik ForwardAuth path loud.
obol agent new --model X provisioned cleanly for an unknown model, then every
chat call failed with 'no healthy deployments for this model'. Add a preflight
in createCRDAgent that checks a non-empty --model against the LiteLLM registry
and fails fast with the available models. A transient list error warns and
continues; an empty model still lets the controller auto-pin.
…races
Docker Desktop on macOS intermittently fails to create the gRPC-FUSE mount
source for a k3d node's workspace data dir under sustained cluster-churn
("error while creating mount source path ...: no such file or directory"),
so the k3s node never reports ready and k3d rolls the cluster back. The host
dir exists; it's a daemon-side file-sharing race. The dual-stack stack-up loop
already retries port-bind and image/Helm transients — extend it to retry this
mount race (a fresh cluster on retry clears it) so the release smoke isn't
flaked by an environment-side Docker hiccup.
@OisinKyne
OisinKyne merged commit a2742da into mainJun 3, 2026
9 checks passed
@OisinKyne
OisinKyne deleted the fix/rc9-ga-blockers branch June 3, 2026 15:01
Sign up for freeto 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

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report - #583

Merged
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers
Jun 3, 2026
Merged

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report#583
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

What changed: Fixes the v0.10.0-rc9 upgrade-report issues. Each was validated against rc9
source (adversarially re-checked) before fixing.

  • Bring obolup code to this repo #1 (GA blocker) — repin serviceoffer-controller from the f5d94fc side-branch build
    (which predated the Secret-create-only reconciler change) to 503016b@sha256:bec62ea0
    (rc9 commit 503016bf, image 0.10.0-rc9). The old pin Updates per-agent Secrets, which
    the tightened RBAC (no secrets:update/patch) 403s → per-agent provisioning never converges.
    Added a tripwire test.
  • User facing ingress #3x402-buyerHoldSign now drops expired pre-signed auths before signing
    (Permit2 deadline / ERC-3009 validBefore), ending the 503 invalid_payment_expired cascade.
  • WIP: Working towards a high quality rpc. #4buy.py status/list count is expiry-aware (valid vs expired auths).
  • Specify flag passing to obolup (or obol) #5reconcileDeletingPurchase finalizes the delete-drain on the not found in sidecar status signal instead of requeueing forever (stranded Terminating).
  • P1 — suppress the per-request verifyOnly=false warning on the in-process settle path
    (HandleProxy / obol sell inference); the Traefik ForwardAuth path still warns.
  • P2obol agent new --model X validates X against the LiteLLM registry; fails fast.

Issue #2 (master-Hermes PVC ownership on k3d local-path) is already fixed on main
(eb985bd/671c8ac root-chown init container); this branch inherits it. Per-agent Hermes was
confirmed not a residual (it seeds via Secret, not host PVC writes).

Why it matters: #1 and #2 are GA blockers from the rc9 report; the rest are buy-side
correctness / UX fixes on the monetize path.

Risk level: low — the controller fix is a pin bump to an already-published rc9 image; the rest
are narrow, regression-tested behaviour changes. No RBAC widened, no security surface added.

Commit under test: 8cfd0ca1 (live-chain evidence captured at b90118fd; 8cfd0ca1
adds only the dual-stack flows retry, no runtime behaviour change)

Base branch: main

Scope

  • Code
  • Charts / manifests
  • Flows / QA scripts
  • Docs / skills
  • Images / dependencies
  • Other:

Validation

CI checks:

CheckStatusLink
(added on push)pending

Unit tests:

go test ./... → ok (33 packages), gofmt + go vet clean
new regression tests (all PASS, #3/P1 fail without the fix):
internal/x402/buyer TestPreSignedSigner_DropsExpiredAuths, TestAuthDeadlineUnix (#3)
internal/serviceoffercontroller TestIsSidecarUpstreamGone (#5)
internal/x402 TestForwardAuth_SettlesInProcess_SuppressesWarning (P1)
cmd/obol TestIsModelConfigured (P2)
internal/embed TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix (#1)
+ dev-rewrite/production-pin guards updated for the new pin
commit: b90118fd

Integration tests:

n/a — covered by the release smoke below.

Flow tests (best result per flow across 3 full smoke runs on local darwin/arm64, k3d):

FlowNetworkResultEvidence
flow-01..10 (single stack incl. buy+lifecycle)base-sepolia (anvil)PASSrun5 + run6
flow-11 dual-stack (USDC)base-sepolia (anvil)PASSrun6
flow-13 dual-stack-obol (anvil fork)base-sepolia (anvil fork)PASSrun5
flow-14 live-obolbase-sepolia (LIVE)PASSrun5 + run6 — on-chain receipts below

Release smoke:

RELEASE_SMOKE_INCLUDE_OBOL=true RELEASE_SMOKE_INCLUDE_OBOL_FORK=true \
OBOL_LLM_ENDPOINT=http://<spark>:8000/v1 OBOL_LLM_MODEL=qwen36-deep \
bash flows/release-smoke.sh
LLM routed through vLLM qwen36-deep (DGX Spark over tailscale).
Every flow passed; no single run reached 13/13 because this macOS/Docker-Desktop +
cloudflare-quick-tunnel box hit a DIFFERENT environment-side transient each pass
(none code-related):
run5: 12/13 — flow-11 Docker Desktop gRPC-FUSE mount race on Alice cluster create
run6: 12/13 — flow-13 same mount race on Bob cluster create
run7: — flow-07/08 cloudflare quick-tunnel failed to establish (local 402 gate PASS)
The mount race now has a retry (commit 8cfd0ca1). The tunnel flake is external (trycloudflare).
#1 pin bump validated separately on a live cluster: deploying 503016b@sha256:bec62ea0 made a
per-agent `obol agent new` reach Ready with no 403; the prior f5d94fc image reproduces the 403.
Dev-mode rebuilds controller/buyer/verifier from this branch's source, so the smoke exercises
the source fixes directly.

Live Chain Evidence

Network: Base Sepolia (eip155:84532)

RPC/provider: paid drpc load-balancer (redacted)

Facilitator: https://x402.gcp.obol.tech (prometheus-overlay)

Contracts and tokens:

NameAddressVersion / notes
OBOL token0x0a09371a8b011d5110656ceBCc70603e53FD2c78Obol Network / OBOL / 18 dp, Permit2
ERC-8004 Identity Registry0x8004a818bfb912233c491871b3d84c89a494bd9emint = agentId

Wallet roles:

RoleAddressSource
Alice / seller / register0xC0De030F6C37f490594F93fB99e2756703c4297Eseller payTo + funded EOA
Bob / buyer / payer0x57b0eF875DeB5A37301F1640E469a2129Da9490Edeterministic 2nd-derived from REMOTE_SIGNER_PRIVATE_KEY; bobSigner == BOB_WALLET ✓

Balances:

TokenAddressBeforeAfterExpected deltaActual delta
OBOL (Bob)0x0a09371a…4949000000000000000 wei4948000000000000000 wei-1000000000000000 (0.001 OBOL)-1000000000000000 ✓ exact
OBOL (Alice)0x0a09371a…+1000000000000000 (0.001 OBOL)+1000000000000000 ✓ exact

Transaction receipts:

PurposeTx hashFromToAmount / eventStatus
ERC-8004 registration0xff4cdbbdeea75e578728f097eb35ba230c42cc2410eb67fb2ce910782d2c2863AliceIdentity Registry 0x8004a818…mint agentId 67240x1
Metadata / service offer0x19055e9680e6f6072e0783310364e80c72f89c9836e4700ad78f841894a010c4AliceIdentity RegistrysetMetadata0x1
Settlement transfer0x81f86c63992089802beba5fad18525f5bcd2509bcdc95913958c310df400f455Bob 0x57b0eF…Alice 0xC0De03…OBOL 0.001 (Permit2)0x1

Runtime Evidence

QA environment:

ItemValue
OS / archmacOS (darwin) / arm64
Backendk3d (rancher/k3s v1.35.1-k3s1)
Tool versionskubectl 1.35.3, helm 3.20.1, helmfile 1.4.3, k3d 5.8.3
QA agent/modelHermes via LiteLLM → vLLM qwen36-deep (27B-class)

Images:

ComponentImageTag / digestSource
serviceoffer-controller (release pin)ghcr.io/obolnetwork/serviceoffer-controller503016b@sha256:bec62ea0…121957rc9 (this PR's repin)
serviceoffer-controller/buyer/verifier (smoke)ghcr.io/obolnetwork/…:latestbuilt from this branch (dev mode)

Kubernetes / stack:

ItemValue
Stack IDsper-run default + alice/bob (petnames)
Namespaceshermes-obol-agent, llm, x402, erpc, traefik, agent-*
Pod readinessall core pods Running (per flow checks)
Cleanup resultstacks torn down by release-smoke cleanup trap

Model and routing:

ItemValue
Agent/model usedqwen36-deep (vLLM, enable_thinking=false)
LiteLLM routecustom endpoint → host-reachable vLLM; paid/* → x402-buyer sidecar
Paid endpoint statuspaid/qwen3.5:9b Ready (5 auths loaded)
Auth token sourceobol agent auth (LiteLLM master key for upstream)

Artifacts and logs:

ArtifactLocation / linkNotes
Release report.tmp/release-smoke-20260603-130613/RELEASE_REPORT.mdrun6 per-flow table
flow-14 receipts.tmp/release-smoke-20260603-130613/flow-14-receiptsrun6 registration + settlement JSON

Demo readiness:

ItemStatusNotes
Seller visible / registeredERC-8004 agentId on Base Sepolia
Buyer discovery works402 → probe → pre-sign → PurchaseRequest
Paid route workspaid/* → 200
Settlement visible on-chainOBOL Transfer, status 0x1

Review Notes

Known gaps:

Follow-ups:

Reviewer focus:

  • internal/embed/infrastructure/base/templates/x402.yaml controller pin + embed_crd_test.go
    tripwire (no RBAC widened).
  • internal/x402/buyer/signer.go expiry filter (USDC validBefore=2106 never dropped).
  • internal/serviceoffercontroller/purchase.go not-found drain case (transient errors still requeue).

bussyjd added 7 commits June 3, 2026 10:24
…et create-only
The pinned serviceoffer-controller image (f5d94fc) was a side-branch build that
predated the change making Secret create-only in the reconciler. The tightened
ClusterRole grants no secrets update/patch verb, so the deployed binary 403s
when it Updates the per-agent hermes-api-server / remote-signer-keystore Secrets
on re-reconcile, and per-agent provisioning never converges.
Repin to 503016b@sha256:bec62ea0 (rc9 commit 503016b, image 0.10.0-rc9), whose
reconciler treats Secret as create-only and matches the shipped RBAC. Add a
tripwire test mirroring the x402-verifier one so a future downgrade can't
silently re-ship the bug. The short-SHA tag keeps the dev-mode :latest rewrite
and production pin invariants intact.
HoldSign popped s.auths[0] with no deadline check. A pre-signed Permit2 (OBOL)
batch shares one ~5-min deadline, so once expired the buyer served the whole
batch auth-by-auth, each returning 503 invalid_payment_expired from the
verifier before reaching a fresh auth. Add authDeadlineUnix (covering the
Permit2 deadline, nested ERC-3009 validBefore, and legacy flat field) and skip
expired auths at pick time. USDC vouchers use a year-2106 validBefore and are
never dropped.
… is gone
reconcileDeletingPurchase routed the 'not found in sidecar status' error into
the Configured&&Remaining>0 branch, which kept Remaining>0 and requeued every
5s forever, stranding the PurchaseRequest in Terminating until its finalizer was
force-removed. That signal means the sidecar has nothing left to drain. Add a
case (via isSidecarUpstreamGone) that collapses Remaining to 0 so cleanup and
finalizer removal proceed, consistent with the terminal not-found check already
present later in the function. Transient errors still requeue.
buy.py status/list showed the raw sidecar 'remaining' count, so an all-expired
Permit2 auth pool read as ready to spend. Add _auth_deadline / _count_valid_auths
and surface expired auths in both commands so an operator or agent tops up
instead of burning expired vouchers into 503s.
… path
HandleProxy (and the standalone inference gateway) rebuild the ForwardAuth
middleware per request with VerifyOnly=false by design — they proxy to the real
upstream and settle only after a <400 response — so the verifyOnly=false warning
fired on every paid request telling operators to 'fix' correct config. Add a
SettlesInProcess flag that suppresses the warning on those paths while leaving
the genuinely-dangerous Traefik ForwardAuth path loud.
obol agent new --model X provisioned cleanly for an unknown model, then every
chat call failed with 'no healthy deployments for this model'. Add a preflight
in createCRDAgent that checks a non-empty --model against the LiteLLM registry
and fails fast with the available models. A transient list error warns and
continues; an empty model still lets the controller auto-pin.
…races
Docker Desktop on macOS intermittently fails to create the gRPC-FUSE mount
source for a k3d node's workspace data dir under sustained cluster-churn
("error while creating mount source path ...: no such file or directory"),
so the k3s node never reports ready and k3d rolls the cluster back. The host
dir exists; it's a daemon-side file-sharing race. The dual-stack stack-up loop
already retries port-bind and image/Helm transients — extend it to retry this
mount race (a fresh cluster on retry clears it) so the release smoke isn't
flaked by an environment-side Docker hiccup.
@OisinKyne
OisinKyne merged commit a2742da into mainJun 3, 2026
9 checks passed
@OisinKyne
OisinKyne deleted the fix/rc9-ga-blockers branch June 3, 2026 15:01
Sign up for freeto 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

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report - #583

Merged
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers
Jun 3, 2026
Merged

fix(rc9): GA blockers + monetize buy-side fixes from the v0.10.0-rc9 report#583
OisinKyne merged 7 commits into
mainfrom
fix/rc9-ga-blockers

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

What changed: Fixes the v0.10.0-rc9 upgrade-report issues. Each was validated against rc9
source (adversarially re-checked) before fixing.

  • Bring obolup code to this repo #1 (GA blocker) — repin serviceoffer-controller from the f5d94fc side-branch build
    (which predated the Secret-create-only reconciler change) to 503016b@sha256:bec62ea0
    (rc9 commit 503016bf, image 0.10.0-rc9). The old pin Updates per-agent Secrets, which
    the tightened RBAC (no secrets:update/patch) 403s → per-agent provisioning never converges.
    Added a tripwire test.
  • User facing ingress #3x402-buyerHoldSign now drops expired pre-signed auths before signing
    (Permit2 deadline / ERC-3009 validBefore), ending the 503 invalid_payment_expired cascade.
  • WIP: Working towards a high quality rpc. #4buy.py status/list count is expiry-aware (valid vs expired auths).
  • Specify flag passing to obolup (or obol) #5reconcileDeletingPurchase finalizes the delete-drain on the not found in sidecar status signal instead of requeueing forever (stranded Terminating).
  • P1 — suppress the per-request verifyOnly=false warning on the in-process settle path
    (HandleProxy / obol sell inference); the Traefik ForwardAuth path still warns.
  • P2obol agent new --model X validates X against the LiteLLM registry; fails fast.

Issue #2 (master-Hermes PVC ownership on k3d local-path) is already fixed on main
(eb985bd/671c8ac root-chown init container); this branch inherits it. Per-agent Hermes was
confirmed not a residual (it seeds via Secret, not host PVC writes).

Why it matters: #1 and #2 are GA blockers from the rc9 report; the rest are buy-side
correctness / UX fixes on the monetize path.

Risk level: low — the controller fix is a pin bump to an already-published rc9 image; the rest
are narrow, regression-tested behaviour changes. No RBAC widened, no security surface added.

Commit under test: 8cfd0ca1 (live-chain evidence captured at b90118fd; 8cfd0ca1
adds only the dual-stack flows retry, no runtime behaviour change)

Base branch: main

Scope

  • Code
  • Charts / manifests
  • Flows / QA scripts
  • Docs / skills
  • Images / dependencies
  • Other:

Validation

CI checks:

CheckStatusLink
(added on push)pending

Unit tests:

go test ./... → ok (33 packages), gofmt + go vet clean
new regression tests (all PASS, #3/P1 fail without the fix):
internal/x402/buyer TestPreSignedSigner_DropsExpiredAuths, TestAuthDeadlineUnix (#3)
internal/serviceoffercontroller TestIsSidecarUpstreamGone (#5)
internal/x402 TestForwardAuth_SettlesInProcess_SuppressesWarning (P1)
cmd/obol TestIsModelConfigured (P2)
internal/embed TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix (#1)
+ dev-rewrite/production-pin guards updated for the new pin
commit: b90118fd

Integration tests:

n/a — covered by the release smoke below.

Flow tests (best result per flow across 3 full smoke runs on local darwin/arm64, k3d):

FlowNetworkResultEvidence
flow-01..10 (single stack incl. buy+lifecycle)base-sepolia (anvil)PASSrun5 + run6
flow-11 dual-stack (USDC)base-sepolia (anvil)PASSrun6
flow-13 dual-stack-obol (anvil fork)base-sepolia (anvil fork)PASSrun5
flow-14 live-obolbase-sepolia (LIVE)PASSrun5 + run6 — on-chain receipts below

Release smoke:

RELEASE_SMOKE_INCLUDE_OBOL=true RELEASE_SMOKE_INCLUDE_OBOL_FORK=true \
OBOL_LLM_ENDPOINT=http://<spark>:8000/v1 OBOL_LLM_MODEL=qwen36-deep \
bash flows/release-smoke.sh
LLM routed through vLLM qwen36-deep (DGX Spark over tailscale).
Every flow passed; no single run reached 13/13 because this macOS/Docker-Desktop +
cloudflare-quick-tunnel box hit a DIFFERENT environment-side transient each pass
(none code-related):
run5: 12/13 — flow-11 Docker Desktop gRPC-FUSE mount race on Alice cluster create
run6: 12/13 — flow-13 same mount race on Bob cluster create
run7: — flow-07/08 cloudflare quick-tunnel failed to establish (local 402 gate PASS)
The mount race now has a retry (commit 8cfd0ca1). The tunnel flake is external (trycloudflare).
#1 pin bump validated separately on a live cluster: deploying 503016b@sha256:bec62ea0 made a
per-agent `obol agent new` reach Ready with no 403; the prior f5d94fc image reproduces the 403.
Dev-mode rebuilds controller/buyer/verifier from this branch's source, so the smoke exercises
the source fixes directly.

Live Chain Evidence

Network: Base Sepolia (eip155:84532)

RPC/provider: paid drpc load-balancer (redacted)

Facilitator: https://x402.gcp.obol.tech (prometheus-overlay)

Contracts and tokens:

NameAddressVersion / notes
OBOL token0x0a09371a8b011d5110656ceBCc70603e53FD2c78Obol Network / OBOL / 18 dp, Permit2
ERC-8004 Identity Registry0x8004a818bfb912233c491871b3d84c89a494bd9emint = agentId

Wallet roles:

RoleAddressSource
Alice / seller / register0xC0De030F6C37f490594F93fB99e2756703c4297Eseller payTo + funded EOA
Bob / buyer / payer0x57b0eF875DeB5A37301F1640E469a2129Da9490Edeterministic 2nd-derived from REMOTE_SIGNER_PRIVATE_KEY; bobSigner == BOB_WALLET ✓

Balances:

TokenAddressBeforeAfterExpected deltaActual delta
OBOL (Bob)0x0a09371a…4949000000000000000 wei4948000000000000000 wei-1000000000000000 (0.001 OBOL)-1000000000000000 ✓ exact
OBOL (Alice)0x0a09371a…+1000000000000000 (0.001 OBOL)+1000000000000000 ✓ exact

Transaction receipts:

PurposeTx hashFromToAmount / eventStatus
ERC-8004 registration0xff4cdbbdeea75e578728f097eb35ba230c42cc2410eb67fb2ce910782d2c2863AliceIdentity Registry 0x8004a818…mint agentId 67240x1
Metadata / service offer0x19055e9680e6f6072e0783310364e80c72f89c9836e4700ad78f841894a010c4AliceIdentity RegistrysetMetadata0x1
Settlement transfer0x81f86c63992089802beba5fad18525f5bcd2509bcdc95913958c310df400f455Bob 0x57b0eF…Alice 0xC0De03…OBOL 0.001 (Permit2)0x1

Runtime Evidence

QA environment:

ItemValue
OS / archmacOS (darwin) / arm64
Backendk3d (rancher/k3s v1.35.1-k3s1)
Tool versionskubectl 1.35.3, helm 3.20.1, helmfile 1.4.3, k3d 5.8.3
QA agent/modelHermes via LiteLLM → vLLM qwen36-deep (27B-class)

Images:

ComponentImageTag / digestSource
serviceoffer-controller (release pin)ghcr.io/obolnetwork/serviceoffer-controller503016b@sha256:bec62ea0…121957rc9 (this PR's repin)
serviceoffer-controller/buyer/verifier (smoke)ghcr.io/obolnetwork/…:latestbuilt from this branch (dev mode)

Kubernetes / stack:

ItemValue
Stack IDsper-run default + alice/bob (petnames)
Namespaceshermes-obol-agent, llm, x402, erpc, traefik, agent-*
Pod readinessall core pods Running (per flow checks)
Cleanup resultstacks torn down by release-smoke cleanup trap

Model and routing:

ItemValue
Agent/model usedqwen36-deep (vLLM, enable_thinking=false)
LiteLLM routecustom endpoint → host-reachable vLLM; paid/* → x402-buyer sidecar
Paid endpoint statuspaid/qwen3.5:9b Ready (5 auths loaded)
Auth token sourceobol agent auth (LiteLLM master key for upstream)

Artifacts and logs:

ArtifactLocation / linkNotes
Release report.tmp/release-smoke-20260603-130613/RELEASE_REPORT.mdrun6 per-flow table
flow-14 receipts.tmp/release-smoke-20260603-130613/flow-14-receiptsrun6 registration + settlement JSON

Demo readiness:

ItemStatusNotes
Seller visible / registeredERC-8004 agentId on Base Sepolia
Buyer discovery works402 → probe → pre-sign → PurchaseRequest
Paid route workspaid/* → 200
Settlement visible on-chainOBOL Transfer, status 0x1

Review Notes

Known gaps:

Follow-ups:

Reviewer focus:

  • internal/embed/infrastructure/base/templates/x402.yaml controller pin + embed_crd_test.go
    tripwire (no RBAC widened).
  • internal/x402/buyer/signer.go expiry filter (USDC validBefore=2106 never dropped).
  • internal/serviceoffercontroller/purchase.go not-found drain case (transient errors still requeue).

bussyjd added 7 commits June 3, 2026 10:24
…et create-only
The pinned serviceoffer-controller image (f5d94fc) was a side-branch build that
predated the change making Secret create-only in the reconciler. The tightened
ClusterRole grants no secrets update/patch verb, so the deployed binary 403s
when it Updates the per-agent hermes-api-server / remote-signer-keystore Secrets
on re-reconcile, and per-agent provisioning never converges.
Repin to 503016b@sha256:bec62ea0 (rc9 commit 503016b, image 0.10.0-rc9), whose
reconciler treats Secret as create-only and matches the shipped RBAC. Add a
tripwire test mirroring the x402-verifier one so a future downgrade can't
silently re-ship the bug. The short-SHA tag keeps the dev-mode :latest rewrite
and production pin invariants intact.
HoldSign popped s.auths[0] with no deadline check. A pre-signed Permit2 (OBOL)
batch shares one ~5-min deadline, so once expired the buyer served the whole
batch auth-by-auth, each returning 503 invalid_payment_expired from the
verifier before reaching a fresh auth. Add authDeadlineUnix (covering the
Permit2 deadline, nested ERC-3009 validBefore, and legacy flat field) and skip
expired auths at pick time. USDC vouchers use a year-2106 validBefore and are
never dropped.
… is gone
reconcileDeletingPurchase routed the 'not found in sidecar status' error into
the Configured&&Remaining>0 branch, which kept Remaining>0 and requeued every
5s forever, stranding the PurchaseRequest in Terminating until its finalizer was
force-removed. That signal means the sidecar has nothing left to drain. Add a
case (via isSidecarUpstreamGone) that collapses Remaining to 0 so cleanup and
finalizer removal proceed, consistent with the terminal not-found check already
present later in the function. Transient errors still requeue.
buy.py status/list showed the raw sidecar 'remaining' count, so an all-expired
Permit2 auth pool read as ready to spend. Add _auth_deadline / _count_valid_auths
and surface expired auths in both commands so an operator or agent tops up
instead of burning expired vouchers into 503s.
… path
HandleProxy (and the standalone inference gateway) rebuild the ForwardAuth
middleware per request with VerifyOnly=false by design — they proxy to the real
upstream and settle only after a <400 response — so the verifyOnly=false warning
fired on every paid request telling operators to 'fix' correct config. Add a
SettlesInProcess flag that suppresses the warning on those paths while leaving
the genuinely-dangerous Traefik ForwardAuth path loud.
obol agent new --model X provisioned cleanly for an unknown model, then every
chat call failed with 'no healthy deployments for this model'. Add a preflight
in createCRDAgent that checks a non-empty --model against the LiteLLM registry
and fails fast with the available models. A transient list error warns and
continues; an empty model still lets the controller auto-pin.
…races
Docker Desktop on macOS intermittently fails to create the gRPC-FUSE mount
source for a k3d node's workspace data dir under sustained cluster-churn
("error while creating mount source path ...: no such file or directory"),
so the k3s node never reports ready and k3d rolls the cluster back. The host
dir exists; it's a daemon-side file-sharing race. The dual-stack stack-up loop
already retries port-bind and image/Helm transients — extend it to retry this
mount race (a fresh cluster on retry clears it) so the release smoke isn't
flaked by an environment-side Docker hiccup.
@OisinKyne
OisinKyne merged commit a2742da into mainJun 3, 2026
9 checks passed
@OisinKyne
OisinKyne deleted the fix/rc9-ga-blockers branch June 3, 2026 15:01
Sign up for freeto 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

@bussyjd@OisinKyne