chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA - #493

Merged
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups
May 15, 2026
Merged

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA#493
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups

Conversation

@bussyjd

@bussyjdbussyjd commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase-1 polish + investigation follow-ups from plans/inference-v1337-buy-report-20260514.md. Net result: the report's central technical claim ("controller doesn't reconcile external-seller PurchaseRequests") is retracted — re-running with the new diagnostic gate proved the controller is endpoint-agnostic by design.

Stacks on #492 (folds in #487 + #489 + post-490 cleanups). Targets integration/post-490-cleanups so the diff shows only the 4 commits this PR adds.

What's in this branch

  • feat(buy-external): KEEP_CLUSTER_ON_FAIL knob + diagnostic snapshot on FAIL (b749f95) — adds external_snapshot_on_fail(). On FAIL, snapshots controller logs (current + --previous), PR YAML across all namespaces, buyer sidecar /status (via kubectl exec ... python3 against the litellm container — buyer is distroless), cluster-pods.txt, and recent cluster-events.txt to the artifact dir before any teardown.

  • fix(flows): pick freshest of .build/obol vs .workspace/bin/obol (eb13055) — bootstrap_flow_workspace in flows/lib.sh now stats both paths, picks the larger mtime, and emits a 5-line WARN when they differ by >5 minutes. Removes the silent-stale-binary footgun documented as v1337 attempt 5.

  • docs(skill): document Cloudflare-WAF UA pitfall (849cd93) — entry Add ADK #10 in release-smoke-debugging.md covering HTTP 403 + Cloudflare error 1010 from default Python-urllib UA, the c2dddc1 buy.py fix, and the (unconfirmed) follow-up about Go's http.Client defaults at purchase.go:183.

  • docs(plans): retract v1337 controller-gap hypothesis (82108c3) — plans/inference-v1337-followup-20260514.md companion to the original report. Re-run on spark1 showed the controller reconciles in 55s through Probed → AuthsLoaded → Configured → Ready. The Go-side probe was NOT WAF-blocked.

  • refactor(buy-external): green-only cleanup gate, drop KEEP_CLUSTER_ON_FAIL knob (df5fcff) — replaces the opt-in env knob with an unconditional rule: cleanup happens iff every step passes. Also inverts the prior success-side default (which left the cluster up on success "so the operator can poke around" — in practice operators re-ran from scratch and the leftover cluster mostly leaked).

Net cleanup behavior

Run outcomeBehavior
Every step PASSbob stack down — clean state for next run
Any step FAILSnapshot + preserve cluster; operator pays one manual bob stack down when done

No env knob; the pass/fail exit code is the gate.

Side findings (worth knowing)

The captured controller log surfaces a pre-existing LiteLLM hot-add quirk:

purchase: hot-add paid/qwen3.6-27b failed: POST /model/new: 400 Bad Request:
{"error":{"message":"Authentication Error, [Errno 30] Read-only file system: '/etc/litellm/config.yaml'", ...}}; relying on ConfigMap reload

LiteLLM's /model/new API tries to write back to the ConfigMap volume (read-only by Kubernetes default). The controller catches the 400 and falls back to the ConfigMap-reload path, which works. Not external-seller specific. Worth a one-liner in paid-flows.md so it stops surprising next-debugger.

Test plan

  • bash -n flows/buy-external.sh && bash -n flows/lib.sh — clean
  • spark1 re-run on chore/buy-external-followups against https://inference.v1337.org/services/aeon:
    • 17 of 18 steps PASS
    • PR Ready=True after 55s, observedGeneration: 1, paid/qwen3.6-27b published, remaining: 1, spent: 0
    • Diagnostic gate preserved cluster on FAIL + 7 snapshot files written
    • Step 18 fails on operator-error model name (qwen3.6-27b ≠ v1337's actual model id); Bob's 0.023 OBOL pre-signed auth was NOT consumed
  • No regressions to existing flows (only lib.sh::bootstrap_flow_workspace touched among shared code; signature preserved, contract unchanged)
  • Reviewer: confirm the green-only cleanup gate is the right default for this single-operator QA harness (CI flows that need different behavior would call out separately)

bussyjd added 4 commits May 14, 2026 12:37
…ot on FAIL
When `flows/buy-external.sh` fails (typically at step 14, the `buy.py buy`
invocation), the existing `external_cleanup` immediately tears the cluster
down — destroying the only places that record why the PurchaseRequest never
advanced (controller logs, PR `status.conditions[]`, sidecar `/status`).
This commit:
- Adds `external_snapshot_on_fail()` — best-effort capture of controller
logs (current + `--previous`), PurchaseRequest YAML across all namespaces,
buyer sidecar `/status` (via `kubectl exec ... python3` against the
litellm container — buyer container is distroless), `cluster-pods.txt`,
and recent `cluster-events.txt`. All commands wrapped in `|| true` so a
single failure doesn't abort the bundle. Empty/failed files are removed.
- Calls the snapshot from `external_cleanup` BEFORE any teardown, on the
failure path only — clean exits keep the existing fast-cleanup behavior.
- Honors `KEEP_CLUSTER_ON_FAIL=1` (default unset) — when set, skips
`bob stack down` after the snapshot bundle is written and prints the
preserved stack id + artifact dir + manual cleanup hint.
Unblocks investigation of v1337-style external-seller failures documented
in plans/inference-v1337-buy-report-20260514.md.
…otstrap
`bootstrap_flow_workspace` previously copied unconditionally from the
caller-supplied path (always `$OBOL_ROOT/.build/obol`). When iterating on
embedded skill content (e.g. `internal/embed/skills/buy-x402/scripts/buy.py`)
it's easy to rebuild one of the two binaries and forget the other, silently
baking pre-fix files into the cluster PVC via `syncObolSkills`. Burned six
hours during the v1337 live-buy investigation (attempt 5 in
plans/inference-v1337-buy-report-20260514.md).
Now: stat both paths, pick the one with the larger mtime, and emit a 5-line
WARN to stderr when the two differ by more than 5 minutes — header + both
paths-with-mtimes + which one was picked + a one-line rebuild nudge. Cross-
OS stat handled via `stat -c %Y` with `stat -f %m` fallback. Date formatted
with `date -r <file>` (BSD/macOS friendly), GNU `date -u -d "@<epoch>"`
fallback. Contract preserved (no return value, copies into `$dir/bin/obol`).
Adds entry #10 to the release-smoke debugging reference covering the
HTTP 403 + Cloudflare error 1010 we hit on v1337 attempts 3–4: managed
WAF rules block the default `Python-urllib/X.Y` UA. Documents the buy.py
fix (commit c2dddc1) plus the unconfirmed-but-likely Go-side follow-up
at internal/serviceoffercontroller/purchase.go:183, where Go's
`http.Client` defaults to `User-Agent: Go-http-client/1.1` and may hit
the same WAF block on the controller probe.
Re-ran the v1337 buy with the new KEEP_CLUSTER_ON_FAIL=1 knob (commit
b749f95). The controller reconciled the PurchaseRequest in 55 seconds
through Probed → AuthsLoaded → Configured → Ready, against the same
external endpoint the original report failed on.
The original report's central technical claim — "serviceoffer-controller
does not reconcile PurchaseRequests for external sellers" — is false.
The controller is endpoint-agnostic by design (verified by code review of
internal/serviceoffercontroller/purchase.go). Attempt 5's reconcile-hang
was almost certainly a kubectl-exec session SIGKILL (exit 137), not a
controller bug — likely harness-side run_with_timeout firing while
buy.py was still polling normally.
Today's run did surface a real but unrelated quirk: LiteLLM's POST
/model/new fails with EROFS because /etc/litellm/config.yaml is mounted
read-only as a Kubernetes ConfigMap volume; the controller catches this
and falls back to ConfigMap reload, which works fine. Pre-existing,
worth one line in paid-flows.md so the next debugger isn't startled.
Step 18 (paid request) failed for an operator-error reason: I picked
qwen3.6-27b as the upstream model id, but v1337's vLLM serves under a
different name. Bob's 0.023 OBOL was NOT consumed (LiteLLM 404'd before
the buyer sidecar could settle).
Companion to plans/inference-v1337-buy-report-20260514.md. Retracts
follow-up #1 of that report.
@bussyjd
bussyjd changed the base branch from main to integration/post-490-cleanupsMay 14, 2026 07:17
…_FAIL knob
Replaces the opt-in KEEP_CLUSTER_ON_FAIL=1 env knob (added in b749f95)
with an unconditional rule: cleanup happens iff every step passes. On
FAIL, snapshot the diagnostic bundle and preserve the cluster — every
time, no env override needed.
Also inverts the prior success-side default. The previous design left
the cluster up on success "so the operator can poke around"; in
practice operators re-ran the harness from scratch when they wanted
fresh state, and the leftover cluster mostly leaked across runs. With
the new gate, a green run leaves a clean machine.
Net behavior:
- success → bob stack down (clean state for next run)
- failure → snapshot + preserve (operator pays one manual teardown
when done diagnosing)
The diagnostic snapshot helper from b749f95 is unchanged; only the
preservation gate moved from an env knob to the implicit pass/fail
state.
@bussyjdbussyjd changed the title chore(buy-external): add KEEP_CLUSTER_ON_FAIL knob, normalize obol-bin path, document CF-WAF UAchore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UAMay 15, 2026
@bussyjd
bussyjd merged commit 2a1c1b2 into integration/post-490-cleanupsMay 15, 2026
@OisinKyne
OisinKyne deleted the chore/buy-external-followups branch July 1, 2026 12:34
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.

1 participant

@bussyjd
, '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

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA - #493

Merged
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups
May 15, 2026
Merged

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA#493
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups

Conversation

@bussyjd

@bussyjdbussyjd commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase-1 polish + investigation follow-ups from plans/inference-v1337-buy-report-20260514.md. Net result: the report's central technical claim ("controller doesn't reconcile external-seller PurchaseRequests") is retracted — re-running with the new diagnostic gate proved the controller is endpoint-agnostic by design.

Stacks on #492 (folds in #487 + #489 + post-490 cleanups). Targets integration/post-490-cleanups so the diff shows only the 4 commits this PR adds.

What's in this branch

  • feat(buy-external): KEEP_CLUSTER_ON_FAIL knob + diagnostic snapshot on FAIL (b749f95) — adds external_snapshot_on_fail(). On FAIL, snapshots controller logs (current + --previous), PR YAML across all namespaces, buyer sidecar /status (via kubectl exec ... python3 against the litellm container — buyer is distroless), cluster-pods.txt, and recent cluster-events.txt to the artifact dir before any teardown.

  • fix(flows): pick freshest of .build/obol vs .workspace/bin/obol (eb13055) — bootstrap_flow_workspace in flows/lib.sh now stats both paths, picks the larger mtime, and emits a 5-line WARN when they differ by >5 minutes. Removes the silent-stale-binary footgun documented as v1337 attempt 5.

  • docs(skill): document Cloudflare-WAF UA pitfall (849cd93) — entry Add ADK #10 in release-smoke-debugging.md covering HTTP 403 + Cloudflare error 1010 from default Python-urllib UA, the c2dddc1 buy.py fix, and the (unconfirmed) follow-up about Go's http.Client defaults at purchase.go:183.

  • docs(plans): retract v1337 controller-gap hypothesis (82108c3) — plans/inference-v1337-followup-20260514.md companion to the original report. Re-run on spark1 showed the controller reconciles in 55s through Probed → AuthsLoaded → Configured → Ready. The Go-side probe was NOT WAF-blocked.

  • refactor(buy-external): green-only cleanup gate, drop KEEP_CLUSTER_ON_FAIL knob (df5fcff) — replaces the opt-in env knob with an unconditional rule: cleanup happens iff every step passes. Also inverts the prior success-side default (which left the cluster up on success "so the operator can poke around" — in practice operators re-ran from scratch and the leftover cluster mostly leaked).

Net cleanup behavior

Run outcomeBehavior
Every step PASSbob stack down — clean state for next run
Any step FAILSnapshot + preserve cluster; operator pays one manual bob stack down when done

No env knob; the pass/fail exit code is the gate.

Side findings (worth knowing)

The captured controller log surfaces a pre-existing LiteLLM hot-add quirk:

purchase: hot-add paid/qwen3.6-27b failed: POST /model/new: 400 Bad Request:
{"error":{"message":"Authentication Error, [Errno 30] Read-only file system: '/etc/litellm/config.yaml'", ...}}; relying on ConfigMap reload

LiteLLM's /model/new API tries to write back to the ConfigMap volume (read-only by Kubernetes default). The controller catches the 400 and falls back to the ConfigMap-reload path, which works. Not external-seller specific. Worth a one-liner in paid-flows.md so it stops surprising next-debugger.

Test plan

  • bash -n flows/buy-external.sh && bash -n flows/lib.sh — clean
  • spark1 re-run on chore/buy-external-followups against https://inference.v1337.org/services/aeon:
    • 17 of 18 steps PASS
    • PR Ready=True after 55s, observedGeneration: 1, paid/qwen3.6-27b published, remaining: 1, spent: 0
    • Diagnostic gate preserved cluster on FAIL + 7 snapshot files written
    • Step 18 fails on operator-error model name (qwen3.6-27b ≠ v1337's actual model id); Bob's 0.023 OBOL pre-signed auth was NOT consumed
  • No regressions to existing flows (only lib.sh::bootstrap_flow_workspace touched among shared code; signature preserved, contract unchanged)
  • Reviewer: confirm the green-only cleanup gate is the right default for this single-operator QA harness (CI flows that need different behavior would call out separately)

bussyjd added 4 commits May 14, 2026 12:37
…ot on FAIL
When `flows/buy-external.sh` fails (typically at step 14, the `buy.py buy`
invocation), the existing `external_cleanup` immediately tears the cluster
down — destroying the only places that record why the PurchaseRequest never
advanced (controller logs, PR `status.conditions[]`, sidecar `/status`).
This commit:
- Adds `external_snapshot_on_fail()` — best-effort capture of controller
logs (current + `--previous`), PurchaseRequest YAML across all namespaces,
buyer sidecar `/status` (via `kubectl exec ... python3` against the
litellm container — buyer container is distroless), `cluster-pods.txt`,
and recent `cluster-events.txt`. All commands wrapped in `|| true` so a
single failure doesn't abort the bundle. Empty/failed files are removed.
- Calls the snapshot from `external_cleanup` BEFORE any teardown, on the
failure path only — clean exits keep the existing fast-cleanup behavior.
- Honors `KEEP_CLUSTER_ON_FAIL=1` (default unset) — when set, skips
`bob stack down` after the snapshot bundle is written and prints the
preserved stack id + artifact dir + manual cleanup hint.
Unblocks investigation of v1337-style external-seller failures documented
in plans/inference-v1337-buy-report-20260514.md.
…otstrap
`bootstrap_flow_workspace` previously copied unconditionally from the
caller-supplied path (always `$OBOL_ROOT/.build/obol`). When iterating on
embedded skill content (e.g. `internal/embed/skills/buy-x402/scripts/buy.py`)
it's easy to rebuild one of the two binaries and forget the other, silently
baking pre-fix files into the cluster PVC via `syncObolSkills`. Burned six
hours during the v1337 live-buy investigation (attempt 5 in
plans/inference-v1337-buy-report-20260514.md).
Now: stat both paths, pick the one with the larger mtime, and emit a 5-line
WARN to stderr when the two differ by more than 5 minutes — header + both
paths-with-mtimes + which one was picked + a one-line rebuild nudge. Cross-
OS stat handled via `stat -c %Y` with `stat -f %m` fallback. Date formatted
with `date -r <file>` (BSD/macOS friendly), GNU `date -u -d "@<epoch>"`
fallback. Contract preserved (no return value, copies into `$dir/bin/obol`).
Adds entry #10 to the release-smoke debugging reference covering the
HTTP 403 + Cloudflare error 1010 we hit on v1337 attempts 3–4: managed
WAF rules block the default `Python-urllib/X.Y` UA. Documents the buy.py
fix (commit c2dddc1) plus the unconfirmed-but-likely Go-side follow-up
at internal/serviceoffercontroller/purchase.go:183, where Go's
`http.Client` defaults to `User-Agent: Go-http-client/1.1` and may hit
the same WAF block on the controller probe.
Re-ran the v1337 buy with the new KEEP_CLUSTER_ON_FAIL=1 knob (commit
b749f95). The controller reconciled the PurchaseRequest in 55 seconds
through Probed → AuthsLoaded → Configured → Ready, against the same
external endpoint the original report failed on.
The original report's central technical claim — "serviceoffer-controller
does not reconcile PurchaseRequests for external sellers" — is false.
The controller is endpoint-agnostic by design (verified by code review of
internal/serviceoffercontroller/purchase.go). Attempt 5's reconcile-hang
was almost certainly a kubectl-exec session SIGKILL (exit 137), not a
controller bug — likely harness-side run_with_timeout firing while
buy.py was still polling normally.
Today's run did surface a real but unrelated quirk: LiteLLM's POST
/model/new fails with EROFS because /etc/litellm/config.yaml is mounted
read-only as a Kubernetes ConfigMap volume; the controller catches this
and falls back to ConfigMap reload, which works fine. Pre-existing,
worth one line in paid-flows.md so the next debugger isn't startled.
Step 18 (paid request) failed for an operator-error reason: I picked
qwen3.6-27b as the upstream model id, but v1337's vLLM serves under a
different name. Bob's 0.023 OBOL was NOT consumed (LiteLLM 404'd before
the buyer sidecar could settle).
Companion to plans/inference-v1337-buy-report-20260514.md. Retracts
follow-up #1 of that report.
@bussyjd
bussyjd changed the base branch from main to integration/post-490-cleanupsMay 14, 2026 07:17
…_FAIL knob
Replaces the opt-in KEEP_CLUSTER_ON_FAIL=1 env knob (added in b749f95)
with an unconditional rule: cleanup happens iff every step passes. On
FAIL, snapshot the diagnostic bundle and preserve the cluster — every
time, no env override needed.
Also inverts the prior success-side default. The previous design left
the cluster up on success "so the operator can poke around"; in
practice operators re-ran the harness from scratch when they wanted
fresh state, and the leftover cluster mostly leaked across runs. With
the new gate, a green run leaves a clean machine.
Net behavior:
- success → bob stack down (clean state for next run)
- failure → snapshot + preserve (operator pays one manual teardown
when done diagnosing)
The diagnostic snapshot helper from b749f95 is unchanged; only the
preservation gate moved from an env knob to the implicit pass/fail
state.
@bussyjdbussyjd changed the title chore(buy-external): add KEEP_CLUSTER_ON_FAIL knob, normalize obol-bin path, document CF-WAF UAchore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UAMay 15, 2026
@bussyjd
bussyjd merged commit 2a1c1b2 into integration/post-490-cleanupsMay 15, 2026
@OisinKyne
OisinKyne deleted the chore/buy-external-followups branch July 1, 2026 12:34
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.

1 participant

@bussyjd
, '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

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA - #493

Merged
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups
May 15, 2026
Merged

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA#493
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups

Conversation

@bussyjd

@bussyjdbussyjd commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase-1 polish + investigation follow-ups from plans/inference-v1337-buy-report-20260514.md. Net result: the report's central technical claim ("controller doesn't reconcile external-seller PurchaseRequests") is retracted — re-running with the new diagnostic gate proved the controller is endpoint-agnostic by design.

Stacks on #492 (folds in #487 + #489 + post-490 cleanups). Targets integration/post-490-cleanups so the diff shows only the 4 commits this PR adds.

What's in this branch

  • feat(buy-external): KEEP_CLUSTER_ON_FAIL knob + diagnostic snapshot on FAIL (b749f95) — adds external_snapshot_on_fail(). On FAIL, snapshots controller logs (current + --previous), PR YAML across all namespaces, buyer sidecar /status (via kubectl exec ... python3 against the litellm container — buyer is distroless), cluster-pods.txt, and recent cluster-events.txt to the artifact dir before any teardown.

  • fix(flows): pick freshest of .build/obol vs .workspace/bin/obol (eb13055) — bootstrap_flow_workspace in flows/lib.sh now stats both paths, picks the larger mtime, and emits a 5-line WARN when they differ by >5 minutes. Removes the silent-stale-binary footgun documented as v1337 attempt 5.

  • docs(skill): document Cloudflare-WAF UA pitfall (849cd93) — entry Add ADK #10 in release-smoke-debugging.md covering HTTP 403 + Cloudflare error 1010 from default Python-urllib UA, the c2dddc1 buy.py fix, and the (unconfirmed) follow-up about Go's http.Client defaults at purchase.go:183.

  • docs(plans): retract v1337 controller-gap hypothesis (82108c3) — plans/inference-v1337-followup-20260514.md companion to the original report. Re-run on spark1 showed the controller reconciles in 55s through Probed → AuthsLoaded → Configured → Ready. The Go-side probe was NOT WAF-blocked.

  • refactor(buy-external): green-only cleanup gate, drop KEEP_CLUSTER_ON_FAIL knob (df5fcff) — replaces the opt-in env knob with an unconditional rule: cleanup happens iff every step passes. Also inverts the prior success-side default (which left the cluster up on success "so the operator can poke around" — in practice operators re-ran from scratch and the leftover cluster mostly leaked).

Net cleanup behavior

Run outcomeBehavior
Every step PASSbob stack down — clean state for next run
Any step FAILSnapshot + preserve cluster; operator pays one manual bob stack down when done

No env knob; the pass/fail exit code is the gate.

Side findings (worth knowing)

The captured controller log surfaces a pre-existing LiteLLM hot-add quirk:

purchase: hot-add paid/qwen3.6-27b failed: POST /model/new: 400 Bad Request:
{"error":{"message":"Authentication Error, [Errno 30] Read-only file system: '/etc/litellm/config.yaml'", ...}}; relying on ConfigMap reload

LiteLLM's /model/new API tries to write back to the ConfigMap volume (read-only by Kubernetes default). The controller catches the 400 and falls back to the ConfigMap-reload path, which works. Not external-seller specific. Worth a one-liner in paid-flows.md so it stops surprising next-debugger.

Test plan

  • bash -n flows/buy-external.sh && bash -n flows/lib.sh — clean
  • spark1 re-run on chore/buy-external-followups against https://inference.v1337.org/services/aeon:
    • 17 of 18 steps PASS
    • PR Ready=True after 55s, observedGeneration: 1, paid/qwen3.6-27b published, remaining: 1, spent: 0
    • Diagnostic gate preserved cluster on FAIL + 7 snapshot files written
    • Step 18 fails on operator-error model name (qwen3.6-27b ≠ v1337's actual model id); Bob's 0.023 OBOL pre-signed auth was NOT consumed
  • No regressions to existing flows (only lib.sh::bootstrap_flow_workspace touched among shared code; signature preserved, contract unchanged)
  • Reviewer: confirm the green-only cleanup gate is the right default for this single-operator QA harness (CI flows that need different behavior would call out separately)

bussyjd added 4 commits May 14, 2026 12:37
…ot on FAIL
When `flows/buy-external.sh` fails (typically at step 14, the `buy.py buy`
invocation), the existing `external_cleanup` immediately tears the cluster
down — destroying the only places that record why the PurchaseRequest never
advanced (controller logs, PR `status.conditions[]`, sidecar `/status`).
This commit:
- Adds `external_snapshot_on_fail()` — best-effort capture of controller
logs (current + `--previous`), PurchaseRequest YAML across all namespaces,
buyer sidecar `/status` (via `kubectl exec ... python3` against the
litellm container — buyer container is distroless), `cluster-pods.txt`,
and recent `cluster-events.txt`. All commands wrapped in `|| true` so a
single failure doesn't abort the bundle. Empty/failed files are removed.
- Calls the snapshot from `external_cleanup` BEFORE any teardown, on the
failure path only — clean exits keep the existing fast-cleanup behavior.
- Honors `KEEP_CLUSTER_ON_FAIL=1` (default unset) — when set, skips
`bob stack down` after the snapshot bundle is written and prints the
preserved stack id + artifact dir + manual cleanup hint.
Unblocks investigation of v1337-style external-seller failures documented
in plans/inference-v1337-buy-report-20260514.md.
…otstrap
`bootstrap_flow_workspace` previously copied unconditionally from the
caller-supplied path (always `$OBOL_ROOT/.build/obol`). When iterating on
embedded skill content (e.g. `internal/embed/skills/buy-x402/scripts/buy.py`)
it's easy to rebuild one of the two binaries and forget the other, silently
baking pre-fix files into the cluster PVC via `syncObolSkills`. Burned six
hours during the v1337 live-buy investigation (attempt 5 in
plans/inference-v1337-buy-report-20260514.md).
Now: stat both paths, pick the one with the larger mtime, and emit a 5-line
WARN to stderr when the two differ by more than 5 minutes — header + both
paths-with-mtimes + which one was picked + a one-line rebuild nudge. Cross-
OS stat handled via `stat -c %Y` with `stat -f %m` fallback. Date formatted
with `date -r <file>` (BSD/macOS friendly), GNU `date -u -d "@<epoch>"`
fallback. Contract preserved (no return value, copies into `$dir/bin/obol`).
Adds entry #10 to the release-smoke debugging reference covering the
HTTP 403 + Cloudflare error 1010 we hit on v1337 attempts 3–4: managed
WAF rules block the default `Python-urllib/X.Y` UA. Documents the buy.py
fix (commit c2dddc1) plus the unconfirmed-but-likely Go-side follow-up
at internal/serviceoffercontroller/purchase.go:183, where Go's
`http.Client` defaults to `User-Agent: Go-http-client/1.1` and may hit
the same WAF block on the controller probe.
Re-ran the v1337 buy with the new KEEP_CLUSTER_ON_FAIL=1 knob (commit
b749f95). The controller reconciled the PurchaseRequest in 55 seconds
through Probed → AuthsLoaded → Configured → Ready, against the same
external endpoint the original report failed on.
The original report's central technical claim — "serviceoffer-controller
does not reconcile PurchaseRequests for external sellers" — is false.
The controller is endpoint-agnostic by design (verified by code review of
internal/serviceoffercontroller/purchase.go). Attempt 5's reconcile-hang
was almost certainly a kubectl-exec session SIGKILL (exit 137), not a
controller bug — likely harness-side run_with_timeout firing while
buy.py was still polling normally.
Today's run did surface a real but unrelated quirk: LiteLLM's POST
/model/new fails with EROFS because /etc/litellm/config.yaml is mounted
read-only as a Kubernetes ConfigMap volume; the controller catches this
and falls back to ConfigMap reload, which works fine. Pre-existing,
worth one line in paid-flows.md so the next debugger isn't startled.
Step 18 (paid request) failed for an operator-error reason: I picked
qwen3.6-27b as the upstream model id, but v1337's vLLM serves under a
different name. Bob's 0.023 OBOL was NOT consumed (LiteLLM 404'd before
the buyer sidecar could settle).
Companion to plans/inference-v1337-buy-report-20260514.md. Retracts
follow-up #1 of that report.
@bussyjd
bussyjd changed the base branch from main to integration/post-490-cleanupsMay 14, 2026 07:17
…_FAIL knob
Replaces the opt-in KEEP_CLUSTER_ON_FAIL=1 env knob (added in b749f95)
with an unconditional rule: cleanup happens iff every step passes. On
FAIL, snapshot the diagnostic bundle and preserve the cluster — every
time, no env override needed.
Also inverts the prior success-side default. The previous design left
the cluster up on success "so the operator can poke around"; in
practice operators re-ran the harness from scratch when they wanted
fresh state, and the leftover cluster mostly leaked across runs. With
the new gate, a green run leaves a clean machine.
Net behavior:
- success → bob stack down (clean state for next run)
- failure → snapshot + preserve (operator pays one manual teardown
when done diagnosing)
The diagnostic snapshot helper from b749f95 is unchanged; only the
preservation gate moved from an env knob to the implicit pass/fail
state.
@bussyjdbussyjd changed the title chore(buy-external): add KEEP_CLUSTER_ON_FAIL knob, normalize obol-bin path, document CF-WAF UAchore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UAMay 15, 2026
@bussyjd
bussyjd merged commit 2a1c1b2 into integration/post-490-cleanupsMay 15, 2026
@OisinKyne
OisinKyne deleted the chore/buy-external-followups branch July 1, 2026 12:34
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.

1 participant

@bussyjd
, '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

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA - #493

Merged
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups
May 15, 2026
Merged

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA#493
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups

Conversation

@bussyjd

@bussyjdbussyjd commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase-1 polish + investigation follow-ups from plans/inference-v1337-buy-report-20260514.md. Net result: the report's central technical claim ("controller doesn't reconcile external-seller PurchaseRequests") is retracted — re-running with the new diagnostic gate proved the controller is endpoint-agnostic by design.

Stacks on #492 (folds in #487 + #489 + post-490 cleanups). Targets integration/post-490-cleanups so the diff shows only the 4 commits this PR adds.

What's in this branch

  • feat(buy-external): KEEP_CLUSTER_ON_FAIL knob + diagnostic snapshot on FAIL (b749f95) — adds external_snapshot_on_fail(). On FAIL, snapshots controller logs (current + --previous), PR YAML across all namespaces, buyer sidecar /status (via kubectl exec ... python3 against the litellm container — buyer is distroless), cluster-pods.txt, and recent cluster-events.txt to the artifact dir before any teardown.

  • fix(flows): pick freshest of .build/obol vs .workspace/bin/obol (eb13055) — bootstrap_flow_workspace in flows/lib.sh now stats both paths, picks the larger mtime, and emits a 5-line WARN when they differ by >5 minutes. Removes the silent-stale-binary footgun documented as v1337 attempt 5.

  • docs(skill): document Cloudflare-WAF UA pitfall (849cd93) — entry Add ADK #10 in release-smoke-debugging.md covering HTTP 403 + Cloudflare error 1010 from default Python-urllib UA, the c2dddc1 buy.py fix, and the (unconfirmed) follow-up about Go's http.Client defaults at purchase.go:183.

  • docs(plans): retract v1337 controller-gap hypothesis (82108c3) — plans/inference-v1337-followup-20260514.md companion to the original report. Re-run on spark1 showed the controller reconciles in 55s through Probed → AuthsLoaded → Configured → Ready. The Go-side probe was NOT WAF-blocked.

  • refactor(buy-external): green-only cleanup gate, drop KEEP_CLUSTER_ON_FAIL knob (df5fcff) — replaces the opt-in env knob with an unconditional rule: cleanup happens iff every step passes. Also inverts the prior success-side default (which left the cluster up on success "so the operator can poke around" — in practice operators re-ran from scratch and the leftover cluster mostly leaked).

Net cleanup behavior

Run outcomeBehavior
Every step PASSbob stack down — clean state for next run
Any step FAILSnapshot + preserve cluster; operator pays one manual bob stack down when done

No env knob; the pass/fail exit code is the gate.

Side findings (worth knowing)

The captured controller log surfaces a pre-existing LiteLLM hot-add quirk:

purchase: hot-add paid/qwen3.6-27b failed: POST /model/new: 400 Bad Request:
{"error":{"message":"Authentication Error, [Errno 30] Read-only file system: '/etc/litellm/config.yaml'", ...}}; relying on ConfigMap reload

LiteLLM's /model/new API tries to write back to the ConfigMap volume (read-only by Kubernetes default). The controller catches the 400 and falls back to the ConfigMap-reload path, which works. Not external-seller specific. Worth a one-liner in paid-flows.md so it stops surprising next-debugger.

Test plan

  • bash -n flows/buy-external.sh && bash -n flows/lib.sh — clean
  • spark1 re-run on chore/buy-external-followups against https://inference.v1337.org/services/aeon:
    • 17 of 18 steps PASS
    • PR Ready=True after 55s, observedGeneration: 1, paid/qwen3.6-27b published, remaining: 1, spent: 0
    • Diagnostic gate preserved cluster on FAIL + 7 snapshot files written
    • Step 18 fails on operator-error model name (qwen3.6-27b ≠ v1337's actual model id); Bob's 0.023 OBOL pre-signed auth was NOT consumed
  • No regressions to existing flows (only lib.sh::bootstrap_flow_workspace touched among shared code; signature preserved, contract unchanged)
  • Reviewer: confirm the green-only cleanup gate is the right default for this single-operator QA harness (CI flows that need different behavior would call out separately)

bussyjd added 4 commits May 14, 2026 12:37
…ot on FAIL
When `flows/buy-external.sh` fails (typically at step 14, the `buy.py buy`
invocation), the existing `external_cleanup` immediately tears the cluster
down — destroying the only places that record why the PurchaseRequest never
advanced (controller logs, PR `status.conditions[]`, sidecar `/status`).
This commit:
- Adds `external_snapshot_on_fail()` — best-effort capture of controller
logs (current + `--previous`), PurchaseRequest YAML across all namespaces,
buyer sidecar `/status` (via `kubectl exec ... python3` against the
litellm container — buyer container is distroless), `cluster-pods.txt`,
and recent `cluster-events.txt`. All commands wrapped in `|| true` so a
single failure doesn't abort the bundle. Empty/failed files are removed.
- Calls the snapshot from `external_cleanup` BEFORE any teardown, on the
failure path only — clean exits keep the existing fast-cleanup behavior.
- Honors `KEEP_CLUSTER_ON_FAIL=1` (default unset) — when set, skips
`bob stack down` after the snapshot bundle is written and prints the
preserved stack id + artifact dir + manual cleanup hint.
Unblocks investigation of v1337-style external-seller failures documented
in plans/inference-v1337-buy-report-20260514.md.
…otstrap
`bootstrap_flow_workspace` previously copied unconditionally from the
caller-supplied path (always `$OBOL_ROOT/.build/obol`). When iterating on
embedded skill content (e.g. `internal/embed/skills/buy-x402/scripts/buy.py`)
it's easy to rebuild one of the two binaries and forget the other, silently
baking pre-fix files into the cluster PVC via `syncObolSkills`. Burned six
hours during the v1337 live-buy investigation (attempt 5 in
plans/inference-v1337-buy-report-20260514.md).
Now: stat both paths, pick the one with the larger mtime, and emit a 5-line
WARN to stderr when the two differ by more than 5 minutes — header + both
paths-with-mtimes + which one was picked + a one-line rebuild nudge. Cross-
OS stat handled via `stat -c %Y` with `stat -f %m` fallback. Date formatted
with `date -r <file>` (BSD/macOS friendly), GNU `date -u -d "@<epoch>"`
fallback. Contract preserved (no return value, copies into `$dir/bin/obol`).
Adds entry #10 to the release-smoke debugging reference covering the
HTTP 403 + Cloudflare error 1010 we hit on v1337 attempts 3–4: managed
WAF rules block the default `Python-urllib/X.Y` UA. Documents the buy.py
fix (commit c2dddc1) plus the unconfirmed-but-likely Go-side follow-up
at internal/serviceoffercontroller/purchase.go:183, where Go's
`http.Client` defaults to `User-Agent: Go-http-client/1.1` and may hit
the same WAF block on the controller probe.
Re-ran the v1337 buy with the new KEEP_CLUSTER_ON_FAIL=1 knob (commit
b749f95). The controller reconciled the PurchaseRequest in 55 seconds
through Probed → AuthsLoaded → Configured → Ready, against the same
external endpoint the original report failed on.
The original report's central technical claim — "serviceoffer-controller
does not reconcile PurchaseRequests for external sellers" — is false.
The controller is endpoint-agnostic by design (verified by code review of
internal/serviceoffercontroller/purchase.go). Attempt 5's reconcile-hang
was almost certainly a kubectl-exec session SIGKILL (exit 137), not a
controller bug — likely harness-side run_with_timeout firing while
buy.py was still polling normally.
Today's run did surface a real but unrelated quirk: LiteLLM's POST
/model/new fails with EROFS because /etc/litellm/config.yaml is mounted
read-only as a Kubernetes ConfigMap volume; the controller catches this
and falls back to ConfigMap reload, which works fine. Pre-existing,
worth one line in paid-flows.md so the next debugger isn't startled.
Step 18 (paid request) failed for an operator-error reason: I picked
qwen3.6-27b as the upstream model id, but v1337's vLLM serves under a
different name. Bob's 0.023 OBOL was NOT consumed (LiteLLM 404'd before
the buyer sidecar could settle).
Companion to plans/inference-v1337-buy-report-20260514.md. Retracts
follow-up #1 of that report.
@bussyjd
bussyjd changed the base branch from main to integration/post-490-cleanupsMay 14, 2026 07:17
…_FAIL knob
Replaces the opt-in KEEP_CLUSTER_ON_FAIL=1 env knob (added in b749f95)
with an unconditional rule: cleanup happens iff every step passes. On
FAIL, snapshot the diagnostic bundle and preserve the cluster — every
time, no env override needed.
Also inverts the prior success-side default. The previous design left
the cluster up on success "so the operator can poke around"; in
practice operators re-ran the harness from scratch when they wanted
fresh state, and the leftover cluster mostly leaked across runs. With
the new gate, a green run leaves a clean machine.
Net behavior:
- success → bob stack down (clean state for next run)
- failure → snapshot + preserve (operator pays one manual teardown
when done diagnosing)
The diagnostic snapshot helper from b749f95 is unchanged; only the
preservation gate moved from an env knob to the implicit pass/fail
state.
@bussyjdbussyjd changed the title chore(buy-external): add KEEP_CLUSTER_ON_FAIL knob, normalize obol-bin path, document CF-WAF UAchore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UAMay 15, 2026
@bussyjd
bussyjd merged commit 2a1c1b2 into integration/post-490-cleanupsMay 15, 2026
@OisinKyne
OisinKyne deleted the chore/buy-external-followups branch July 1, 2026 12:34
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.

1 participant

@bussyjd
, '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

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA - #493

Merged
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups
May 15, 2026
Merged

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA#493
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups

Conversation

@bussyjd

@bussyjdbussyjd commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase-1 polish + investigation follow-ups from plans/inference-v1337-buy-report-20260514.md. Net result: the report's central technical claim ("controller doesn't reconcile external-seller PurchaseRequests") is retracted — re-running with the new diagnostic gate proved the controller is endpoint-agnostic by design.

Stacks on #492 (folds in #487 + #489 + post-490 cleanups). Targets integration/post-490-cleanups so the diff shows only the 4 commits this PR adds.

What's in this branch

  • feat(buy-external): KEEP_CLUSTER_ON_FAIL knob + diagnostic snapshot on FAIL (b749f95) — adds external_snapshot_on_fail(). On FAIL, snapshots controller logs (current + --previous), PR YAML across all namespaces, buyer sidecar /status (via kubectl exec ... python3 against the litellm container — buyer is distroless), cluster-pods.txt, and recent cluster-events.txt to the artifact dir before any teardown.

  • fix(flows): pick freshest of .build/obol vs .workspace/bin/obol (eb13055) — bootstrap_flow_workspace in flows/lib.sh now stats both paths, picks the larger mtime, and emits a 5-line WARN when they differ by >5 minutes. Removes the silent-stale-binary footgun documented as v1337 attempt 5.

  • docs(skill): document Cloudflare-WAF UA pitfall (849cd93) — entry Add ADK #10 in release-smoke-debugging.md covering HTTP 403 + Cloudflare error 1010 from default Python-urllib UA, the c2dddc1 buy.py fix, and the (unconfirmed) follow-up about Go's http.Client defaults at purchase.go:183.

  • docs(plans): retract v1337 controller-gap hypothesis (82108c3) — plans/inference-v1337-followup-20260514.md companion to the original report. Re-run on spark1 showed the controller reconciles in 55s through Probed → AuthsLoaded → Configured → Ready. The Go-side probe was NOT WAF-blocked.

  • refactor(buy-external): green-only cleanup gate, drop KEEP_CLUSTER_ON_FAIL knob (df5fcff) — replaces the opt-in env knob with an unconditional rule: cleanup happens iff every step passes. Also inverts the prior success-side default (which left the cluster up on success "so the operator can poke around" — in practice operators re-ran from scratch and the leftover cluster mostly leaked).

Net cleanup behavior

Run outcomeBehavior
Every step PASSbob stack down — clean state for next run
Any step FAILSnapshot + preserve cluster; operator pays one manual bob stack down when done

No env knob; the pass/fail exit code is the gate.

Side findings (worth knowing)

The captured controller log surfaces a pre-existing LiteLLM hot-add quirk:

purchase: hot-add paid/qwen3.6-27b failed: POST /model/new: 400 Bad Request:
{"error":{"message":"Authentication Error, [Errno 30] Read-only file system: '/etc/litellm/config.yaml'", ...}}; relying on ConfigMap reload

LiteLLM's /model/new API tries to write back to the ConfigMap volume (read-only by Kubernetes default). The controller catches the 400 and falls back to the ConfigMap-reload path, which works. Not external-seller specific. Worth a one-liner in paid-flows.md so it stops surprising next-debugger.

Test plan

  • bash -n flows/buy-external.sh && bash -n flows/lib.sh — clean
  • spark1 re-run on chore/buy-external-followups against https://inference.v1337.org/services/aeon:
    • 17 of 18 steps PASS
    • PR Ready=True after 55s, observedGeneration: 1, paid/qwen3.6-27b published, remaining: 1, spent: 0
    • Diagnostic gate preserved cluster on FAIL + 7 snapshot files written
    • Step 18 fails on operator-error model name (qwen3.6-27b ≠ v1337's actual model id); Bob's 0.023 OBOL pre-signed auth was NOT consumed
  • No regressions to existing flows (only lib.sh::bootstrap_flow_workspace touched among shared code; signature preserved, contract unchanged)
  • Reviewer: confirm the green-only cleanup gate is the right default for this single-operator QA harness (CI flows that need different behavior would call out separately)

bussyjd added 4 commits May 14, 2026 12:37
…ot on FAIL
When `flows/buy-external.sh` fails (typically at step 14, the `buy.py buy`
invocation), the existing `external_cleanup` immediately tears the cluster
down — destroying the only places that record why the PurchaseRequest never
advanced (controller logs, PR `status.conditions[]`, sidecar `/status`).
This commit:
- Adds `external_snapshot_on_fail()` — best-effort capture of controller
logs (current + `--previous`), PurchaseRequest YAML across all namespaces,
buyer sidecar `/status` (via `kubectl exec ... python3` against the
litellm container — buyer container is distroless), `cluster-pods.txt`,
and recent `cluster-events.txt`. All commands wrapped in `|| true` so a
single failure doesn't abort the bundle. Empty/failed files are removed.
- Calls the snapshot from `external_cleanup` BEFORE any teardown, on the
failure path only — clean exits keep the existing fast-cleanup behavior.
- Honors `KEEP_CLUSTER_ON_FAIL=1` (default unset) — when set, skips
`bob stack down` after the snapshot bundle is written and prints the
preserved stack id + artifact dir + manual cleanup hint.
Unblocks investigation of v1337-style external-seller failures documented
in plans/inference-v1337-buy-report-20260514.md.
…otstrap
`bootstrap_flow_workspace` previously copied unconditionally from the
caller-supplied path (always `$OBOL_ROOT/.build/obol`). When iterating on
embedded skill content (e.g. `internal/embed/skills/buy-x402/scripts/buy.py`)
it's easy to rebuild one of the two binaries and forget the other, silently
baking pre-fix files into the cluster PVC via `syncObolSkills`. Burned six
hours during the v1337 live-buy investigation (attempt 5 in
plans/inference-v1337-buy-report-20260514.md).
Now: stat both paths, pick the one with the larger mtime, and emit a 5-line
WARN to stderr when the two differ by more than 5 minutes — header + both
paths-with-mtimes + which one was picked + a one-line rebuild nudge. Cross-
OS stat handled via `stat -c %Y` with `stat -f %m` fallback. Date formatted
with `date -r <file>` (BSD/macOS friendly), GNU `date -u -d "@<epoch>"`
fallback. Contract preserved (no return value, copies into `$dir/bin/obol`).
Adds entry #10 to the release-smoke debugging reference covering the
HTTP 403 + Cloudflare error 1010 we hit on v1337 attempts 3–4: managed
WAF rules block the default `Python-urllib/X.Y` UA. Documents the buy.py
fix (commit c2dddc1) plus the unconfirmed-but-likely Go-side follow-up
at internal/serviceoffercontroller/purchase.go:183, where Go's
`http.Client` defaults to `User-Agent: Go-http-client/1.1` and may hit
the same WAF block on the controller probe.
Re-ran the v1337 buy with the new KEEP_CLUSTER_ON_FAIL=1 knob (commit
b749f95). The controller reconciled the PurchaseRequest in 55 seconds
through Probed → AuthsLoaded → Configured → Ready, against the same
external endpoint the original report failed on.
The original report's central technical claim — "serviceoffer-controller
does not reconcile PurchaseRequests for external sellers" — is false.
The controller is endpoint-agnostic by design (verified by code review of
internal/serviceoffercontroller/purchase.go). Attempt 5's reconcile-hang
was almost certainly a kubectl-exec session SIGKILL (exit 137), not a
controller bug — likely harness-side run_with_timeout firing while
buy.py was still polling normally.
Today's run did surface a real but unrelated quirk: LiteLLM's POST
/model/new fails with EROFS because /etc/litellm/config.yaml is mounted
read-only as a Kubernetes ConfigMap volume; the controller catches this
and falls back to ConfigMap reload, which works fine. Pre-existing,
worth one line in paid-flows.md so the next debugger isn't startled.
Step 18 (paid request) failed for an operator-error reason: I picked
qwen3.6-27b as the upstream model id, but v1337's vLLM serves under a
different name. Bob's 0.023 OBOL was NOT consumed (LiteLLM 404'd before
the buyer sidecar could settle).
Companion to plans/inference-v1337-buy-report-20260514.md. Retracts
follow-up #1 of that report.
@bussyjd
bussyjd changed the base branch from main to integration/post-490-cleanupsMay 14, 2026 07:17
…_FAIL knob
Replaces the opt-in KEEP_CLUSTER_ON_FAIL=1 env knob (added in b749f95)
with an unconditional rule: cleanup happens iff every step passes. On
FAIL, snapshot the diagnostic bundle and preserve the cluster — every
time, no env override needed.
Also inverts the prior success-side default. The previous design left
the cluster up on success "so the operator can poke around"; in
practice operators re-ran the harness from scratch when they wanted
fresh state, and the leftover cluster mostly leaked across runs. With
the new gate, a green run leaves a clean machine.
Net behavior:
- success → bob stack down (clean state for next run)
- failure → snapshot + preserve (operator pays one manual teardown
when done diagnosing)
The diagnostic snapshot helper from b749f95 is unchanged; only the
preservation gate moved from an env knob to the implicit pass/fail
state.
@bussyjdbussyjd changed the title chore(buy-external): add KEEP_CLUSTER_ON_FAIL knob, normalize obol-bin path, document CF-WAF UAchore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UAMay 15, 2026
@bussyjd
bussyjd merged commit 2a1c1b2 into integration/post-490-cleanupsMay 15, 2026
@OisinKyne
OisinKyne deleted the chore/buy-external-followups branch July 1, 2026 12:34
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.

1 participant

@bussyjd
, '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

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA - #493

Merged
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups
May 15, 2026
Merged

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA#493
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups

Conversation

@bussyjd

@bussyjdbussyjd commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase-1 polish + investigation follow-ups from plans/inference-v1337-buy-report-20260514.md. Net result: the report's central technical claim ("controller doesn't reconcile external-seller PurchaseRequests") is retracted — re-running with the new diagnostic gate proved the controller is endpoint-agnostic by design.

Stacks on #492 (folds in #487 + #489 + post-490 cleanups). Targets integration/post-490-cleanups so the diff shows only the 4 commits this PR adds.

What's in this branch

  • feat(buy-external): KEEP_CLUSTER_ON_FAIL knob + diagnostic snapshot on FAIL (b749f95) — adds external_snapshot_on_fail(). On FAIL, snapshots controller logs (current + --previous), PR YAML across all namespaces, buyer sidecar /status (via kubectl exec ... python3 against the litellm container — buyer is distroless), cluster-pods.txt, and recent cluster-events.txt to the artifact dir before any teardown.

  • fix(flows): pick freshest of .build/obol vs .workspace/bin/obol (eb13055) — bootstrap_flow_workspace in flows/lib.sh now stats both paths, picks the larger mtime, and emits a 5-line WARN when they differ by >5 minutes. Removes the silent-stale-binary footgun documented as v1337 attempt 5.

  • docs(skill): document Cloudflare-WAF UA pitfall (849cd93) — entry Add ADK #10 in release-smoke-debugging.md covering HTTP 403 + Cloudflare error 1010 from default Python-urllib UA, the c2dddc1 buy.py fix, and the (unconfirmed) follow-up about Go's http.Client defaults at purchase.go:183.

  • docs(plans): retract v1337 controller-gap hypothesis (82108c3) — plans/inference-v1337-followup-20260514.md companion to the original report. Re-run on spark1 showed the controller reconciles in 55s through Probed → AuthsLoaded → Configured → Ready. The Go-side probe was NOT WAF-blocked.

  • refactor(buy-external): green-only cleanup gate, drop KEEP_CLUSTER_ON_FAIL knob (df5fcff) — replaces the opt-in env knob with an unconditional rule: cleanup happens iff every step passes. Also inverts the prior success-side default (which left the cluster up on success "so the operator can poke around" — in practice operators re-ran from scratch and the leftover cluster mostly leaked).

Net cleanup behavior

Run outcomeBehavior
Every step PASSbob stack down — clean state for next run
Any step FAILSnapshot + preserve cluster; operator pays one manual bob stack down when done

No env knob; the pass/fail exit code is the gate.

Side findings (worth knowing)

The captured controller log surfaces a pre-existing LiteLLM hot-add quirk:

purchase: hot-add paid/qwen3.6-27b failed: POST /model/new: 400 Bad Request:
{"error":{"message":"Authentication Error, [Errno 30] Read-only file system: '/etc/litellm/config.yaml'", ...}}; relying on ConfigMap reload

LiteLLM's /model/new API tries to write back to the ConfigMap volume (read-only by Kubernetes default). The controller catches the 400 and falls back to the ConfigMap-reload path, which works. Not external-seller specific. Worth a one-liner in paid-flows.md so it stops surprising next-debugger.

Test plan

  • bash -n flows/buy-external.sh && bash -n flows/lib.sh — clean
  • spark1 re-run on chore/buy-external-followups against https://inference.v1337.org/services/aeon:
    • 17 of 18 steps PASS
    • PR Ready=True after 55s, observedGeneration: 1, paid/qwen3.6-27b published, remaining: 1, spent: 0
    • Diagnostic gate preserved cluster on FAIL + 7 snapshot files written
    • Step 18 fails on operator-error model name (qwen3.6-27b ≠ v1337's actual model id); Bob's 0.023 OBOL pre-signed auth was NOT consumed
  • No regressions to existing flows (only lib.sh::bootstrap_flow_workspace touched among shared code; signature preserved, contract unchanged)
  • Reviewer: confirm the green-only cleanup gate is the right default for this single-operator QA harness (CI flows that need different behavior would call out separately)

bussyjd added 4 commits May 14, 2026 12:37
…ot on FAIL
When `flows/buy-external.sh` fails (typically at step 14, the `buy.py buy`
invocation), the existing `external_cleanup` immediately tears the cluster
down — destroying the only places that record why the PurchaseRequest never
advanced (controller logs, PR `status.conditions[]`, sidecar `/status`).
This commit:
- Adds `external_snapshot_on_fail()` — best-effort capture of controller
logs (current + `--previous`), PurchaseRequest YAML across all namespaces,
buyer sidecar `/status` (via `kubectl exec ... python3` against the
litellm container — buyer container is distroless), `cluster-pods.txt`,
and recent `cluster-events.txt`. All commands wrapped in `|| true` so a
single failure doesn't abort the bundle. Empty/failed files are removed.
- Calls the snapshot from `external_cleanup` BEFORE any teardown, on the
failure path only — clean exits keep the existing fast-cleanup behavior.
- Honors `KEEP_CLUSTER_ON_FAIL=1` (default unset) — when set, skips
`bob stack down` after the snapshot bundle is written and prints the
preserved stack id + artifact dir + manual cleanup hint.
Unblocks investigation of v1337-style external-seller failures documented
in plans/inference-v1337-buy-report-20260514.md.
…otstrap
`bootstrap_flow_workspace` previously copied unconditionally from the
caller-supplied path (always `$OBOL_ROOT/.build/obol`). When iterating on
embedded skill content (e.g. `internal/embed/skills/buy-x402/scripts/buy.py`)
it's easy to rebuild one of the two binaries and forget the other, silently
baking pre-fix files into the cluster PVC via `syncObolSkills`. Burned six
hours during the v1337 live-buy investigation (attempt 5 in
plans/inference-v1337-buy-report-20260514.md).
Now: stat both paths, pick the one with the larger mtime, and emit a 5-line
WARN to stderr when the two differ by more than 5 minutes — header + both
paths-with-mtimes + which one was picked + a one-line rebuild nudge. Cross-
OS stat handled via `stat -c %Y` with `stat -f %m` fallback. Date formatted
with `date -r <file>` (BSD/macOS friendly), GNU `date -u -d "@<epoch>"`
fallback. Contract preserved (no return value, copies into `$dir/bin/obol`).
Adds entry #10 to the release-smoke debugging reference covering the
HTTP 403 + Cloudflare error 1010 we hit on v1337 attempts 3–4: managed
WAF rules block the default `Python-urllib/X.Y` UA. Documents the buy.py
fix (commit c2dddc1) plus the unconfirmed-but-likely Go-side follow-up
at internal/serviceoffercontroller/purchase.go:183, where Go's
`http.Client` defaults to `User-Agent: Go-http-client/1.1` and may hit
the same WAF block on the controller probe.
Re-ran the v1337 buy with the new KEEP_CLUSTER_ON_FAIL=1 knob (commit
b749f95). The controller reconciled the PurchaseRequest in 55 seconds
through Probed → AuthsLoaded → Configured → Ready, against the same
external endpoint the original report failed on.
The original report's central technical claim — "serviceoffer-controller
does not reconcile PurchaseRequests for external sellers" — is false.
The controller is endpoint-agnostic by design (verified by code review of
internal/serviceoffercontroller/purchase.go). Attempt 5's reconcile-hang
was almost certainly a kubectl-exec session SIGKILL (exit 137), not a
controller bug — likely harness-side run_with_timeout firing while
buy.py was still polling normally.
Today's run did surface a real but unrelated quirk: LiteLLM's POST
/model/new fails with EROFS because /etc/litellm/config.yaml is mounted
read-only as a Kubernetes ConfigMap volume; the controller catches this
and falls back to ConfigMap reload, which works fine. Pre-existing,
worth one line in paid-flows.md so the next debugger isn't startled.
Step 18 (paid request) failed for an operator-error reason: I picked
qwen3.6-27b as the upstream model id, but v1337's vLLM serves under a
different name. Bob's 0.023 OBOL was NOT consumed (LiteLLM 404'd before
the buyer sidecar could settle).
Companion to plans/inference-v1337-buy-report-20260514.md. Retracts
follow-up #1 of that report.
@bussyjd
bussyjd changed the base branch from main to integration/post-490-cleanupsMay 14, 2026 07:17
…_FAIL knob
Replaces the opt-in KEEP_CLUSTER_ON_FAIL=1 env knob (added in b749f95)
with an unconditional rule: cleanup happens iff every step passes. On
FAIL, snapshot the diagnostic bundle and preserve the cluster — every
time, no env override needed.
Also inverts the prior success-side default. The previous design left
the cluster up on success "so the operator can poke around"; in
practice operators re-ran the harness from scratch when they wanted
fresh state, and the leftover cluster mostly leaked across runs. With
the new gate, a green run leaves a clean machine.
Net behavior:
- success → bob stack down (clean state for next run)
- failure → snapshot + preserve (operator pays one manual teardown
when done diagnosing)
The diagnostic snapshot helper from b749f95 is unchanged; only the
preservation gate moved from an env knob to the implicit pass/fail
state.
@bussyjdbussyjd changed the title chore(buy-external): add KEEP_CLUSTER_ON_FAIL knob, normalize obol-bin path, document CF-WAF UAchore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UAMay 15, 2026
@bussyjd
bussyjd merged commit 2a1c1b2 into integration/post-490-cleanupsMay 15, 2026
@OisinKyne
OisinKyne deleted the chore/buy-external-followups branch July 1, 2026 12:34
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.

1 participant

@bussyjd
, '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

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA - #493

Merged
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups
May 15, 2026
Merged

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA#493
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups

Conversation

@bussyjd

@bussyjdbussyjd commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase-1 polish + investigation follow-ups from plans/inference-v1337-buy-report-20260514.md. Net result: the report's central technical claim ("controller doesn't reconcile external-seller PurchaseRequests") is retracted — re-running with the new diagnostic gate proved the controller is endpoint-agnostic by design.

Stacks on #492 (folds in #487 + #489 + post-490 cleanups). Targets integration/post-490-cleanups so the diff shows only the 4 commits this PR adds.

What's in this branch

  • feat(buy-external): KEEP_CLUSTER_ON_FAIL knob + diagnostic snapshot on FAIL (b749f95) — adds external_snapshot_on_fail(). On FAIL, snapshots controller logs (current + --previous), PR YAML across all namespaces, buyer sidecar /status (via kubectl exec ... python3 against the litellm container — buyer is distroless), cluster-pods.txt, and recent cluster-events.txt to the artifact dir before any teardown.

  • fix(flows): pick freshest of .build/obol vs .workspace/bin/obol (eb13055) — bootstrap_flow_workspace in flows/lib.sh now stats both paths, picks the larger mtime, and emits a 5-line WARN when they differ by >5 minutes. Removes the silent-stale-binary footgun documented as v1337 attempt 5.

  • docs(skill): document Cloudflare-WAF UA pitfall (849cd93) — entry Add ADK #10 in release-smoke-debugging.md covering HTTP 403 + Cloudflare error 1010 from default Python-urllib UA, the c2dddc1 buy.py fix, and the (unconfirmed) follow-up about Go's http.Client defaults at purchase.go:183.

  • docs(plans): retract v1337 controller-gap hypothesis (82108c3) — plans/inference-v1337-followup-20260514.md companion to the original report. Re-run on spark1 showed the controller reconciles in 55s through Probed → AuthsLoaded → Configured → Ready. The Go-side probe was NOT WAF-blocked.

  • refactor(buy-external): green-only cleanup gate, drop KEEP_CLUSTER_ON_FAIL knob (df5fcff) — replaces the opt-in env knob with an unconditional rule: cleanup happens iff every step passes. Also inverts the prior success-side default (which left the cluster up on success "so the operator can poke around" — in practice operators re-ran from scratch and the leftover cluster mostly leaked).

Net cleanup behavior

Run outcomeBehavior
Every step PASSbob stack down — clean state for next run
Any step FAILSnapshot + preserve cluster; operator pays one manual bob stack down when done

No env knob; the pass/fail exit code is the gate.

Side findings (worth knowing)

The captured controller log surfaces a pre-existing LiteLLM hot-add quirk:

purchase: hot-add paid/qwen3.6-27b failed: POST /model/new: 400 Bad Request:
{"error":{"message":"Authentication Error, [Errno 30] Read-only file system: '/etc/litellm/config.yaml'", ...}}; relying on ConfigMap reload

LiteLLM's /model/new API tries to write back to the ConfigMap volume (read-only by Kubernetes default). The controller catches the 400 and falls back to the ConfigMap-reload path, which works. Not external-seller specific. Worth a one-liner in paid-flows.md so it stops surprising next-debugger.

Test plan

  • bash -n flows/buy-external.sh && bash -n flows/lib.sh — clean
  • spark1 re-run on chore/buy-external-followups against https://inference.v1337.org/services/aeon:
    • 17 of 18 steps PASS
    • PR Ready=True after 55s, observedGeneration: 1, paid/qwen3.6-27b published, remaining: 1, spent: 0
    • Diagnostic gate preserved cluster on FAIL + 7 snapshot files written
    • Step 18 fails on operator-error model name (qwen3.6-27b ≠ v1337's actual model id); Bob's 0.023 OBOL pre-signed auth was NOT consumed
  • No regressions to existing flows (only lib.sh::bootstrap_flow_workspace touched among shared code; signature preserved, contract unchanged)
  • Reviewer: confirm the green-only cleanup gate is the right default for this single-operator QA harness (CI flows that need different behavior would call out separately)

bussyjd added 4 commits May 14, 2026 12:37
…ot on FAIL
When `flows/buy-external.sh` fails (typically at step 14, the `buy.py buy`
invocation), the existing `external_cleanup` immediately tears the cluster
down — destroying the only places that record why the PurchaseRequest never
advanced (controller logs, PR `status.conditions[]`, sidecar `/status`).
This commit:
- Adds `external_snapshot_on_fail()` — best-effort capture of controller
logs (current + `--previous`), PurchaseRequest YAML across all namespaces,
buyer sidecar `/status` (via `kubectl exec ... python3` against the
litellm container — buyer container is distroless), `cluster-pods.txt`,
and recent `cluster-events.txt`. All commands wrapped in `|| true` so a
single failure doesn't abort the bundle. Empty/failed files are removed.
- Calls the snapshot from `external_cleanup` BEFORE any teardown, on the
failure path only — clean exits keep the existing fast-cleanup behavior.
- Honors `KEEP_CLUSTER_ON_FAIL=1` (default unset) — when set, skips
`bob stack down` after the snapshot bundle is written and prints the
preserved stack id + artifact dir + manual cleanup hint.
Unblocks investigation of v1337-style external-seller failures documented
in plans/inference-v1337-buy-report-20260514.md.
…otstrap
`bootstrap_flow_workspace` previously copied unconditionally from the
caller-supplied path (always `$OBOL_ROOT/.build/obol`). When iterating on
embedded skill content (e.g. `internal/embed/skills/buy-x402/scripts/buy.py`)
it's easy to rebuild one of the two binaries and forget the other, silently
baking pre-fix files into the cluster PVC via `syncObolSkills`. Burned six
hours during the v1337 live-buy investigation (attempt 5 in
plans/inference-v1337-buy-report-20260514.md).
Now: stat both paths, pick the one with the larger mtime, and emit a 5-line
WARN to stderr when the two differ by more than 5 minutes — header + both
paths-with-mtimes + which one was picked + a one-line rebuild nudge. Cross-
OS stat handled via `stat -c %Y` with `stat -f %m` fallback. Date formatted
with `date -r <file>` (BSD/macOS friendly), GNU `date -u -d "@<epoch>"`
fallback. Contract preserved (no return value, copies into `$dir/bin/obol`).
Adds entry #10 to the release-smoke debugging reference covering the
HTTP 403 + Cloudflare error 1010 we hit on v1337 attempts 3–4: managed
WAF rules block the default `Python-urllib/X.Y` UA. Documents the buy.py
fix (commit c2dddc1) plus the unconfirmed-but-likely Go-side follow-up
at internal/serviceoffercontroller/purchase.go:183, where Go's
`http.Client` defaults to `User-Agent: Go-http-client/1.1` and may hit
the same WAF block on the controller probe.
Re-ran the v1337 buy with the new KEEP_CLUSTER_ON_FAIL=1 knob (commit
b749f95). The controller reconciled the PurchaseRequest in 55 seconds
through Probed → AuthsLoaded → Configured → Ready, against the same
external endpoint the original report failed on.
The original report's central technical claim — "serviceoffer-controller
does not reconcile PurchaseRequests for external sellers" — is false.
The controller is endpoint-agnostic by design (verified by code review of
internal/serviceoffercontroller/purchase.go). Attempt 5's reconcile-hang
was almost certainly a kubectl-exec session SIGKILL (exit 137), not a
controller bug — likely harness-side run_with_timeout firing while
buy.py was still polling normally.
Today's run did surface a real but unrelated quirk: LiteLLM's POST
/model/new fails with EROFS because /etc/litellm/config.yaml is mounted
read-only as a Kubernetes ConfigMap volume; the controller catches this
and falls back to ConfigMap reload, which works fine. Pre-existing,
worth one line in paid-flows.md so the next debugger isn't startled.
Step 18 (paid request) failed for an operator-error reason: I picked
qwen3.6-27b as the upstream model id, but v1337's vLLM serves under a
different name. Bob's 0.023 OBOL was NOT consumed (LiteLLM 404'd before
the buyer sidecar could settle).
Companion to plans/inference-v1337-buy-report-20260514.md. Retracts
follow-up #1 of that report.
@bussyjd
bussyjd changed the base branch from main to integration/post-490-cleanupsMay 14, 2026 07:17
…_FAIL knob
Replaces the opt-in KEEP_CLUSTER_ON_FAIL=1 env knob (added in b749f95)
with an unconditional rule: cleanup happens iff every step passes. On
FAIL, snapshot the diagnostic bundle and preserve the cluster — every
time, no env override needed.
Also inverts the prior success-side default. The previous design left
the cluster up on success "so the operator can poke around"; in
practice operators re-ran the harness from scratch when they wanted
fresh state, and the leftover cluster mostly leaked across runs. With
the new gate, a green run leaves a clean machine.
Net behavior:
- success → bob stack down (clean state for next run)
- failure → snapshot + preserve (operator pays one manual teardown
when done diagnosing)
The diagnostic snapshot helper from b749f95 is unchanged; only the
preservation gate moved from an env knob to the implicit pass/fail
state.
@bussyjdbussyjd changed the title chore(buy-external): add KEEP_CLUSTER_ON_FAIL knob, normalize obol-bin path, document CF-WAF UAchore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UAMay 15, 2026
@bussyjd
bussyjd merged commit 2a1c1b2 into integration/post-490-cleanupsMay 15, 2026
@OisinKyne
OisinKyne deleted the chore/buy-external-followups branch July 1, 2026 12:34
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.

1 participant

@bussyjd
, '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

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA - #493

Merged
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups
May 15, 2026
Merged

chore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UA#493
bussyjd merged 5 commits into
integration/post-490-cleanupsfrom
chore/buy-external-followups

Conversation

@bussyjd

@bussyjdbussyjd commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase-1 polish + investigation follow-ups from plans/inference-v1337-buy-report-20260514.md. Net result: the report's central technical claim ("controller doesn't reconcile external-seller PurchaseRequests") is retracted — re-running with the new diagnostic gate proved the controller is endpoint-agnostic by design.

Stacks on #492 (folds in #487 + #489 + post-490 cleanups). Targets integration/post-490-cleanups so the diff shows only the 4 commits this PR adds.

What's in this branch

  • feat(buy-external): KEEP_CLUSTER_ON_FAIL knob + diagnostic snapshot on FAIL (b749f95) — adds external_snapshot_on_fail(). On FAIL, snapshots controller logs (current + --previous), PR YAML across all namespaces, buyer sidecar /status (via kubectl exec ... python3 against the litellm container — buyer is distroless), cluster-pods.txt, and recent cluster-events.txt to the artifact dir before any teardown.

  • fix(flows): pick freshest of .build/obol vs .workspace/bin/obol (eb13055) — bootstrap_flow_workspace in flows/lib.sh now stats both paths, picks the larger mtime, and emits a 5-line WARN when they differ by >5 minutes. Removes the silent-stale-binary footgun documented as v1337 attempt 5.

  • docs(skill): document Cloudflare-WAF UA pitfall (849cd93) — entry Add ADK #10 in release-smoke-debugging.md covering HTTP 403 + Cloudflare error 1010 from default Python-urllib UA, the c2dddc1 buy.py fix, and the (unconfirmed) follow-up about Go's http.Client defaults at purchase.go:183.

  • docs(plans): retract v1337 controller-gap hypothesis (82108c3) — plans/inference-v1337-followup-20260514.md companion to the original report. Re-run on spark1 showed the controller reconciles in 55s through Probed → AuthsLoaded → Configured → Ready. The Go-side probe was NOT WAF-blocked.

  • refactor(buy-external): green-only cleanup gate, drop KEEP_CLUSTER_ON_FAIL knob (df5fcff) — replaces the opt-in env knob with an unconditional rule: cleanup happens iff every step passes. Also inverts the prior success-side default (which left the cluster up on success "so the operator can poke around" — in practice operators re-ran from scratch and the leftover cluster mostly leaked).

Net cleanup behavior

Run outcomeBehavior
Every step PASSbob stack down — clean state for next run
Any step FAILSnapshot + preserve cluster; operator pays one manual bob stack down when done

No env knob; the pass/fail exit code is the gate.

Side findings (worth knowing)

The captured controller log surfaces a pre-existing LiteLLM hot-add quirk:

purchase: hot-add paid/qwen3.6-27b failed: POST /model/new: 400 Bad Request:
{"error":{"message":"Authentication Error, [Errno 30] Read-only file system: '/etc/litellm/config.yaml'", ...}}; relying on ConfigMap reload

LiteLLM's /model/new API tries to write back to the ConfigMap volume (read-only by Kubernetes default). The controller catches the 400 and falls back to the ConfigMap-reload path, which works. Not external-seller specific. Worth a one-liner in paid-flows.md so it stops surprising next-debugger.

Test plan

  • bash -n flows/buy-external.sh && bash -n flows/lib.sh — clean
  • spark1 re-run on chore/buy-external-followups against https://inference.v1337.org/services/aeon:
    • 17 of 18 steps PASS
    • PR Ready=True after 55s, observedGeneration: 1, paid/qwen3.6-27b published, remaining: 1, spent: 0
    • Diagnostic gate preserved cluster on FAIL + 7 snapshot files written
    • Step 18 fails on operator-error model name (qwen3.6-27b ≠ v1337's actual model id); Bob's 0.023 OBOL pre-signed auth was NOT consumed
  • No regressions to existing flows (only lib.sh::bootstrap_flow_workspace touched among shared code; signature preserved, contract unchanged)
  • Reviewer: confirm the green-only cleanup gate is the right default for this single-operator QA harness (CI flows that need different behavior would call out separately)

bussyjd added 4 commits May 14, 2026 12:37
…ot on FAIL
When `flows/buy-external.sh` fails (typically at step 14, the `buy.py buy`
invocation), the existing `external_cleanup` immediately tears the cluster
down — destroying the only places that record why the PurchaseRequest never
advanced (controller logs, PR `status.conditions[]`, sidecar `/status`).
This commit:
- Adds `external_snapshot_on_fail()` — best-effort capture of controller
logs (current + `--previous`), PurchaseRequest YAML across all namespaces,
buyer sidecar `/status` (via `kubectl exec ... python3` against the
litellm container — buyer container is distroless), `cluster-pods.txt`,
and recent `cluster-events.txt`. All commands wrapped in `|| true` so a
single failure doesn't abort the bundle. Empty/failed files are removed.
- Calls the snapshot from `external_cleanup` BEFORE any teardown, on the
failure path only — clean exits keep the existing fast-cleanup behavior.
- Honors `KEEP_CLUSTER_ON_FAIL=1` (default unset) — when set, skips
`bob stack down` after the snapshot bundle is written and prints the
preserved stack id + artifact dir + manual cleanup hint.
Unblocks investigation of v1337-style external-seller failures documented
in plans/inference-v1337-buy-report-20260514.md.
…otstrap
`bootstrap_flow_workspace` previously copied unconditionally from the
caller-supplied path (always `$OBOL_ROOT/.build/obol`). When iterating on
embedded skill content (e.g. `internal/embed/skills/buy-x402/scripts/buy.py`)
it's easy to rebuild one of the two binaries and forget the other, silently
baking pre-fix files into the cluster PVC via `syncObolSkills`. Burned six
hours during the v1337 live-buy investigation (attempt 5 in
plans/inference-v1337-buy-report-20260514.md).
Now: stat both paths, pick the one with the larger mtime, and emit a 5-line
WARN to stderr when the two differ by more than 5 minutes — header + both
paths-with-mtimes + which one was picked + a one-line rebuild nudge. Cross-
OS stat handled via `stat -c %Y` with `stat -f %m` fallback. Date formatted
with `date -r <file>` (BSD/macOS friendly), GNU `date -u -d "@<epoch>"`
fallback. Contract preserved (no return value, copies into `$dir/bin/obol`).
Adds entry #10 to the release-smoke debugging reference covering the
HTTP 403 + Cloudflare error 1010 we hit on v1337 attempts 3–4: managed
WAF rules block the default `Python-urllib/X.Y` UA. Documents the buy.py
fix (commit c2dddc1) plus the unconfirmed-but-likely Go-side follow-up
at internal/serviceoffercontroller/purchase.go:183, where Go's
`http.Client` defaults to `User-Agent: Go-http-client/1.1` and may hit
the same WAF block on the controller probe.
Re-ran the v1337 buy with the new KEEP_CLUSTER_ON_FAIL=1 knob (commit
b749f95). The controller reconciled the PurchaseRequest in 55 seconds
through Probed → AuthsLoaded → Configured → Ready, against the same
external endpoint the original report failed on.
The original report's central technical claim — "serviceoffer-controller
does not reconcile PurchaseRequests for external sellers" — is false.
The controller is endpoint-agnostic by design (verified by code review of
internal/serviceoffercontroller/purchase.go). Attempt 5's reconcile-hang
was almost certainly a kubectl-exec session SIGKILL (exit 137), not a
controller bug — likely harness-side run_with_timeout firing while
buy.py was still polling normally.
Today's run did surface a real but unrelated quirk: LiteLLM's POST
/model/new fails with EROFS because /etc/litellm/config.yaml is mounted
read-only as a Kubernetes ConfigMap volume; the controller catches this
and falls back to ConfigMap reload, which works fine. Pre-existing,
worth one line in paid-flows.md so the next debugger isn't startled.
Step 18 (paid request) failed for an operator-error reason: I picked
qwen3.6-27b as the upstream model id, but v1337's vLLM serves under a
different name. Bob's 0.023 OBOL was NOT consumed (LiteLLM 404'd before
the buyer sidecar could settle).
Companion to plans/inference-v1337-buy-report-20260514.md. Retracts
follow-up #1 of that report.
@bussyjd
bussyjd changed the base branch from main to integration/post-490-cleanupsMay 14, 2026 07:17
…_FAIL knob
Replaces the opt-in KEEP_CLUSTER_ON_FAIL=1 env knob (added in b749f95)
with an unconditional rule: cleanup happens iff every step passes. On
FAIL, snapshot the diagnostic bundle and preserve the cluster — every
time, no env override needed.
Also inverts the prior success-side default. The previous design left
the cluster up on success "so the operator can poke around"; in
practice operators re-ran the harness from scratch when they wanted
fresh state, and the leftover cluster mostly leaked across runs. With
the new gate, a green run leaves a clean machine.
Net behavior:
- success → bob stack down (clean state for next run)
- failure → snapshot + preserve (operator pays one manual teardown
when done diagnosing)
The diagnostic snapshot helper from b749f95 is unchanged; only the
preservation gate moved from an env knob to the implicit pass/fail
state.
@bussyjdbussyjd changed the title chore(buy-external): add KEEP_CLUSTER_ON_FAIL knob, normalize obol-bin path, document CF-WAF UAchore(buy-external): preserve cluster on FAIL, normalize obol-bin path, document CF-WAF UAMay 15, 2026
@bussyjd
bussyjd merged commit 2a1c1b2 into integration/post-490-cleanupsMay 15, 2026
@OisinKyne
OisinKyne deleted the chore/buy-external-followups branch July 1, 2026 12:34
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.

1 participant

@bussyjd