Skip to content

feat(sell): loud resume failures, clock-skew preflight, honest status staleness#705

Open
OisinKyne wants to merge 1 commit into
oisin/permanent-appsfrom
oisin/fable-trust
Open

feat(sell): loud resume failures, clock-skew preflight, honest status staleness#705
OisinKyne wants to merge 1 commit into
oisin/permanent-appsfrom
oisin/fable-trust

Conversation

@OisinKyne

Copy link
Copy Markdown
Contributor

Summary

Three fixes sharing one theme: can a seller trust this stack with real revenue? Each closes a way the stack could look healthy while money silently stopped flowing.

1. Partial resumes are now loud

Previously resumeSellOffers warned per-offer and returned nil — after a reboot, a seller reading "stack up: OK" could have offers that never came back and no signal beyond a scrolled-past warning. Now:

  • Failures are collected across both stores (inference gateways + the http/agent manifest ledger) and summarized unmissably at the end:
    ✗ 2 sell offer(s) failed to resume and are NOT live for buyers: my-qwen, llm/ollama-gated
      Retry with `obol sell resume`; inspect with `obol sell status <name> -n <namespace>`.
    
  • obol sell resume exits non-zero on any failed offer — so the systemd boot unit reports failure instead of green.
  • obol stack up keeps warn-and-continue (one broken offer must not block cluster startup) but now surfaces the same named-offer summary.

The test that pinned the old silent behavior (TestResumeSellOffers_HTTPOnlyStore) was deliberately flipped: it now asserts the aggregate error is returned and names the failed offer.

2. Clock-skew preflight

EIP-3009 authorizations anchor validAfter/validBefore to wall clocks. A drifted seller host makes the facilitator reject every buyer payment with authorization is not yet valid while sell status looks perfectly healthy — the classic silent-revenue-stop (see CLAUDE.md pitfall 18 for the anvil-fork variant of the same failure).

New x402.MeasureClockSkew probes the facilitator's HTTP Date header (3s timeout, request-midpoint latency correction). warnOnClockSkew fires above ±30s — comfortably over Date-header resolution + network noise, well under the drift that breaks payments:

⚠ This machine's clock is 2m10s ahead of the payment facilitator (https://x402.gcp.obol.tech).
  Buyer payment authorizations may be rejected ("authorization is not yet valid") and revenue silently stops.
  Fix: enable NTP time sync (macOS: Date & Time → set automatically; Linux: timedatectl set-ntp true).

Called at the four moments a seller acts: sell inference (honoring its --facilitator flag), sell http, sell agent, sell resume. Probe failures are silent by design — the check may never block or delay selling. Deliberately not wired into sell status (runs inside release-smoke loops; an external 3s probe per call would slow them) or stack up directly (reached via resume).

3. Honest status staleness

sell status <name> conditions are the controller's reconcile snapshot, not live counters — a known trap (CLAUDE.md "PurchaseRequest status caveat"). Conditions now show their age and the output says what it is:

Conditions:
  ✓ Ready: Ready — all checks passed (changed 3h ago)

Note: conditions are the controller's last reconciled snapshot — they update on
state changes, not per paid request. The chain is the canonical revenue record.

Labeled "changed" (not "updated") because lastTransitionTime only moves on state flips — calling it "last updated" would misrepresent a healthy long-running offer. Zero timestamps render no age. The closing line echoes docs/observability.md's chain-is-canonical doctrine.

Testing

  • Clock skew: 4 tests (2-minute offset detected within tolerance, in-sync stays under threshold, missing Date header errors, unreachable facilitator errors) against httptest servers.
  • Resume: flipped TestResumeSellOffers_HTTPOnlyStore pins the loud contract; TestResumeSellHTTPOffers_EmptyStoreNoOp extended for the new two-value return.
  • Status: TestFormatConditionLine_ShowsAge (incl. zero-time guard), TestServiceOfferStatusLines_SnapshotCaveat (caveat only when conditions exist), TestHumanAge table test.
  • Full suite green: go build ./... && go test ./...

Review focus

  • cmd/obol/sell.go resumeSellOffers: error semantics — sell resume must fail on partial resume, stack up must not; per-offer loop must still continue past failures.
  • internal/x402/clockskew.go: read-only GET, bounded by a 3s context, no interaction with verification/settlement code paths.
  • warnOnClockSkew call sites: preflight only — nothing downstream may depend on its outcome.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant