Skip to content

docs(x402): document error_detail.payment_rejected on the 402 challenge (closes #46, supersedes #45) - #76

Merged
yakimoto merged 1 commit into
mainfrom
fix/audit-402-payment-rejected
Sep 3, 2026
Merged

docs(x402): document error_detail.payment_rejected on the 402 challenge (closes #46, supersedes #45)#76
yakimoto merged 1 commit into
mainfrom
fix/audit-402-payment-rejected

Conversation

@yakimoto

@yakimoto yakimoto commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Breaking: yes

Breaking: yes — acknowledged on purpose. oasdiff flags the X402PaymentRequired.error_detail shape change (previously $ref: Error, an { error: {...} } wrapper; now allOf: [ErrorBody, { payment_rejected }], the flat object). The wire did not change: the live gateway has always returned the flat body — receipt curl -s https://api.wave.online/v1/clips | python3 -c 'import sys,json;print(list(json.load(sys.stdin)["error_detail"].keys()))'['code','message','session_id','suggestions','doc_url','next_action'] (no error wrapper). The spec was wrong, the API is unchanged; any generated client that trusted the old shape was already failing to read error_detail. Only the contract document moves.

Motivation

Issue #46 (OPEN): the 402 x402 challenge contract omits error_detail.payment_rejected, so generated clients cannot discover the deny reason on a rejected payment. Confirmed on origin/main before this change: git -C ~/wave-av/api-spec grep -n "payment_rejected" origin/main -- openapi.yaml returned no hits, while the gateway (wave-gateway src/x402-envelope.ts) publishes error_detail.payment_rejected: { reason, rail } on every 402 that follows a rejected payment.

An earlier PR (#45, branch docs/x402-payment-rejected-field, 2026-08-12) documented this but now conflicts with origin/main. This PR re-applies the same documentation by hand on top of current main and supersedes #45 (do not merge #45).

Root cause / what changed

While verifying the field against a live receipt, the base error_detail schema was also found to be wrong: it $ref'd the Error envelope ({ error: { code, message, ... } }), but the gateway nests the bare error object directly under error_detail — no inner error wrapper. Documenting payment_rejected correctly required fixing this first, or the new field would sit at the wrong JSON path in generated types.

openapi.yaml:

  • Extracted the Error envelope's inner object as a new ErrorBody component schema. Error.error now $refs ErrorBody — every one of the other 21 $ref: '#/components/schemas/Error' usages in the spec is unchanged (still requires error: ErrorBody).
  • X402PaymentRequired.error_detail now composes allOf: [ErrorBody, { payment_rejected }] instead of $ref: Error, matching the live wire shape (flat object, no error wrapper).
  • Added error_detail.payment_rejected: { reason: string, rail: string } (both required when the object is present), documented as present only when a submitted payment credential was rejected. reason is a stable token — the generic fallback is payment_rejected; more specific tokens name the failing condition (e.g. invalid_permit_header, session_expired, duplicate, rate_limited). rail names the payment rail that rejected it.

generated/api-types.d.ts regenerated from the updated spec (the sdk-types CI gate fails on drift).

CHANGELOG.md: Fixed entry for the error_detail nesting bug, Added entry for payment_rejected (closes #46).

Live receipts

Unauthenticated challenge (no payment_rejected, as expected):

$ curl -s https://api.wave.online/v1/clips | python3 -c 'import sys,json;print(json.load(sys.stdin)["error_detail"])'
{'code': 'PAYMENT_REQUIRED', 'message': 'Payment required. Complete the x402 challenge in `accepts` to proceed.', 'session_id': '...', 'suggestions': [...], 'doc_url': 'https://gateway.wave.online/.well-known/payments.json', 'next_action': {'type': 'none', 'reason': '...'}}

Confirms error_detail is a FLAT object (code/message/session_id/suggestions/doc_url siblings) — never wrapped under an error key. This is what motivated the ErrorBody extraction above.

Rejected payment (malformed credential):

$ curl -s -H "X-PAYMENT: bm90LWEtcGF5bG9hZA==" https://api.wave.online/v1/clips | python3 -m json.tool | head -40
...
"error_detail": {
    "code": "PAYMENT_REQUIRED",
    "message": "The submitted payment was REJECTED (see `error_detail.payment_rejected`). Correct it and retry the x402 challenge in `accepts`.",
    "session_id": "...",
    "payment_rejected": {
        "reason": "invalid_payment_header",
        "rail": "base-usdc"
    },
    "suggestions": [...],
    "doc_url": "https://gateway.wave.online/.well-known/payments.json",
    ...
}

Confirms the exact shape now documented: error_detail.payment_rejected: { reason, rail }, both strings, sibling to code/message/suggestions/doc_url.

Gates run (this branch, off origin/main)

redocly lint openapi.yaml (2.40.0) — valid, 55 warnings (identical count to origin/main baseline — zero new warnings/errors introduced):

openapi.yaml: validated in 95ms
Woohoo! Your API description is valid. 🎉
You have 55 warnings.

node .github/scripts/assert-refs.mjs openapi.yaml (mirrors the spec-lint CI job):

assert-refs: 236 $ref(s) in openapi.yaml, all resolve

openapi-typescript@7.13.0 openapi.yaml -o generated/api-types.d.ts (mirrors the sdk-types CI job) — succeeded, pre-existing warnings only (unused components, missing 4xx on unrelated operations — unchanged by this PR); regenerated file is committed so CI's git diff --exit-code step is clean.

Generated type spot-check confirms the fix:

error_detail?: components["schemas"]["ErrorBody"] & {
    payment_rejected?: {
        reason: string;
        rail: string;
    };
};

Operator steps

None. No secrets, no infra, no deploy — spec-only change.

Closes #46. Supersedes #45 — the old branch is stale and conflicts with origin/main; do not merge it.

🤖 Generated with Claude Code
https://claude.ai/code/session_01K9mRh8G2ugbUt2kaXvFvF6


Note

Low Risk
Spec and generated types only; no gateway or runtime behavior changes. SDK consumers may need to stop reading error_detail.error in favor of flat code/message, which matches production wire format.

Overview
Aligns the OpenAPI contract for HTTP 402 x402 challenges with what the gateway actually returns, and documents rejection diagnostics for paying clients.

error_detail shape fix: The spec previously pointed X402PaymentRequired.error_detail at the full Error envelope ({ error: { code, message, ... } }), but live 402 bodies put the normalized error object flat under error_detail (no inner error key). This PR extracts that inner object as a reusable ErrorBody schema (Error.error now $refs it; other Error responses are unchanged) and models error_detail as ErrorBody instead, so generated/api-types.d.ts no longer implies a bogus error_detail.error path.

Rejected-payment field: error_detail is extended (via allOf) with optional payment_rejected: { reason, rail }, present only when a submitted payment was denied—not on a plain unpaid challenge. CHANGELOG.md records both the nesting fix and the new field (closes #46).

Reviewed by Cursor Bugbot for commit 530e54d. Bugbot is set up for automated code reviews on this repo. Configure here.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by Sourcery

Align the x402 payment-required schema and generated types with the gateway’s live error responses, including rejected-payment diagnostics.

New Features:

  • Document the conditional X402PaymentRequired.error_detail.payment_rejected denial details, including the rejection reason and payment rail.

Bug Fixes:

  • Correct the x402 error_detail schema to model the gateway’s flat error object instead of incorrectly requiring an inner error envelope.

Enhancements:

  • Extract the shared error payload into an ErrorBody schema while preserving the existing Error envelope for other responses.

Documentation:

  • Document the new x402 payment rejection field and corrected error-detail nesting in the changelog.

Chores:

  • Regenerate the API type definitions from the updated OpenAPI specification.

Issue #46: the 402 x402 challenge contract omitted error_detail.payment_rejected,
so generated clients had no way to discover the deny reason on a rejected payment.

- Extracted the Error envelopes inner object as the ErrorBody component schema
  and re-pointed X402PaymentRequired.error_detail at it (allOf + payment_rejected)
  instead of the wrapped Error schema — the gateway nests the bare error object
  directly under error_detail with no inner "error" key, confirmed against a live
  402 receipt from api.wave.online. Every other Error usage is unchanged (Error
  still requires "error": ErrorBody).
- Documents error_detail.payment_rejected: { reason, rail }, present only when a
  submitted payment credential was rejected. reason is a stable token (generic
  token payment_rejected, or a more specific one naming the failing condition);
  rail names the payment rail that rejected it. Verified against a live rejected-
  payment receipt.
- Regenerated generated/api-types.d.ts from the updated spec (sdk-types CI gate).
- CHANGELOG.md: Fixed (error_detail nesting) + Added (payment_rejected) entries.

Supersedes #45 (stale branch, conflicts with current main; re-applied by hand).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9mRh8G2ugbUt2kaXvFvF6
@codeant-ai

codeant-ai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @yakimoto, this account has used its review budget of 2,500,000 diff characters for the last 7 days.

You can request another review in 23 hours and 27 minutes by commenting @sourcery-ai review.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your workspace is out of credits. Ask your workspace admin to add credits to resume reviews. Manage billing

@cursor

cursor Bot commented Sep 3, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ba8635e3-ba6c-4f49-b193-09d9705a10c2)

@sourcery-ai

sourcery-ai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates the x402 402 challenge contract to reflect the gateway’s flat error_detail wire shape and documents the conditional payment_rejected { reason, rail } diagnostic, with regenerated TypeScript types and changelog entries.

Sequence diagram for rejected x402 payment diagnostics

sequenceDiagram
    participant Client
    participant Gateway
    Client->>Gateway: GET /v1/clips with X-PAYMENT
    Gateway->>Gateway: Validate payment credential
    Gateway-->>Client: 402 with error_detail { code, message, payment_rejected }
    Note over Client,Gateway: payment_rejected is present only after a submitted payment is rejected
    Client->>Client: Read payment_rejected.reason and payment_rejected.rail
Loading

File-Level Changes

Change Details Files
Correct the x402 challenge error schema to match the flat gateway response and expose rejected-payment diagnostics.
  • Extract the reusable inner error object into an ErrorBody component while preserving the existing Error envelope and its other references.
  • Change X402PaymentRequired.error_detail to compose ErrorBody directly, eliminating the nonexistent nested error member.
  • Add optional conditional payment_rejected data with required reason and rail string fields and document rejection-token semantics.
openapi.yaml
Regenerate client-facing TypeScript definitions from the corrected OpenAPI schema.
  • Update generated types to represent flat error_detail fields and the optional payment_rejected object.
generated/api-types.d.ts
Record the schema correction and newly documented x402 rejection field in project release notes.
  • Add changelog entries for the error_detail nesting fix and payment_rejected support, including live-response behavior and issue closure.
CHANGELOG.md

Assessment against linked issues

Issue Objective Addressed Explanation
#46 Expose an optional error_detail.payment_rejected object on the 402 payment challenge at the correct flat JSON path, with required reason and rail fields, while preserving the existing unpaid-challenge shape.
#46 Document the machine-readable rejection vocabulary: a server-controlled reason with the specified stable fallback markers and an open-enum handling model, plus a rail constrained to the supported rail values. The PR documents reason and rail as unrestricted strings. It gives examples and mentions unknown, but does not encode or comprehensively document the required reason fallback markers or the supported rail vocabulary (base-usdc, tempo-pathusd, wave-hub, unknown) as the contract requires.
#46 Expose the same error_detail.payment_rejected shape for streaming STREAM_HALTED 402 responses so clients can use the same lookup path. The shown schema change only updates X402PaymentRequired. It does not add or update a streaming STREAM_HALTED 402 response schema, nor establish that the streaming response reuses the updated schema.

Possibly linked issues

  • #402: The PR directly adds payment_rejected to X402PaymentRequired.error_detail, resolving the issue's missing generated-client field.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 25cd0de2-3a18-4934-9be0-f869a43759c1

📥 Commits

Reviewing files that changed from the base of the PR and between b4b4e47 and 530e54d.

⛔ Files ignored due to path filters (1)
  • generated/api-types.d.ts is excluded by !**/generated/**
📒 Files selected for processing (2)
  • CHANGELOG.md
  • openapi.yaml

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: semgrep-cloud-platform/scan
⚠️ CI failures not shown inline (8)

GitHub Actions: foundation-gate / 0_gate _ verify-routes.txt: docs(x402): document error_detail.payment_rejected on the 402 challenge (closes #46, supersedes #45)

Conclusion: failure

View job details

##[group]Run fail=0
 �[36;1mfail=0�[0m
 �[36;1many=0�[0m
 �[36;1mwhile IFS= read -r f; do�[0m
 �[36;1m  any=1�[0m
 �[36;1m  # Extract host portion (everything before the first /) from `pattern = "host/..."` lines.�[0m
 �[36;1m  # Handles both inline-string `pattern = "..."` and `routes = ["a.wave.online/*", ...]`.�[0m
 �[36;1m  while IFS= read -r h; do�[0m
 �[36;1m    [ -z "$h" ] && continue�[0m
 �[36;1m    case "$h" in�[0m
 �[36;1m      *-edge.wave.online)�[0m
 �[36;1m        echo "::error file=$f::URL '$h' uses banned -edge suffix (see docs/conventions/url-naming.md)"; fail=1 ;;�[0m

GitHub Actions: foundation-gate / gate _ verify-routes: docs(x402): document error_detail.payment_rejected on the 402 challenge (closes #46, supersedes #45)

Conclusion: failure

View job details

##[group]Run fail=0
 �[36;1mfail=0�[0m
 �[36;1many=0�[0m
 �[36;1mwhile IFS= read -r f; do�[0m
 �[36;1m  any=1�[0m
 �[36;1m  # Extract host portion (everything before the first /) from `pattern = "host/..."` lines.�[0m
 �[36;1m  # Handles both inline-string `pattern = "..."` and `routes = ["a.wave.online/*", ...]`.�[0m
 �[36;1m  while IFS= read -r h; do�[0m
 �[36;1m    [ -z "$h" ] && continue�[0m
 �[36;1m    case "$h" in�[0m
 �[36;1m      *-edge.wave.online)�[0m
 �[36;1m        echo "::error file=$f::URL '$h' uses banned -edge suffix (see docs/conventions/url-naming.md)"; fail=1 ;;�[0m

GitHub Actions: foundation-gate / 1_gate _ checks.txt: docs(x402): document error_detail.payment_rejected on the 402 challenge (closes #46, supersedes #45)

Conclusion: failure

View job details

##[group]Run # Real-secret patterns. Lines tagged "pragma: allowlist secret" (e.g. test fixtures / regex
 �[36;1m# Real-secret patterns. Lines tagged "pragma: allowlist secret" (e.g. test fixtures / regex�[0m
 �[36;1m# definitions) are skipped — that's how a vetting module can define key-shapes without tripping.�[0m
 �[36;1mHITS=$(grep -rIEn '(sk-[A-Za-z0-9]{20}|sk_(live|test)_[A-Za-z0-9]{20}|npm_[A-Za-z0-9]{30}|sbp_[a-f0-9]{40}|github_pat_[A-Za-z0-9_]{40}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30}|AIzaSy[A-Za-z0-9_-]{20}|xai-[A-Za-z0-9]{40}|xoxb-[A-Za-z0-9-]+|-----BEGIN [A-Z ]*PRIVATE KEY)' \�[0m
 �[36;1m    --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=dist . | grep -v 'allowlist secret' \�[0m
 �[36;1m    | { [ -f .github/.secret-allowlist ] && grep -vFf .github/.secret-allowlist || cat; } || true)�[0m
 �[36;1mif [ -n "$HITS" ]; then echo "::error::secret-like pattern found — do not commit credentials"; echo "$HITS"; exit 1; fi�[0m

GitHub Actions: foundation-gate / gate _ checks: docs(x402): document error_detail.payment_rejected on the 402 challenge (closes #46, supersedes #45)

Conclusion: failure

View job details

##[group]Run # Real-secret patterns. Lines tagged "pragma: allowlist secret" (e.g. test fixtures / regex
 �[36;1m# Real-secret patterns. Lines tagged "pragma: allowlist secret" (e.g. test fixtures / regex�[0m
 �[36;1m# definitions) are skipped — that's how a vetting module can define key-shapes without tripping.�[0m
 �[36;1mHITS=$(grep -rIEn '(sk-[A-Za-z0-9]{20}|sk_(live|test)_[A-Za-z0-9]{20}|npm_[A-Za-z0-9]{30}|sbp_[a-f0-9]{40}|github_pat_[A-Za-z0-9_]{40}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30}|AIzaSy[A-Za-z0-9_-]{20}|xai-[A-Za-z0-9]{40}|xoxb-[A-Za-z0-9-]+|-----BEGIN [A-Z ]*PRIVATE KEY)' \�[0m
 �[36;1m    --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=dist . | grep -v 'allowlist secret' \�[0m
 �[36;1m    | { [ -f .github/.secret-allowlist ] && grep -vFf .github/.secret-allowlist || cat; } || true)�[0m
 �[36;1mif [ -n "$HITS" ]; then echo "::error::secret-like pattern found — do not commit credentials"; echo "$HITS"; exit 1; fi�[0m

GitHub Actions: foundation-gate / gate _ checks: docs(x402): document error_detail.payment_rejected on the 402 challenge (closes #46, supersedes #45)

Conclusion: failure

View job details

##[group]Run fail=0
 �[36;1mfail=0�[0m
 �[36;1mwhile IFS= read -r f; do�[0m
 �[36;1m  grep -qxF "$f" .github/.filesize-allowlist 2>/dev/null && continue   # justified exception�[0m
 �[36;1m  n=$(wc -l < "$f")�[0m
 �[36;1m  if [ "$n" -gt "$MAX" ]; then echo "::error::$f has $n lines (> $MAX)"; fail=1; fi�[0m

GitHub Actions: foundation-gate / 2_gate _ skill-validate.txt: docs(x402): document error_detail.payment_rejected on the 402 challenge (closes #46, supersedes #45)

Conclusion: failure

View job details

##[group]Run python3 - <<'PY'
 �[36;1mpython3 - <<'PY'�[0m
 �[36;1mimport sys, os, re, subprocess�[0m
 �[36;1mimport yaml�[0m
 �[36;1mallf = subprocess.run(["git","ls-files","*SKILL.md"], capture_output=True, text=True).stdout.splitlines()�[0m
 �[36;1m# Only validate invocable skills; skip nested reference/vendored SKILL.md.�[0m
 �[36;1mskip = ("/references/", "/_external/", "/_consolidated", "/_archived", "/node_modules/", "/dist/")�[0m
 �[36;1mfiles = [f for f in allf if not any(s in "/" + f for s in skip)]�[0m
 �[36;1mif not files:�[0m
 �[36;1m    print("no SKILL.md in repo — skill gate is a no-op"); sys.exit(0)�[0m
 �[36;1merrs = []�[0m
 �[36;1mfor p in files:�[0m
 �[36;1m    d = os.path.basename(os.path.dirname(p))�[0m
 �[36;1m    t = open(p, encoding="utf-8", errors="replace").read()�[0m
 �[36;1m    m = re.match(r"^---\s*\n(.*?)\n---", t, re.S)�[0m
 �[36;1m    if not m:�[0m
 �[36;1m        errs.append(f"{p}: no frontmatter block"); continue�[0m
 �[36;1m    fm = m.group(1)�[0m
 �[36;1m    seen, dup = set(), set()�[0m
 �[36;1m    for line in fm.split("\n"):�[0m
 �[36;1m        k = re.match(r"^([A-Za-z_][\w-]*):", line)�[0m
 �[36;1m        if k: (dup if k.group(1) in seen else seen).add(k.group(1))�[0m
 �[36;1m    if dup: errs.append(f"{p}: duplicate frontmatter keys: {', '.join(sorted(dup))}")�[0m
 �[36;1m    try:�[0m
 �[36;1m        data = yaml.safe_load(fm) or {}�[0m
 �[36;1m    except yaml.YAMLError as e:�[0m
 �[36;1m        errs.append(f"{p}: invalid YAML frontmatter: {e}"); continue�[0m
 �[36;1m    if not isinstance(data, dict):�[0m
 �[36;1m        errs.append(f"{p}: frontmatter is not a mapping"); continue�[0m
 �[36;1m    if data.get("name") != d:�[0m
 �[36;1m        errs.append(f"{p}: name '{data.get('name')}' != directory '{d}'")�[0m
 �[36;1m    if not str(data.get("description") or "").strip():�[0m
 �[36;1m        errs.append(f"{p}: missing/empty description")�[0m
 �[36;1m    for key in ("allowed-tools", "hooks"):�[0m
 �[36;1m        if key in data:�...

GitHub Actions: foundation-gate / gate _ skill-validate: docs(x402): document error_detail.payment_rejected on the 402 challenge (closes #46, supersedes #45)

Conclusion: failure

View job details

##[group]Run python3 - <<'PY'
 �[36;1mpython3 - <<'PY'�[0m
 �[36;1mimport sys, os, re, subprocess�[0m
 �[36;1mimport yaml�[0m
 �[36;1mallf = subprocess.run(["git","ls-files","*SKILL.md"], capture_output=True, text=True).stdout.splitlines()�[0m
 �[36;1m# Only validate invocable skills; skip nested reference/vendored SKILL.md.�[0m
 �[36;1mskip = ("/references/", "/_external/", "/_consolidated", "/_archived", "/node_modules/", "/dist/")�[0m
 �[36;1mfiles = [f for f in allf if not any(s in "/" + f for s in skip)]�[0m
 �[36;1mif not files:�[0m
 �[36;1m    print("no SKILL.md in repo — skill gate is a no-op"); sys.exit(0)�[0m
 �[36;1merrs = []�[0m
 �[36;1mfor p in files:�[0m
 �[36;1m    d = os.path.basename(os.path.dirname(p))�[0m
 �[36;1m    t = open(p, encoding="utf-8", errors="replace").read()�[0m
 �[36;1m    m = re.match(r"^---\s*\n(.*?)\n---", t, re.S)�[0m
 �[36;1m    if not m:�[0m
 �[36;1m        errs.append(f"{p}: no frontmatter block"); continue�[0m
 �[36;1m    fm = m.group(1)�[0m
 �[36;1m    seen, dup = set(), set()�[0m
 �[36;1m    for line in fm.split("\n"):�[0m
 �[36;1m        k = re.match(r"^([A-Za-z_][\w-]*):", line)�[0m
 �[36;1m        if k: (dup if k.group(1) in seen else seen).add(k.group(1))�[0m
 �[36;1m    if dup: errs.append(f"{p}: duplicate frontmatter keys: {', '.join(sorted(dup))}")�[0m
 �[36;1m    try:�[0m
 �[36;1m        data = yaml.safe_load(fm) or {}�[0m
 �[36;1m    except yaml.YAMLError as e:�[0m
 �[36;1m        errs.append(f"{p}: invalid YAML frontmatter: {e}"); continue�[0m
 �[36;1m    if not isinstance(data, dict):�[0m
 �[36;1m        errs.append(f"{p}: frontmatter is not a mapping"); continue�[0m
 �[36;1m    if data.get("name") != d:�[0m
 �[36;1m        errs.append(f"{p}: name '{data.get('name')}' != directory '{d}'")�[0m
 �[36;1m    if not str(data.get("description") or "").strip():�[0m
 �[36;1m        errs.append(f"{p}: missing/empty description")�[0m
 �[36;1m    for key in ("allowed-tools", "hooks"):�[0m
 �[36;1m        if key in data:�...

GitHub Actions: foundation-gate / 3_breaking-change.txt: docs(x402): document error_detail.payment_rejected on the 402 challenge (closes #46, supersedes #45)

Conclusion: failure

View job details

##[group]Run if oasdiff breaking --fail-on ERR /tmp/base/openapi.yaml "$PWD/openapi.yaml" > /tmp/oasdiff.txt 2>&1; then
 �[36;1mif oasdiff breaking --fail-on ERR /tmp/base/openapi.yaml "$PWD/openapi.yaml" > /tmp/oasdiff.txt 2>&1; then�[0m
 �[36;1m  echo "No breaking spec changes."�[0m
 �[36;1melse�[0m
 �[36;1m  if gh pr view "76" --repo "wave-av/api-spec" \�[0m
 �[36;1m      --json body --jq '.body' | grep -q "Breaking: yes"; then�[0m
 �[36;1m    echo "::warning::Breaking spec changes acknowledged via 'Breaking: yes' marker."�[0m
 �[36;1m    cat /tmp/oasdiff.txt�[0m
 �[36;1m  else�[0m
 �[36;1m    echo "::error::Breaking spec changes detected. Acknowledge them with 'Breaking: yes' in the PR body."�[0m
🧰 Additional context used
📓 Path-based instructions (1)
Conventional Commit titles; update `CHANGELOG.md` (`Unreleased`) for user-facing changes.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • CHANGELOG.md
🪛 Checkov (3.3.11)
openapi.yaml

[high] 1-4266: Ensure that security operations is not empty.

(CKV_OPENAPI_5)

🪛 GitHub Actions: foundation-gate / breaking-change
openapi.yaml

[error] 1-1: oasdiff detected breaking API specification changes. The check failed; acknowledge them by adding 'Breaking: yes' to the pull request body.

🔇 Additional comments (2)
openapi.yaml (1)

2370-2410: LGTM!

Also applies to: 4069-4091

CHANGELOG.md (1)

35-42: LGTM!

Also applies to: 76-83


📝 Summary

Summary by CodeRabbit

  • Documentation
    • Updated the OpenAPI documentation with a reusable normalized error format.
    • Clarified payment-required error details, including optional rejection reasons and payment rails.
    • Documented the gateway’s 402 error response shape more accurately.

Walkthrough

The OpenAPI specification adds a reusable ErrorBody schema, updates Error.error to use it, and documents optional payment_rejected details for x402 responses. The changelog records both schema changes.

Changes

Error contract updates

Layer / File(s) Summary
Normalized error body schema
openapi.yaml
Adds the reusable ErrorBody schema and changes Error.error to reference it.
x402 payment rejection details
openapi.yaml, CHANGELOG.md
Changes x402 error_detail to use ErrorBody and adds optional payment_rejected.reason and payment_rejected.rail fields. The changelog documents these updates.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 530e5

The API contract now documents optional payment-rejection diagnostics while preserving the expected flat 402 error-detail shape. The change is ready to merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The PR addresses the core requirement in issue #46 by adding optional error_detail.payment_rejected with required reason and rail fields and correcting the flat ErrorBody path. Full compliance cannot … Review the excluded generated/api-types.d.ts and the relevant streaming 402 schema. Confirm that payment_rejected is available at the same path for STREAM_HALTED responses and that the documented reason and rail vocabularies meet issue #46.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The ErrorBody extraction, x402 schema correction, payment_rejected documentation, generated type regeneration, and changelog update all support the linked issue and stated pull request objectives. No …
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Title check ✅ Passed The title clearly identifies the main change: documenting error_detail.payment_rejected on the x402 402 challenge. The issue and superseded PR references add useful context.
Description check ✅ Passed The description directly explains the schema correction, the new payment_rejected field, generated type updates, validation steps, and related issue.
Full details: Linked Issues check

Explanation

The PR addresses the core requirement in issue #46 by adding optional error_detail.payment_rejected with required reason and rail fields and correcting the flat ErrorBody path. Full compliance cannot be confirmed because generated/api-types.d.ts is excluded by the !/generated/ filter, and the supplied summaries do not verify the required STREAM_HALTED 402 shape or the specified reason and rail vocabularies.

Full details: Out of Scope Changes check

Explanation

The ErrorBody extraction, x402 schema correction, payment_rejected documentation, generated type regeneration, and changelog update all support the linked issue and stated pull request objectives. No unrelated changes are shown.

Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (2 skipped: 2 unsupported.)

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audit-402-payment-rejected
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/audit-402-payment-rejected

Comment @coderabbitai help to get the list of available commands.

@macroscopeapp

macroscopeapp Bot commented Sep 3, 2026

Copy link
Copy Markdown

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The implementation impact is limited to the OpenAPI contract, generated compile-time types, and changelog, with no gateway runtime changes. However, the PR changes the client-visible x402 payment-rejection contract, so its billing/financial implications warrant human review.

Not approved because:

  • Credit balance exhausted. Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more.

@gitar-bot

gitar-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your team has used its included automatic processing for this billing period (headroom scales with your seat count). You can still comment "Gitar review" to run one anytime, and automatic reviews resume on their own by October 1. Add seats for more headroom.
Learn more

Code Review ✅ Approved

Aligns the x402 payment-required schema with the gateway's live error responses by correcting error_detail to model a flat error object (not wrapped in an error envelope) and documenting the conditional payment_rejected field with denial details. Extracts a reusable ErrorBody schema, regenerates generated/api-types.d.ts, and updates the changelog. Spec is valid, all $refs resolve, and no new linting warnings introduced.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@yakimoto
yakimoto merged commit 1de0cf7 into main Sep 3, 2026
28 of 29 checks passed
@yakimoto
yakimoto deleted the fix/audit-402-payment-rejected branch September 3, 2026 19:30
yakimoto added a commit that referenced this pull request Sep 3, 2026
…t_rejected entry, keep link refs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01K9mRh8G2ugbUt2kaXvFvF6
yakimoto added a commit that referenced this pull request Sep 3, 2026
…sed/Fixed, not under 1.0.0

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01K9mRh8G2ugbUt2kaXvFvF6
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.

402 contract omits error_detail.payment_rejected — generated clients cannot discover the deny reason

1 participant