q-runtime-core 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,575 @@
1
+ """Read-only external x402 probing and protocol/economic auditing for Q Runtime v0.5.
2
+
3
+ No PAYMENT-SIGNATURE is ever sent by this module. It performs ordinary unpaid HTTP
4
+ requests, records the observed 402 challenge, and evaluates wire-level invariants.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from datetime import datetime, timezone
10
+ import base64
11
+ import ipaddress
12
+ import json
13
+ from pathlib import Path
14
+ import re
15
+ from typing import Any, Mapping
16
+ from urllib.error import HTTPError, URLError
17
+ from urllib.parse import urljoin, urlparse
18
+ from urllib.request import Request, urlopen
19
+
20
+ from qruntime.rails.x402 import X402DecodeError, X402PaymentRequired, parse_payment_required
21
+
22
+
23
+ class ExternalProbeError(RuntimeError):
24
+ pass
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class AuditFinding:
29
+ severity: str # info | warning | error
30
+ invariant: str
31
+ reason: str
32
+
33
+ def as_dict(self) -> dict[str, str]:
34
+ return {"severity": self.severity, "invariant": self.invariant, "reason": self.reason}
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ExternalProbe:
39
+ request_url: str
40
+ final_url: str
41
+ method: str
42
+ status_code: int
43
+ captured_at: str
44
+ response_headers: dict[str, str]
45
+ body_text: str
46
+ challenge_source: str | None
47
+ challenge_wire: str | dict[str, Any] | None
48
+ payment_required: X402PaymentRequired | None
49
+ findings: tuple[AuditFinding, ...]
50
+ audit_mode: str = "strict"
51
+
52
+ @property
53
+ def passed(self) -> bool:
54
+ return not any(item.severity == "error" for item in self.findings)
55
+
56
+ @property
57
+ def classification(self) -> str:
58
+ if any(item.invariant == "LegacyX402V1Detected" for item in self.findings):
59
+ return "legacy_x402_v1"
60
+ if self.passed:
61
+ return "accepted_v2"
62
+ return "nonconformant"
63
+
64
+ def as_dict(self) -> dict[str, Any]:
65
+ return {
66
+ "format": "qruntime-x402-external-audit-v2",
67
+ "audit_mode": self.audit_mode,
68
+ "classification": self.classification,
69
+ "source": {
70
+ "kind": "external_http",
71
+ "captured_at": self.captured_at,
72
+ "request_url": self.request_url,
73
+ "final_url": self.final_url,
74
+ "method": self.method,
75
+ "status_code": self.status_code,
76
+ "note": "Unpaid read-only probe. PAYMENT-SIGNATURE was never sent.",
77
+ },
78
+ "challenge_source": self.challenge_source,
79
+ "challenge": self.challenge_wire,
80
+ "findings": [item.as_dict() for item in self.findings],
81
+ "summary": {
82
+ "passed": self.passed,
83
+ "classification": self.classification,
84
+ "errors": sum(1 for item in self.findings if item.severity == "error"),
85
+ "warnings": sum(1 for item in self.findings if item.severity == "warning"),
86
+ },
87
+ "raw_exchange": {
88
+ "response_headers": self.response_headers,
89
+ "body_preview": self.body_text[:4000],
90
+ },
91
+ }
92
+
93
+
94
+ # Formal CAIP-2 grammar (CAIP-2 Final): namespace 3-8 chars, reference 1-32 chars.
95
+ _CAIP2 = re.compile(r"^[-a-z0-9]{3,8}:[-_A-Za-z0-9]{1,32}$")
96
+
97
+ # x402's current network-support documentation (Sep 2026) explicitly publishes these
98
+ # Algorand identifiers as its supported network strings, even though their base64
99
+ # genesis-hash references fall outside the formal CAIP-2 reference grammar above.
100
+ # Treat them as an upstream x402 profile exception, not as a defect in each service.
101
+ _X402_DOCUMENTED_NETWORK_EXCEPTIONS = {
102
+ "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
103
+ "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
104
+ }
105
+ _EVM_ADDR = re.compile(r"^0x[0-9a-fA-F]{40}$")
106
+
107
+
108
+ def _header(headers: Mapping[str, str], name: str) -> str | None:
109
+ name = name.lower()
110
+ for key, value in headers.items():
111
+ if key.lower() == name:
112
+ return value
113
+ return None
114
+
115
+
116
+ def _body_json(body_text: str) -> dict[str, Any] | None:
117
+ try:
118
+ value = json.loads(body_text)
119
+ except (json.JSONDecodeError, TypeError):
120
+ return None
121
+ return value if isinstance(value, dict) else None
122
+
123
+
124
+ def _decode_header_strict(value: str) -> dict[str, Any]:
125
+ raw = value.strip()
126
+ if raw.startswith("{"):
127
+ raise X402DecodeError("PAYMENT-REQUIRED header contains raw JSON; x402 v2 HTTP header must be base64 JSON")
128
+ padded = raw + "=" * (-len(raw) % 4)
129
+ try:
130
+ decoded = base64.b64decode(padded, validate=True)
131
+ parsed = json.loads(decoded.decode("utf-8"))
132
+ except Exception as exc: # noqa: BLE001
133
+ raise X402DecodeError(f"invalid base64 JSON PAYMENT-REQUIRED header: {exc}") from exc
134
+ if not isinstance(parsed, dict):
135
+ raise X402DecodeError("PAYMENT-REQUIRED must decode to a JSON object")
136
+ return parsed
137
+
138
+
139
+ def _is_public_https(url: str) -> bool:
140
+ parsed = urlparse(url)
141
+ if parsed.scheme.lower() != "https" or not parsed.hostname:
142
+ return False
143
+ host = parsed.hostname.lower()
144
+ if host in {"localhost", "localhost.localdomain"} or host.endswith(".local"):
145
+ return False
146
+ try:
147
+ ip = ipaddress.ip_address(host)
148
+ except ValueError:
149
+ return True
150
+ return not (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved)
151
+
152
+
153
+ def _resource_binding(request_url: str, resource_url: str) -> tuple[bool, bool]:
154
+ """Return (same_resource, query_fully_bound_in_url).
155
+
156
+ x402 ResourceInfo identifies the protected resource, but deployed services may bind
157
+ request parameters through extensions rather than reproducing the query string in
158
+ resource.url. Origin/path mismatch is therefore a hard error; an omitted query is
159
+ recorded as a warning, not treated as a protocol failure. When both URLs carry a
160
+ query, they must match exactly.
161
+ """
162
+ a = urlparse(request_url)
163
+ resolved_resource_url = urljoin(request_url, resource_url)
164
+ b = urlparse(resolved_resource_url)
165
+ same_base = (a.scheme.lower(), a.netloc.lower(), a.path or "/") == (
166
+ b.scheme.lower(), b.netloc.lower(), b.path or "/"
167
+ )
168
+ if not same_base:
169
+ return False, False
170
+ if b.query:
171
+ return a.query == b.query, a.query == b.query
172
+ return True, not bool(a.query)
173
+
174
+
175
+ def audit_observed_exchange(
176
+ *,
177
+ request_url: str,
178
+ final_url: str,
179
+ method: str,
180
+ status_code: int,
181
+ response_headers: Mapping[str, str],
182
+ body_text: str,
183
+ mode: str = "strict",
184
+ ) -> tuple[str | None, str | dict[str, Any] | None, X402PaymentRequired | None, tuple[AuditFinding, ...]]:
185
+ if mode not in {"strict", "compatibility"}:
186
+ raise ExternalProbeError("audit mode must be 'strict' or 'compatibility'")
187
+
188
+ findings: list[AuditFinding] = []
189
+ header_wire = _header(response_headers, "PAYMENT-REQUIRED")
190
+ body_obj = _body_json(body_text)
191
+ body_candidate = body_obj if body_obj and ("x402Version" in body_obj or "accepts" in body_obj) else None
192
+
193
+ if status_code != 402:
194
+ findings.append(AuditFinding("error", "PaymentRequiredStatus", f"expected HTTP 402, received HTTP {status_code}"))
195
+ return None, None, None, tuple(findings)
196
+
197
+ challenge_source: str | None = None
198
+ challenge_wire: str | dict[str, Any] | None = None
199
+ raw_obj: dict[str, Any] | None = None
200
+
201
+ if header_wire is not None:
202
+ challenge_source = "header"
203
+ challenge_wire = header_wire
204
+ raw = header_wire.strip()
205
+ if raw.startswith("{"):
206
+ try:
207
+ candidate = json.loads(raw)
208
+ except json.JSONDecodeError:
209
+ candidate = None
210
+ if isinstance(candidate, dict) and candidate.get("x402Version") == 1:
211
+ # Root-cause aggregation: do not report a v2 encoding error on a v1 envelope.
212
+ raw_obj = candidate
213
+ else:
214
+ findings.append(AuditFinding(
215
+ "error",
216
+ "CanonicalPaymentRequiredEncoding",
217
+ "PAYMENT-REQUIRED header contains raw JSON; x402 v2 HTTP header must be base64 JSON",
218
+ ))
219
+ raw_obj = candidate if isinstance(candidate, dict) else None
220
+ else:
221
+ try:
222
+ raw_obj = _decode_header_strict(header_wire)
223
+ except X402DecodeError as exc:
224
+ findings.append(AuditFinding("error", "CanonicalPaymentRequiredEncoding", str(exc)))
225
+ elif body_candidate is not None:
226
+ challenge_source = "body"
227
+ challenge_wire = body_candidate
228
+ raw_obj = body_candidate
229
+ else:
230
+ findings.append(AuditFinding("error", "PaymentRequirementKnownBeforeAuthorization", "HTTP 402 contained no parseable PAYMENT-REQUIRED challenge"))
231
+ return None, None, None, tuple(findings)
232
+
233
+ parsed: X402PaymentRequired | None = None
234
+ if raw_obj is not None:
235
+ version = raw_obj.get("x402Version")
236
+
237
+ if version == 1:
238
+ severity = "error" if mode == "strict" else "warning"
239
+ accepts = raw_obj.get("accepts") if isinstance(raw_obj.get("accepts"), list) else []
240
+ legacy_networks = [str(req.get("network")) for req in accepts if isinstance(req, dict) and req.get("network") is not None]
241
+ has_legacy_amount = any(isinstance(req, dict) and "maxAmountRequired" in req for req in accepts)
242
+ has_legacy_resource = any(isinstance(req, dict) and isinstance(req.get("resource"), str) for req in accepts)
243
+ derived = []
244
+ if challenge_source == "body":
245
+ derived.append("body-primary challenge")
246
+ if legacy_networks:
247
+ derived.append("legacy network identifier(s): " + ", ".join(legacy_networks[:3]))
248
+ if has_legacy_amount:
249
+ derived.append("maxAmountRequired instead of v2 amount")
250
+ if has_legacy_resource:
251
+ derived.append("resource embedded in PaymentRequirements instead of v2 top-level resource object")
252
+ reason = "x402 v1 legacy handshake detected; strict x402 v2 compatibility requires migration"
253
+ if derived:
254
+ reason += "; derived legacy shape: " + "; ".join(derived)
255
+ findings.append(AuditFinding(severity, "LegacyX402V1Detected", reason))
256
+ findings.append(AuditFinding(
257
+ "info",
258
+ "LegacyMigrationRequired",
259
+ "classify as one legacy root cause rather than separate v2 field failures",
260
+ ))
261
+ else:
262
+ if version != 2:
263
+ findings.append(AuditFinding("error", "X402VersionSupported", f"expected x402Version=2, observed {version!r}"))
264
+
265
+ if challenge_source == "body":
266
+ findings.append(AuditFinding(
267
+ "error" if mode == "strict" else "warning",
268
+ "CanonicalPaymentRequiredLocation",
269
+ "HTTP 402 challenge was found only in the response body; x402 v2 canonical HTTP location is PAYMENT-REQUIRED header",
270
+ ))
271
+
272
+ accepts = raw_obj.get("accepts")
273
+ if not isinstance(accepts, list) or not accepts:
274
+ findings.append(AuditFinding("error", "NonEmptyPaymentRequirements", "accepts must be a non-empty array"))
275
+ else:
276
+ for idx, req in enumerate(accepts):
277
+ prefix = f"accepts[{idx}]"
278
+ if not isinstance(req, dict):
279
+ findings.append(AuditFinding("error", "ValidPaymentRequirementShape", f"{prefix} is not an object"))
280
+ continue
281
+ amount = req.get("amount")
282
+ if not isinstance(amount, str) or not amount.isdigit():
283
+ findings.append(AuditFinding("error", "AtomicAmountCanonical", f"{prefix}.amount must be a decimal string of atomic units"))
284
+ elif int(amount) <= 0:
285
+ findings.append(AuditFinding("warning", "PositivePaymentAmount", f"{prefix}.amount is {amount}; a zero-priced resource should not require payment"))
286
+ network = req.get("network")
287
+ if not isinstance(network, str):
288
+ findings.append(AuditFinding("error", "CanonicalNetworkIdentifier", f"{prefix}.network must be a string network identifier"))
289
+ elif _CAIP2.fullmatch(network):
290
+ pass
291
+ elif network in _X402_DOCUMENTED_NETWORK_EXCEPTIONS:
292
+ findings.append(AuditFinding(
293
+ "info",
294
+ "X402DocumentedNetworkProfileException",
295
+ f"{prefix}.network={network!r} is documented by x402 as a supported Algorand network identifier but falls outside the formal CAIP-2 reference grammar; classify as an upstream standards-profile exception, not a service defect",
296
+ ))
297
+ else:
298
+ findings.append(AuditFinding("error", "CanonicalNetworkIdentifier", f"{prefix}.network is not a recognized x402/CAIP-2 network identifier"))
299
+ timeout = req.get("maxTimeoutSeconds")
300
+ scheme = req.get("scheme")
301
+ positive_timeout_schemes = {"exact", "upto", "batch-settlement"}
302
+ if not isinstance(timeout, int) or isinstance(timeout, bool) or timeout < 0:
303
+ findings.append(AuditFinding("error", "LiveAuthorizationWindow", f"{prefix}.maxTimeoutSeconds must be a non-negative integer"))
304
+ elif timeout == 0 and scheme in positive_timeout_schemes:
305
+ findings.append(AuditFinding("error", "LiveAuthorizationWindow", f"{prefix}.maxTimeoutSeconds must be positive for x402 scheme {scheme!r}"))
306
+ elif timeout == 0:
307
+ findings.append(AuditFinding(
308
+ "info",
309
+ "SchemeSpecificTimeoutSemanticsUnknown",
310
+ f"{prefix}.scheme={scheme!r} uses maxTimeoutSeconds=0; core v2 defines the field but scheme-specific zero semantics are not known to Q Runtime, so do not classify this custom scheme as invalid without its scheme specification",
311
+ ))
312
+ elif timeout > 86400:
313
+ findings.append(AuditFinding("warning", "BoundedAuthorizationWindow", f"{prefix}.maxTimeoutSeconds={timeout} is longer than 24 hours"))
314
+ if isinstance(network, str) and network.startswith("eip155:"):
315
+ pay_to = req.get("payTo")
316
+ asset = req.get("asset")
317
+ if not isinstance(pay_to, str) or not _EVM_ADDR.fullmatch(pay_to):
318
+ findings.append(AuditFinding("error", "ValidPaymentRecipient", f"{prefix}.payTo is not a 20-byte EVM address"))
319
+ if not isinstance(asset, str) or not _EVM_ADDR.fullmatch(asset):
320
+ findings.append(AuditFinding("error", "ValidPaymentAsset", f"{prefix}.asset is not a 20-byte EVM token address"))
321
+
322
+ resource = raw_obj.get("resource")
323
+ if not isinstance(resource, dict) or not isinstance(resource.get("url"), str) or not resource.get("url"):
324
+ findings.append(AuditFinding("error", "PaymentPayloadBoundToResource", "resource.url is missing"))
325
+ else:
326
+ same_resource, query_bound = _resource_binding(final_url, resource["url"])
327
+ if not same_resource:
328
+ findings.append(AuditFinding(
329
+ "error",
330
+ "PaymentPayloadBoundToResource",
331
+ f"advertised resource URL {resource['url']} does not match requested origin/path {final_url}",
332
+ ))
333
+ elif not query_bound:
334
+ findings.append(AuditFinding(
335
+ "warning",
336
+ "ResourceQueryBindingExternalized",
337
+ "request query parameters are not reproduced in resource.url; verify that an extension or application policy binds them before authorization",
338
+ ))
339
+
340
+ try:
341
+ parsed = parse_payment_required(raw_obj)
342
+ except X402DecodeError as exc:
343
+ findings.append(AuditFinding("error", "ValidX402WireData", str(exc)))
344
+
345
+ cache_control = _header(response_headers, "Cache-Control") or ""
346
+ directives = [part.strip().lower() for part in cache_control.split(",") if part.strip()]
347
+ has_no_store = "no-store" in directives
348
+ has_no_cache = "no-cache" in directives
349
+ has_must_revalidate = "must-revalidate" in directives or "proxy-revalidate" in directives
350
+ explicitly_public = "public" in directives
351
+ positive_max_age = False
352
+ freshness_directive_seen = False
353
+ for directive in directives:
354
+ if directive.startswith("max-age=") or directive.startswith("s-maxage="):
355
+ freshness_directive_seen = True
356
+ try:
357
+ value = int(directive.split("=", 1)[1].strip().strip('"'))
358
+ except ValueError:
359
+ value = 1 # malformed freshness directive: fail conservatively
360
+ positive_max_age = positive_max_age or value > 0
361
+
362
+ # no-store forbids storage; no-cache requires revalidation. The legacy-compatible
363
+ # max-age=0,must-revalidate combination is effectively revalidation-on-every-use,
364
+ # so it must not be reported as stale-replay capable.
365
+ immediate_revalidation = has_must_revalidate and freshness_directive_seen and not positive_max_age
366
+ if not has_no_store and not has_no_cache and not immediate_revalidation and (explicitly_public or positive_max_age):
367
+ findings.append(AuditFinding(
368
+ "warning",
369
+ "NonCacheablePaymentChallenge",
370
+ f"402 response is cacheable ({cache_control}); stale payment requirements may be replayed by shared caches",
371
+ ))
372
+
373
+ if parsed is not None and not any(f.severity == "error" for f in findings):
374
+ findings.append(AuditFinding("info", "ExternalPaymentRequiredAccepted", "x402 v2 payment challenge passed current Q Runtime wire/economic checks"))
375
+
376
+ return challenge_source, challenge_wire, parsed, tuple(findings)
377
+
378
+ def probe_external(
379
+ url: str,
380
+ *,
381
+ method: str = "GET",
382
+ timeout: float = 10.0,
383
+ request_headers: Mapping[str, str] | None = None,
384
+ json_body: Any | None = None,
385
+ require_public_https: bool = False,
386
+ mode: str = "strict",
387
+ ) -> ExternalProbe:
388
+ if require_public_https and not _is_public_https(url):
389
+ raise ExternalProbeError("external directory probes are restricted to public HTTPS URLs")
390
+ method = method.upper()
391
+ if method not in {"GET", "POST", "HEAD"}:
392
+ raise ExternalProbeError(f"unsupported probe method: {method}")
393
+
394
+ headers = {
395
+ "User-Agent": "q-runtime/0.5 external-audit",
396
+ "Accept": "application/json, */*",
397
+ }
398
+ data: bytes | None = None
399
+ if json_body is not None:
400
+ data = json.dumps(json_body, separators=(",", ":")).encode("utf-8")
401
+ headers["Content-Type"] = "application/json"
402
+ if request_headers:
403
+ for key, value in request_headers.items():
404
+ if key.lower() in {"payment-signature", "x-payment"}:
405
+ raise ExternalProbeError("Q Runtime external probe refuses to send payment authorization headers")
406
+ headers[str(key)] = str(value)
407
+
408
+ req = Request(url, data=data, headers=headers, method=method)
409
+ body = b""
410
+ final_url = url
411
+ try:
412
+ with urlopen(req, timeout=timeout) as response: # noqa: S310 - URL chosen by operator/directory
413
+ status = int(response.status)
414
+ final_url = response.geturl()
415
+ response_headers = dict(response.headers.items())
416
+ if method != "HEAD":
417
+ body = response.read(65536)
418
+ except HTTPError as exc:
419
+ status = int(exc.code)
420
+ final_url = exc.geturl()
421
+ response_headers = dict(exc.headers.items()) if exc.headers else {}
422
+ if method != "HEAD":
423
+ body = exc.read(65536)
424
+ except URLError as exc:
425
+ raise ExternalProbeError(f"HTTP probe failed for {url}: {exc}") from exc
426
+
427
+ body_text = body.decode("utf-8", errors="replace")
428
+ source, wire, parsed, findings = audit_observed_exchange(
429
+ request_url=url,
430
+ final_url=final_url,
431
+ method=method,
432
+ status_code=status,
433
+ response_headers=response_headers,
434
+ body_text=body_text,
435
+ mode=mode,
436
+ )
437
+ return ExternalProbe(
438
+ request_url=url,
439
+ final_url=final_url,
440
+ method=method,
441
+ status_code=status,
442
+ captured_at=datetime.now(timezone.utc).isoformat(),
443
+ response_headers=response_headers,
444
+ body_text=body_text,
445
+ challenge_source=source,
446
+ challenge_wire=wire,
447
+ payment_required=parsed,
448
+ findings=findings,
449
+ audit_mode=mode,
450
+ )
451
+
452
+
453
+ def write_external_probe(probe: ExternalProbe, path: str | Path) -> Path:
454
+ target = Path(path)
455
+ target.parent.mkdir(parents=True, exist_ok=True)
456
+ target.write_text(json.dumps(probe.as_dict(), indent=2, ensure_ascii=False), encoding="utf-8")
457
+ return target
458
+
459
+
460
+ def fetch_json(url: str, *, timeout: float = 10.0) -> dict[str, Any]:
461
+ req = Request(url, headers={"User-Agent": "q-runtime/0.5 directory-audit", "Accept": "application/json"})
462
+ try:
463
+ with urlopen(req, timeout=timeout) as response: # noqa: S310
464
+ raw = response.read(2_000_000)
465
+ except (HTTPError, URLError) as exc:
466
+ raise ExternalProbeError(f"directory request failed for {url}: {exc}") from exc
467
+ try:
468
+ parsed = json.loads(raw.decode("utf-8"))
469
+ except json.JSONDecodeError as exc:
470
+ raise ExternalProbeError(f"directory returned invalid JSON for {url}") from exc
471
+ if not isinstance(parsed, dict):
472
+ raise ExternalProbeError("directory response must be a JSON object")
473
+ return parsed
474
+
475
+
476
+ def _get(mapping: Mapping[str, Any], *names: str) -> Any:
477
+ for name in names:
478
+ if name in mapping:
479
+ return mapping[name]
480
+ return None
481
+
482
+
483
+ def _extract_service_slugs(payload: Mapping[str, Any]) -> list[str]:
484
+ data = payload.get("data")
485
+ if not isinstance(data, list):
486
+ return []
487
+ out: list[str] = []
488
+ for item in data:
489
+ if not isinstance(item, Mapping):
490
+ continue
491
+ slug = _get(item, "slug", "service_slug", "id")
492
+ if isinstance(slug, str) and slug and "/" not in slug:
493
+ out.append(slug)
494
+ return out
495
+
496
+
497
+ def _endpoint_candidates(detail: Mapping[str, Any]) -> list[tuple[str, str]]:
498
+ root = detail.get("data") if isinstance(detail.get("data"), Mapping) else detail
499
+ if not isinstance(root, Mapping):
500
+ return []
501
+ base = _get(root, "base_url", "baseUrl", "url")
502
+ endpoints = _get(root, "endpoints", "routes")
503
+ if not isinstance(base, str) or not isinstance(endpoints, list):
504
+ return []
505
+ out: list[tuple[str, str]] = []
506
+ for ep in endpoints:
507
+ if not isinstance(ep, Mapping):
508
+ continue
509
+ method = str(_get(ep, "method", "http_method") or "GET").upper()
510
+ path = _get(ep, "path", "route", "url")
511
+ if method != "GET" or not isinstance(path, str) or not path or "{" in path or "}" in path:
512
+ continue
513
+ absolute = path if path.startswith("http://") or path.startswith("https://") else urljoin(base.rstrip("/") + "/", path.lstrip("/"))
514
+ if _is_public_https(absolute):
515
+ out.append((method, absolute))
516
+ return out
517
+
518
+
519
+ def audit_x402_list_directory(
520
+ *,
521
+ limit: int = 10,
522
+ timeout: float = 10.0,
523
+ directory_base: str = "https://x402-list.com/api/v1",
524
+ mode: str = "compatibility",
525
+ ) -> dict[str, Any]:
526
+ if limit < 1 or limit > 50:
527
+ raise ExternalProbeError("directory audit limit must be between 1 and 50")
528
+ listing = fetch_json(f"{directory_base}/services?status=online&sort=uptime&per_page=100&ref=qruntime", timeout=timeout)
529
+ slugs = _extract_service_slugs(listing)
530
+ results: list[dict[str, Any]] = []
531
+ skipped: list[dict[str, str]] = []
532
+
533
+ for slug in slugs:
534
+ if len(results) >= limit:
535
+ break
536
+ try:
537
+ detail = fetch_json(f"{directory_base}/services/{slug}?ref=qruntime", timeout=timeout)
538
+ except ExternalProbeError as exc:
539
+ skipped.append({"service": slug, "reason": str(exc)})
540
+ continue
541
+ candidates = _endpoint_candidates(detail)
542
+ if not candidates:
543
+ skipped.append({"service": slug, "reason": "no safe non-templated GET endpoint found"})
544
+ continue
545
+ method, url = candidates[0]
546
+ try:
547
+ probe = probe_external(url, method=method, timeout=timeout, require_public_https=True, mode=mode)
548
+ results.append({"service": slug, "endpoint": url, "audit": probe.as_dict()})
549
+ except ExternalProbeError as exc:
550
+ results.append({
551
+ "service": slug,
552
+ "endpoint": url,
553
+ "audit": {
554
+ "summary": {"passed": False, "errors": 1, "warnings": 0},
555
+ "findings": [{"severity": "error", "invariant": "ExternalReachability", "reason": str(exc)}],
556
+ },
557
+ })
558
+
559
+ error_count = sum(int(item["audit"]["summary"].get("errors", 0)) for item in results)
560
+ warning_count = sum(int(item["audit"]["summary"].get("warnings", 0)) for item in results)
561
+ legacy_count = sum(1 for item in results if item["audit"].get("classification") == "legacy_x402_v1" or item["audit"].get("summary", {}).get("classification") == "legacy_x402_v1")
562
+ return {
563
+ "format": "qruntime-x402-directory-audit-v1",
564
+ "directory": directory_base,
565
+ "captured_at": datetime.now(timezone.utc).isoformat(),
566
+ "safety": "GET only; public HTTPS only; no PAYMENT-SIGNATURE; no payment attempted",
567
+ "audit_mode": mode,
568
+ "requested_limit": limit,
569
+ "services_audited": len(results),
570
+ "errors": error_count,
571
+ "warnings": warning_count,
572
+ "legacy_x402_v1": legacy_count,
573
+ "results": results,
574
+ "skipped": skipped,
575
+ }
@@ -0,0 +1,65 @@
1
+ """Known independent x402 targets used as cross-implementation validation, not fixtures."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from qruntime.rails.x402_external import ExternalProbeError, probe_external
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ExternalCase:
12
+ name: str
13
+ url: str
14
+ expected: str # pass | error
15
+ expected_invariant: str | None = None
16
+
17
+
18
+ DEFAULT_CASES = (
19
+ ExternalCase("querylines-production", "https://api.querylines.com/v1/premium?state=FL", "pass"),
20
+ ExternalCase("grug402-compliant", "https://grug402.dev/api/x402/compliant-get", "pass"),
21
+ ExternalCase("grug402-missing-header", "https://grug402.dev/api/x402/missing-payment-required", "error", "CanonicalPaymentRequiredLocation"),
22
+ ExternalCase("grug402-raw-json-header", "https://grug402.dev/api/x402/header-raw-json", "error", "CanonicalPaymentRequiredEncoding"),
23
+ ExternalCase("grug402-resource-host-mismatch", "https://grug402.dev/api/x402/resource-host-mismatch", "error", "PaymentPayloadBoundToResource"),
24
+ )
25
+
26
+
27
+ def run_external_suite(*, timeout: float = 10.0, mode: str = "strict") -> dict[str, Any]:
28
+ results: list[dict[str, Any]] = []
29
+ suite_passed = True
30
+ for case in DEFAULT_CASES:
31
+ try:
32
+ probe = probe_external(case.url, timeout=timeout, require_public_https=True, mode=mode)
33
+ observed_error_invariants = {f.invariant for f in probe.findings if f.severity == "error"}
34
+ if case.expected == "pass":
35
+ matched = probe.passed
36
+ else:
37
+ matched = (not probe.passed) and (
38
+ case.expected_invariant is None or case.expected_invariant in observed_error_invariants
39
+ )
40
+ result = {
41
+ "name": case.name,
42
+ "url": case.url,
43
+ "expected": case.expected,
44
+ "expected_invariant": case.expected_invariant,
45
+ "matched_expectation": matched,
46
+ "audit": probe.as_dict(),
47
+ }
48
+ except ExternalProbeError as exc:
49
+ matched = False
50
+ result = {
51
+ "name": case.name,
52
+ "url": case.url,
53
+ "expected": case.expected,
54
+ "expected_invariant": case.expected_invariant,
55
+ "matched_expectation": False,
56
+ "error": str(exc),
57
+ }
58
+ suite_passed = suite_passed and matched
59
+ results.append(result)
60
+ return {
61
+ "format": "qruntime-x402-external-suite-v2",
62
+ "audit_mode": mode,
63
+ "suite_passed": suite_passed,
64
+ "cases": results,
65
+ }