arc-builder-kit 0.2.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.
Files changed (58) hide show
  1. arc_builder_kit/__init__.py +4 -0
  2. arc_builder_kit/__main__.py +6 -0
  3. arc_builder_kit/_paths.py +47 -0
  4. arc_builder_kit/cli.py +277 -0
  5. arc_builder_kit/config/arc_testnet.facts.json +31 -0
  6. arc_builder_kit/doctor.py +936 -0
  7. arc_builder_kit/examples/agent-commerce-components/components.js +200 -0
  8. arc_builder_kit/examples/agent-commerce-components/index.html +120 -0
  9. arc_builder_kit/examples/agent-commerce-flows/flows.js +271 -0
  10. arc_builder_kit/examples/agent-commerce-flows/index.html +114 -0
  11. arc_builder_kit/examples/agent-commerce-live/commerce-live.js +190 -0
  12. arc_builder_kit/examples/agent-commerce-live/index.html +105 -0
  13. arc_builder_kit/examples/agent-commerce-review-packet/index.html +96 -0
  14. arc_builder_kit/examples/agent-commerce-review-packet/packet.js +125 -0
  15. arc_builder_kit/examples/agent-identity-profile-preview/identity.js +126 -0
  16. arc_builder_kit/examples/agent-identity-profile-preview/index.html +104 -0
  17. arc_builder_kit/examples/arc-agent-treasury-lab/index.html +152 -0
  18. arc_builder_kit/examples/arc-agent-treasury-lab/treasury.js +532 -0
  19. arc_builder_kit/examples/arc-testnet-operator-evidence/evidence.example.json +47 -0
  20. arc_builder_kit/examples/arc-testnet-wallet-send-gate/index.html +233 -0
  21. arc_builder_kit/examples/arc-testnet-wallet-send-gate/live-infrastructure-policy.example.json +59 -0
  22. arc_builder_kit/examples/arc-testnet-wallet-send-gate/wallet-send-gate.js +472 -0
  23. arc_builder_kit/examples/circle-wallet-integration/index.html +155 -0
  24. arc_builder_kit/examples/circle-wallet-integration/wallet-lab.js +91 -0
  25. arc_builder_kit/examples/job-escrow-simulator/index.html +121 -0
  26. arc_builder_kit/examples/job-escrow-simulator/simulator.js +162 -0
  27. arc_builder_kit/examples/payment-intent-demo/index.html +132 -0
  28. arc_builder_kit/examples/payment-intent-playground/index.html +301 -0
  29. arc_builder_kit/examples/payment-intent-playground/playground.js +835 -0
  30. arc_builder_kit/examples/payment-intent-receipt-matcher/index.html +157 -0
  31. arc_builder_kit/examples/payment-intent-receipt-matcher/matcher.js +877 -0
  32. arc_builder_kit/examples/receipt-verifier-playground/index.html +120 -0
  33. arc_builder_kit/examples/receipt-verifier-playground/verifier.js +226 -0
  34. arc_builder_kit/examples/receipt-viewer/index.html +138 -0
  35. arc_builder_kit/examples/receipt-viewer/receipt-viewer.js +472 -0
  36. arc_builder_kit/examples/transaction-status-playground/index.html +135 -0
  37. arc_builder_kit/examples/transaction-status-playground/status.js +518 -0
  38. arc_builder_kit/examples/x402-local-challenge-server/.env.example +25 -0
  39. arc_builder_kit/examples/x402-local-challenge-server/README.md +111 -0
  40. arc_builder_kit/examples/x402-local-challenge-server/server.py +711 -0
  41. arc_builder_kit/mcp_server.py +463 -0
  42. arc_builder_kit/release_packet.py +469 -0
  43. arc_builder_kit/templates/README.md +25 -0
  44. arc_builder_kit/templates/job-escrow-starter/README.md +25 -0
  45. arc_builder_kit/templates/job-escrow-starter/index.html +41 -0
  46. arc_builder_kit/templates/job-escrow-starter/index.js +14 -0
  47. arc_builder_kit/templates/payment-intent-starter/README.md +25 -0
  48. arc_builder_kit/templates/payment-intent-starter/index.html +42 -0
  49. arc_builder_kit/templates/payment-intent-starter/index.js +7 -0
  50. arc_builder_kit/templates/x402-agent-starter/README.md +29 -0
  51. arc_builder_kit/templates/x402-agent-starter/server.py +201 -0
  52. arc_builder_kit/validate_repo.py +2212 -0
  53. arc_builder_kit-0.2.0.dist-info/METADATA +543 -0
  54. arc_builder_kit-0.2.0.dist-info/RECORD +58 -0
  55. arc_builder_kit-0.2.0.dist-info/WHEEL +5 -0
  56. arc_builder_kit-0.2.0.dist-info/entry_points.txt +3 -0
  57. arc_builder_kit-0.2.0.dist-info/licenses/LICENSE +21 -0
  58. arc_builder_kit-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2212 @@
1
+ #!/usr/bin/env python3
2
+ """Lightweight repository validation for the Arc MCP Builder Assistant.
3
+
4
+ This script enforces a few cheap, deterministic invariants so that the
5
+ GitHub Pages site cannot accidentally regress on:
6
+
7
+ - presence of the documents required by the project
8
+ - absence of obvious credential patterns
9
+ - safe HTML (no executable scripts, no inline event handlers, no broken
10
+ anchors, external links carry rel=noopener noreferrer, images carry
11
+ alt text)
12
+
13
+ - presence of the SEO/meta basics (lang, viewport, description, charset)
14
+
15
+ It is intentionally dependency-free so it can run in CI without setup.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import re
22
+ import sys
23
+ from html.parser import HTMLParser
24
+ from pathlib import Path
25
+
26
+ from arc_builder_kit._paths import IS_SOURCE_CHECKOUT, REPO_ROOT as ROOT
27
+
28
+ INSTALLED_REQUIRED_FILES = (
29
+ "__init__.py",
30
+ "__main__.py",
31
+ "_paths.py",
32
+ "cli.py",
33
+ "doctor.py",
34
+ "mcp_server.py",
35
+ "release_packet.py",
36
+ "validate_repo.py",
37
+ "config/arc_testnet.facts.json",
38
+ "templates/payment-intent-starter/README.md",
39
+ "templates/payment-intent-starter/index.html",
40
+ "templates/x402-agent-starter/server.py",
41
+ "templates/job-escrow-starter/index.html",
42
+ "examples/payment-intent-receipt-matcher/index.html",
43
+ "examples/arc-testnet-wallet-send-gate/live-infrastructure-policy.example.json",
44
+ "examples/x402-local-challenge-server/.env.example",
45
+ "examples/x402-local-challenge-server/server.py",
46
+ )
47
+
48
+ WORKFLOW_FILES = (
49
+ ".github/workflows/validate.yml",
50
+ ".github/workflows/pages.yml",
51
+ ".github/workflows/readiness-monitor.yml",
52
+ )
53
+ REQUIRED_FILES = [
54
+ "README.md",
55
+ "index.html",
56
+ "404.html",
57
+ "robots.txt",
58
+ "sitemap.xml",
59
+ "LICENSE",
60
+ "SECURITY.md",
61
+ "CONTRIBUTING.md",
62
+ "CODE_OF_CONDUCT.md",
63
+ ".editorconfig",
64
+ ".gitattributes",
65
+ ".gitignore",
66
+ ".env.example",
67
+ "pyproject.toml",
68
+ "MANIFEST.in",
69
+ "setup.py",
70
+ "build_support.py",
71
+ "arc_builder_kit/__init__.py",
72
+ "arc_builder_kit/_paths.py",
73
+ "arc_builder_kit/cli.py",
74
+ "arc_builder_kit/mcp_server.py",
75
+ "config/arc_testnet.facts.json",
76
+ ".github/workflows/validate.yml",
77
+ ".github/workflows/pages.yml",
78
+ ".github/workflows/publish-pypi.yml",
79
+ ".github/dependabot.yml",
80
+ ".github/CODEOWNERS",
81
+ ".github/PULL_REQUEST_TEMPLATE.md",
82
+ ".github/ISSUE_TEMPLATE/config.yml",
83
+ ".github/ISSUE_TEMPLATE/bug_report.yml",
84
+ ".github/ISSUE_TEMPLATE/feature_request.yml",
85
+ "docs/arc-mcp-setup.md",
86
+ "docs/arc-docs-map.md",
87
+ "docs/deploy-contracts-arc.md",
88
+ "docs/agent-identity-erc8004.md",
89
+ "docs/agent-identity-profile-preview.md",
90
+ "docs/builder-workflows.md",
91
+ "docs/payment-intent-demo.md",
92
+ "docs/payment-intent-quickstart.md",
93
+ "docs/payment-status-tutorial.md",
94
+ "docs/contest-demo-script.md",
95
+ "docs/content-pack.md",
96
+ "docs/public-launch-packet.md",
97
+ "docs/arc-discord-introduction.md",
98
+ "docs/receipt-verifier-playground.md",
99
+ "docs/receipt-viewer.md",
100
+ "docs/payment-intent-receipt-matcher.md",
101
+ "docs/transaction-status-playground.md",
102
+ "docs/x402-mcp-manifest.md",
103
+ "docs/x402-demo-transcript.md",
104
+ "docs/arc-production-deployment.md",
105
+ "docs/prompt-library.md",
106
+ "docs/arc-builder-readiness-checklist.md",
107
+ "docs/completion-contract.md",
108
+ "docs/current-readiness-report.md",
109
+ "docs/arc-testnet-integration-runbook.md",
110
+ "docs/arc-wallet-integration-notes.md",
111
+ "docs/wallet-preflight-contract.md",
112
+ "docs/arc-testnet-send-readiness-gate.md",
113
+ "docs/guarded-wallet-send-runbook.md",
114
+ "docs/custody-and-mainnet-gates.md",
115
+ "docs/arc-testnet-operator-runbook.md",
116
+ "docs/arc-testnet-operator-evidence.md",
117
+ "docs/agent-commerce-use-cases.md",
118
+ "docs/agent-commerce-components.md",
119
+ "docs/agent-commerce-flow-library.md",
120
+ "docs/agent-commerce-review-packet.md",
121
+ "docs/job-escrow-demo.md",
122
+ "docs/arc-agent-treasury-lab.md",
123
+ "docs/circle-wallet-integration.md",
124
+ "docs/agent-commerce-live-evidence.md",
125
+ "docs/agentic-maintainer-loop.md",
126
+ "docs/mcp-query-examples.md",
127
+ "docs/arc-house-submission.md",
128
+ "docs/builder-tooling.md",
129
+ "docs/build-log.md",
130
+ "docs/view.html",
131
+ "docs/viewer.js",
132
+ "prompts/explain-arc-docs.md",
133
+ "prompts/build-payment-intent-demo.md",
134
+ "prompts/register-agent-notes.md",
135
+ "prompts/deploy-contracts-on-arc.md",
136
+ "prompts/wire-arc-testnet-status.md",
137
+ "prompts/agentic-maintainer-loop.md",
138
+ "examples/payment-intent-demo/index.html",
139
+ "examples/payment-intent-playground/index.html",
140
+ "examples/payment-intent-playground/playground.js",
141
+ "examples/receipt-verifier-playground/index.html",
142
+ "examples/receipt-verifier-playground/verifier.js",
143
+ "examples/receipt-viewer/index.html",
144
+ "examples/receipt-viewer/receipt-viewer.js",
145
+ "examples/payment-intent-receipt-matcher/index.html",
146
+ "examples/payment-intent-receipt-matcher/matcher.js",
147
+ "examples/transaction-status-playground/index.html",
148
+ "examples/transaction-status-playground/status.js",
149
+ "examples/arc-testnet-wallet-send-gate/index.html",
150
+ "examples/arc-testnet-wallet-send-gate/wallet-send-gate.js",
151
+ "examples/arc-testnet-wallet-send-gate/live-infrastructure-policy.example.json",
152
+ "examples/agent-commerce-components/index.html",
153
+ "examples/agent-commerce-components/components.js",
154
+ "examples/agent-commerce-flows/index.html",
155
+ "examples/agent-commerce-flows/flows.js",
156
+ "examples/agent-commerce-review-packet/index.html",
157
+ "examples/agent-commerce-review-packet/packet.js",
158
+ "examples/agent-identity-profile-preview/index.html",
159
+ "examples/agent-identity-profile-preview/identity.js",
160
+ "examples/job-escrow-simulator/index.html",
161
+ "examples/job-escrow-simulator/simulator.js",
162
+ "examples/arc-agent-treasury-lab/index.html",
163
+ "examples/arc-agent-treasury-lab/treasury.js",
164
+ "examples/circle-wallet-integration/index.html",
165
+ "examples/circle-wallet-integration/wallet-lab.js",
166
+ "examples/agent-commerce-live/index.html",
167
+ "examples/agent-commerce-live/commerce-live.js",
168
+ "examples/arc-testnet-operator-evidence/evidence.example.json",
169
+ "examples/x402-local-challenge-server/README.md",
170
+ "examples/x402-local-challenge-server/.env.example",
171
+ "scripts/validate_arc_testnet_facts.py",
172
+ "scripts/test_arc_testnet_facts.py",
173
+ "examples/x402-local-challenge-server/server.py",
174
+ "scripts/check_arc_testnet_status.py",
175
+ "scripts/check_completion.py",
176
+ "scripts/live_arc_gateway_smoke.py",
177
+ "scripts/test_all.py",
178
+ "scripts/scan_for_secrets.py",
179
+ "scripts/serve_local.py",
180
+ "scripts/test_arc_production_deployment.py",
181
+ "scripts/test_arc_testnet_status_helper.py",
182
+ "scripts/test_completion_contract.py",
183
+ "scripts/test_public_claims.py",
184
+ "scripts/test_docs_viewer_security.py",
185
+ "scripts/test_docs_viewer_behavior.py",
186
+ "scripts/docs_viewer_behavior_harness.mjs",
187
+ "scripts/test_workflow_security.py",
188
+ "scripts/generate_arc_release_packet.py",
189
+ "scripts/test_arc_release_packet.py",
190
+ "scripts/test_payment_intent_playground.py",
191
+ "scripts/test_x402_boundary.py",
192
+ "scripts/test_transaction_status_playground.py",
193
+ "scripts/test_transaction_status_behavior.py",
194
+ "scripts/transaction_status_behavior_harness.mjs",
195
+ "scripts/test_receipt_viewer.py",
196
+ "scripts/receipt_viewer_behavior_harness.mjs",
197
+ "scripts/test_payment_intent_receipt_matcher.py",
198
+ "scripts/payment_intent_receipt_matcher_behavior_harness.mjs",
199
+ "scripts/test_arc_testnet_wallet_send_gate.py",
200
+ "scripts/test_arc_testnet_wallet_send_behavior.py",
201
+ "scripts/wallet_send_behavior_harness.mjs",
202
+ "scripts/validate_live_infrastructure_policy.py",
203
+ "scripts/test_live_infrastructure_policy.py",
204
+ "scripts/test_agent_commerce_components.py",
205
+ "scripts/test_agent_commerce_flows.py",
206
+ "scripts/test_agent_commerce_review_packet.py",
207
+ "scripts/test_agent_identity_profile_preview.py",
208
+ "scripts/test_job_escrow_simulator.py",
209
+ "scripts/test_arc_agent_treasury_lab.py",
210
+ "scripts/arc_agent_treasury_behavior_harness.mjs",
211
+ "scripts/validate_operator_evidence.py",
212
+ "scripts/test_operator_evidence.py",
213
+ "scripts/generate_operator_evidence_draft.py",
214
+ "scripts/test_operator_evidence_draft.py",
215
+ "scripts/report_operator_evidence.py",
216
+ "scripts/test_operator_evidence_report.py",
217
+ "scripts/arc_builder_cli.py",
218
+ "scripts/test_arc_builder_cli.py",
219
+ "scripts/arc_builder_mcp_server.py",
220
+ "scripts/test_arc_builder_mcp_server.py",
221
+ "scripts/test_templates.py",
222
+ "scripts/test_package_distribution.py",
223
+ "scripts/pre_commit_guard.py",
224
+ "scripts/install_repo_hooks.py",
225
+ "scripts/hooks/pre-commit",
226
+ "scripts/test_pre_commit_guard.py",
227
+ "scripts/check_release_version.py",
228
+ "scripts/test_release_version.py",
229
+ "templates/README.md",
230
+ "templates/payment-intent-starter/README.md",
231
+ "templates/payment-intent-starter/index.html",
232
+ "templates/payment-intent-starter/index.js",
233
+ "templates/x402-agent-starter/README.md",
234
+ "templates/x402-agent-starter/server.py",
235
+ "templates/job-escrow-starter/README.md",
236
+ "templates/job-escrow-starter/index.html",
237
+ "templates/job-escrow-starter/index.js",
238
+ "assets/screenshots/landing.png",
239
+ "assets/screenshots/security-viewer.png",
240
+ "assets/screenshots/payment-intent-playground.png",
241
+ "assets/screenshots/job-escrow-simulator.png",
242
+ ]
243
+
244
+ # Every HTML file in the repo is checked with these full invariants.
245
+ HTML_FILES_TO_VALIDATE = [
246
+ "index.html",
247
+ "404.html",
248
+ "docs/view.html",
249
+ "examples/payment-intent-demo/index.html",
250
+ "examples/payment-intent-playground/index.html",
251
+ "examples/receipt-verifier-playground/index.html",
252
+ "examples/receipt-viewer/index.html",
253
+ "examples/payment-intent-receipt-matcher/index.html",
254
+ "examples/transaction-status-playground/index.html",
255
+ "examples/arc-testnet-wallet-send-gate/index.html",
256
+ "examples/agent-commerce-components/index.html",
257
+ "examples/agent-commerce-flows/index.html",
258
+ "examples/agent-commerce-review-packet/index.html",
259
+ "examples/agent-identity-profile-preview/index.html",
260
+ "examples/job-escrow-simulator/index.html",
261
+ "examples/arc-agent-treasury-lab/index.html",
262
+ "templates/payment-intent-starter/index.html",
263
+ "templates/job-escrow-starter/index.html",
264
+ ]
265
+
266
+ CANONICAL_BASE_URL = "https://anstrays.github.io/arc-mcp-builder-assistant/"
267
+ SITE_BASE_PATH = "/arc-mcp-builder-assistant/"
268
+ SITEMAP_REQUIRED_LOCATIONS = (
269
+ CANONICAL_BASE_URL,
270
+ CANONICAL_BASE_URL + "docs/view.html",
271
+ CANONICAL_BASE_URL + "examples/payment-intent-demo/",
272
+ CANONICAL_BASE_URL + "examples/payment-intent-playground/",
273
+ CANONICAL_BASE_URL + "examples/receipt-verifier-playground/",
274
+ CANONICAL_BASE_URL + "examples/receipt-viewer/",
275
+ CANONICAL_BASE_URL + "examples/payment-intent-receipt-matcher/",
276
+ CANONICAL_BASE_URL + "examples/transaction-status-playground/",
277
+ CANONICAL_BASE_URL + "examples/arc-testnet-wallet-send-gate/",
278
+ CANONICAL_BASE_URL + "examples/agent-commerce-components/",
279
+ CANONICAL_BASE_URL + "examples/agent-commerce-flows/",
280
+ CANONICAL_BASE_URL + "examples/agent-commerce-review-packet/",
281
+ CANONICAL_BASE_URL + "examples/agent-identity-profile-preview/",
282
+ CANONICAL_BASE_URL + "examples/job-escrow-simulator/",
283
+ CANONICAL_BASE_URL + "examples/arc-agent-treasury-lab/",
284
+ CANONICAL_BASE_URL + "examples/circle-wallet-integration/",
285
+ CANONICAL_BASE_URL + "examples/agent-commerce-live/",
286
+ )
287
+ REDUCED_MOTION_MEDIA_RE = re.compile(
288
+ r"@media\s*\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)",
289
+ re.IGNORECASE,
290
+ )
291
+ DEMO_SAFETY_MARKERS = (
292
+ "does not connect to a wallet",
293
+ "broadcast transactions",
294
+ "talk to any backend",
295
+ "human keeps approval control",
296
+ "controls are intentionally disabled",
297
+ )
298
+
299
+ SECRET_PATTERNS = [
300
+ re.compile(r"ghp_[A-Za-z0-9_]{20,}"),
301
+ re.compile(r"github_pat_[A-Za-z0-9_]{20,}"),
302
+ re.compile(r"AKIA[0-9A-Z]{16}"),
303
+ re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}"),
304
+ re.compile(r"[0-9]{8,10}:[A-Za-z0-9_-]{35}"), # Telegram bot token shape
305
+ re.compile(
306
+ r"(?i)(?:export\s+)?[A-Z0-9_]*(?:api[_-]?key|secret|password|private[_-]?key|"
307
+ r"entity[_-]?secret|bot[_-]?token)[A-Z0-9_]*[ \t]*[:=][ \t]*['\"]?"
308
+ r"(?![ \t]*(?:process\.env\.|os\.environ|placeholder|example|changeme|todo|your[_-]?|\*+|<|\[|$))[^'\"\s#]{8,}"
309
+ ),
310
+ re.compile(r"-----BEGIN (RSA|OPENSSH|EC|DSA) PRIVATE KEY-----"),
311
+ ]
312
+
313
+ TEXT_SUFFIXES_TO_SECRET_SCAN = {
314
+ "",
315
+ ".css",
316
+ ".env",
317
+ ".example",
318
+ ".html",
319
+ ".js",
320
+ ".json",
321
+ ".md",
322
+ ".py",
323
+ ".txt",
324
+ ".xml",
325
+ ".yaml",
326
+ ".yml",
327
+ }
328
+
329
+ MOJIBAKE_MARKERS = (
330
+ "\u00c3",
331
+ "\u00c2",
332
+ "\u00e2\u20ac",
333
+ "\u0432\u0402",
334
+ "\u0420\u00b0",
335
+ )
336
+
337
+ # Files we never want to scan for secrets — they only describe patterns,
338
+ # not real credentials.
339
+ SECRET_SCAN_SKIP = {
340
+ Path("scripts/validate_repo.py"),
341
+ Path("scripts/scan_for_secrets.py"),
342
+ }
343
+
344
+ # script type values that are inert (no JavaScript execution).
345
+ INERT_SCRIPT_TYPES = {
346
+ "application/ld+json",
347
+ "application/json",
348
+ "text/plain",
349
+ }
350
+
351
+
352
+ class HtmlInspector(HTMLParser):
353
+ def __init__(self) -> None:
354
+ super().__init__()
355
+ self.elements: list[tuple[str, dict[str, str]]] = []
356
+ self.ids: set[str] = set()
357
+ self.html_lang: str | None = None
358
+ self.has_charset = False
359
+ self.has_viewport = False
360
+ self.has_description = False
361
+ self.has_csp = False
362
+
363
+ self.script_type_stack: list[str] = []
364
+ self.script_text_segments: list[str] = []
365
+ self._in_inert_script = False
366
+
367
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
368
+ attr = {key.lower(): (value or "") for key, value in attrs}
369
+ self.elements.append((tag, attr))
370
+ if "id" in attr:
371
+ self.ids.add(attr["id"])
372
+ if tag == "html":
373
+ self.html_lang = attr.get("lang")
374
+ if tag == "meta":
375
+ if attr.get("charset"):
376
+ self.has_charset = True
377
+ name = attr.get("name", "").lower()
378
+ if name == "viewport":
379
+ self.has_viewport = True
380
+ if name == "description" and attr.get("content"):
381
+ self.has_description = True
382
+ if attr.get("http-equiv", "").lower() == "content-security-policy" and attr.get("content"):
383
+ self.has_csp = True
384
+
385
+ if tag == "script":
386
+ script_type = attr.get("type", "").lower()
387
+ self.script_type_stack.append(script_type)
388
+ self._in_inert_script = script_type in INERT_SCRIPT_TYPES
389
+
390
+ def handle_endtag(self, tag: str) -> None:
391
+ if tag == "script" and self.script_type_stack:
392
+ self.script_type_stack.pop()
393
+ self._in_inert_script = bool(
394
+ self.script_type_stack
395
+ and self.script_type_stack[-1] in INERT_SCRIPT_TYPES
396
+ )
397
+
398
+ def handle_data(self, data: str) -> None:
399
+ if self.script_type_stack and not self._in_inert_script and data.strip():
400
+ self.script_text_segments.append(data)
401
+
402
+
403
+ def fail(message: str) -> None:
404
+ raise SystemExit(f"validation failed: {message}")
405
+
406
+
407
+ def validate_required_files() -> None:
408
+ for relative in REQUIRED_FILES:
409
+ path = ROOT / relative
410
+ if not path.is_file():
411
+ fail(f"missing required file: {relative}")
412
+
413
+
414
+ def validate_workflow_security() -> None:
415
+ """Keep CI reproducible, least-privilege, and aligned with local tests."""
416
+ action_pin = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[0-9a-f]{40}$")
417
+ permission_grant = re.compile(
418
+ r"^\s*([a-z][a-z0-9-]*):\s*(read|write)\s*(?:#.*)?$",
419
+ re.IGNORECASE,
420
+ )
421
+
422
+ def parsed_permissions(relative: str, text: str) -> set[tuple[str, str]]:
423
+ lines = text.splitlines()
424
+ permission_lines = [
425
+ index
426
+ for index, line in enumerate(lines)
427
+ if line.lstrip().startswith("permissions:")
428
+ and not line.lstrip().startswith("#")
429
+ ]
430
+ if len(permission_lines) != 1:
431
+ fail(f"{relative}: workflow must contain exactly one permissions block")
432
+ start = permission_lines[0]
433
+ if lines[start] != "permissions:":
434
+ fail(f"{relative}: permissions must be one explicit top-level map")
435
+
436
+ observed: set[tuple[str, str]] = set()
437
+ for line in lines[start + 1:]:
438
+ if line and not line[0].isspace():
439
+ break
440
+ if not line.strip() or line.lstrip().startswith("#"):
441
+ continue
442
+ match = permission_grant.fullmatch(line)
443
+ if not match:
444
+ fail(f"{relative}: invalid permissions entry: {line.strip()}")
445
+ observed.add((match.group(1).lower(), match.group(2).lower()))
446
+ return observed
447
+
448
+ for relative in WORKFLOW_FILES:
449
+ text = (ROOT / relative).read_text(encoding="utf-8")
450
+ active_lines = [
451
+ line.strip().removeprefix("- ").lstrip()
452
+ for line in text.splitlines()
453
+ if line.strip() and not line.strip().startswith("#")
454
+ ]
455
+ for forbidden in ("pull_request_target:", "workflow_run:"):
456
+ if forbidden in text:
457
+ fail(f"{relative}: forbidden privileged workflow trigger: {forbidden}")
458
+ active_actions: set[str] = set()
459
+ for line in text.splitlines():
460
+ stripped = line.strip()
461
+ if stripped.startswith("- "):
462
+ stripped = stripped[2:].lstrip()
463
+ if not stripped.startswith("uses:"):
464
+ continue
465
+ action = stripped.removeprefix("uses:").split("#", 1)[0].strip()
466
+ if action.startswith("./"):
467
+ continue
468
+ if not action_pin.fullmatch(action):
469
+ fail(f"{relative}: action must use a full commit SHA: {action}")
470
+ active_actions.add(action.split("@", 1)[0])
471
+ for action in ("actions/setup-python", "actions/setup-node"):
472
+ if action not in active_actions:
473
+ fail(f"{relative}: missing active runtime setup action: {action}")
474
+ for marker in (
475
+ 'python-version: "3.12"',
476
+ 'node-version: "22"',
477
+ "run: python scripts/test_all.py",
478
+ ):
479
+ if marker not in active_lines:
480
+ fail(f"{relative}: missing active runtime/test contract marker: {marker}")
481
+
482
+ expected_permissions = {
483
+ WORKFLOW_FILES[0]: {("contents", "read")},
484
+ WORKFLOW_FILES[1]: {
485
+ ("contents", "read"),
486
+ ("pages", "write"),
487
+ ("id-token", "write"),
488
+ },
489
+ WORKFLOW_FILES[2]: {("contents", "read")},
490
+ }
491
+ for relative, expected in expected_permissions.items():
492
+ text = (ROOT / relative).read_text(encoding="utf-8")
493
+ observed = parsed_permissions(relative, text)
494
+ if observed != expected:
495
+ fail(
496
+ f"{relative}: workflow permissions must be exactly "
497
+ f"{sorted(expected)}; observed {sorted(observed)}"
498
+ )
499
+
500
+ validate = (ROOT / WORKFLOW_FILES[0]).read_text(encoding="utf-8")
501
+ validate_active_lines = [
502
+ line.strip().removeprefix("- ").lstrip()
503
+ for line in validate.splitlines()
504
+ if line.strip() and not line.strip().startswith("#")
505
+ ]
506
+ for marker in (
507
+ "python3 scripts/arc_builder_doctor.py --markdown",
508
+ '>> "$GITHUB_STEP_SUMMARY"',
509
+ ):
510
+ if not any(marker in line for line in validate_active_lines):
511
+ fail(f"{WORKFLOW_FILES[0]}: missing active Doctor summary marker: {marker}")
512
+
513
+ monitor = (ROOT / WORKFLOW_FILES[2]).read_text(encoding="utf-8")
514
+ monitor_active_lines = [
515
+ line.strip().removeprefix("- ").lstrip()
516
+ for line in monitor.splitlines()
517
+ if line.strip() and not line.strip().startswith("#")
518
+ ]
519
+ for marker in (
520
+ "schedule:",
521
+ "workflow_dispatch:",
522
+ "timeout-minutes: 10",
523
+ "set +e",
524
+ "--include-arc-rpc",
525
+ "--include-public-site",
526
+ "--strict",
527
+ "--markdown",
528
+ '"$RUNNER_TEMP/arc-builder-doctor.md"',
529
+ '"$GITHUB_STEP_SUMMARY"',
530
+ "doctor_status=$?",
531
+ "set -e",
532
+ 'exit "$doctor_status"',
533
+ ):
534
+ if not any(marker in line for line in monitor_active_lines):
535
+ fail(f"{WORKFLOW_FILES[2]}: missing active readiness-monitor safety marker: {marker}")
536
+
537
+ validate_pypi_publish_workflow()
538
+
539
+
540
+ def validate_pypi_publish_workflow() -> None:
541
+ """Keep package publication release-only, keyless, and least-privilege."""
542
+ relative = ".github/workflows/publish-pypi.yml"
543
+ text = (ROOT / relative).read_text(encoding="utf-8")
544
+ active_lines = [
545
+ line.strip().removeprefix("- ").lstrip()
546
+ for line in text.splitlines()
547
+ if line.strip() and not line.strip().startswith("#")
548
+ ]
549
+
550
+ if "pull_request_target:" in text or "workflow_run:" in text or "workflow_dispatch:" in text:
551
+ fail(f"{relative}: publishing must only be triggered by a published GitHub release")
552
+ for marker in (
553
+ "release:",
554
+ "types: [published]",
555
+ "if: github.event.release.prerelease == false",
556
+ "name: pypi",
557
+ 'python scripts/check_release_version.py --tag "${GITHUB_REF_NAME}"',
558
+ "python scripts/test_all.py",
559
+ "build==1.5.0",
560
+ "twine==6.2.0",
561
+ "python -m build",
562
+ "python -m twine check dist/*",
563
+ "pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b",
564
+ ):
565
+ if not any(marker in line for line in active_lines):
566
+ fail(f"{relative}: missing active trusted-publishing marker: {marker}")
567
+
568
+ action_pin = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[0-9a-f]{40}$")
569
+ for line in text.splitlines():
570
+ stripped = line.strip()
571
+ if stripped.startswith("- "):
572
+ stripped = stripped[2:].lstrip()
573
+ if stripped.startswith("uses:"):
574
+ ref = stripped.split(":", 1)[1].strip().split()[0]
575
+ if not action_pin.fullmatch(ref):
576
+ fail(f"{relative}: action must be pinned to a full commit SHA: {ref}")
577
+
578
+ permission_headers = [line for line in text.splitlines() if line.strip() == "permissions:"]
579
+ if len(permission_headers) != 2:
580
+ fail(f"{relative}: expected top-level and publish-job permissions only")
581
+ top_permissions = re.search(r"(?m)^permissions:\n((?: [a-z][a-z0-9-]*: (?:read|write)\n)+)", text)
582
+ job_permissions = re.search(r"(?m)^ permissions:\n((?: [a-z][a-z0-9-]*: (?:read|write)\n)+)", text)
583
+ if top_permissions is None or set(top_permissions.group(1).splitlines()) != {" contents: read"}:
584
+ fail(f"{relative}: top-level permissions must be exactly contents: read")
585
+ if job_permissions is None or set(job_permissions.group(1).splitlines()) != {
586
+ " contents: read",
587
+ " id-token: write",
588
+ }:
589
+ fail(f"{relative}: publish job permissions must be exactly contents: read and id-token: write")
590
+
591
+ lowered = text.lower()
592
+ for forbidden in ("secrets.", "pypi_api_token", "password:", "username: __token__", "skip-existing"):
593
+ if forbidden in lowered:
594
+ fail(f"{relative}: forbidden token-based or fail-open publishing marker: {forbidden}")
595
+
596
+
597
+ def validate_no_secrets() -> None:
598
+ for path in ROOT.rglob("*"):
599
+ if ".git" in path.parts or not path.is_file():
600
+ continue
601
+ relative = path.relative_to(ROOT)
602
+ if relative in SECRET_SCAN_SKIP:
603
+ continue
604
+ if path.suffix.lower() not in TEXT_SUFFIXES_TO_SECRET_SCAN:
605
+ continue
606
+ try:
607
+ text = path.read_text(encoding="utf-8")
608
+ except UnicodeDecodeError:
609
+ fail(f"non-UTF-8 text file blocks secret scan: {relative}")
610
+ continue
611
+ for pattern in SECRET_PATTERNS:
612
+ if pattern.search(text):
613
+ fail(f"potential secret pattern in {relative}")
614
+
615
+
616
+ def validate_public_text_integrity() -> None:
617
+ """Reject common UTF-8/Windows-1252 decoding damage in public text files."""
618
+ for path in ROOT.rglob("*"):
619
+ if ".git" in path.parts or ".hermes" in path.parts or not path.is_file():
620
+ continue
621
+ if path.suffix.lower() not in TEXT_SUFFIXES_TO_SECRET_SCAN:
622
+ continue
623
+ relative = path.relative_to(ROOT)
624
+ try:
625
+ text = path.read_text(encoding="utf-8")
626
+ except UnicodeDecodeError:
627
+ continue
628
+ for marker in MOJIBAKE_MARKERS:
629
+ if marker in text:
630
+ fail(f"possible mojibake marker in {relative}: {marker!r}")
631
+
632
+
633
+ def validate_repository_line_ending_policy() -> None:
634
+ """Keep Windows, WSL, and CI diffs deterministic without bulk churn."""
635
+ relative = ".gitattributes"
636
+ attributes = (ROOT / relative).read_text(encoding="utf-8")
637
+ for marker in (
638
+ "* text=auto eol=lf",
639
+ "*.png binary",
640
+ "*.jpg binary",
641
+ "*.jpeg binary",
642
+ "*.gif binary",
643
+ "*.webp binary",
644
+ ):
645
+ if marker not in attributes:
646
+ fail(f"{relative}: missing line-ending/binary marker: {marker}")
647
+
648
+
649
+ def validate_documented_secret_handling() -> None:
650
+ """Keep builder tutorials from normalizing raw-key or frontend-secret use."""
651
+ docs_map = (ROOT / "docs/arc-docs-map.md").read_text(encoding="utf-8")
652
+ for forbidden in ("--private-key", "$PRIVATE_KEY"):
653
+ if forbidden in docs_map:
654
+ fail(f"docs/arc-docs-map.md: forbidden raw private-key deploy marker: {forbidden}")
655
+ for marker in (
656
+ "cast wallet import arc-testnet-review",
657
+ "--account arc-testnet-review",
658
+ "Never put a raw private key in",
659
+ ):
660
+ if marker not in docs_map:
661
+ fail(f"docs/arc-docs-map.md: missing encrypted-keystore guidance: {marker}")
662
+
663
+ deploy_notes = (ROOT / "docs/deploy-contracts-arc.md").read_text(encoding="utf-8")
664
+ for marker in (
665
+ "separate backend/custody integration path",
666
+ "keep credentials out of frontend code and chat",
667
+ "use a deployment secret manager",
668
+ ):
669
+ if marker not in deploy_notes:
670
+ fail(f"docs/deploy-contracts-arc.md: missing custody/secret boundary: {marker}")
671
+
672
+
673
+ def validate_html_file(relative: str) -> None:
674
+ html_path = ROOT / relative
675
+ html = html_path.read_text(encoding="utf-8")
676
+ if "<!doctype html>" not in html.lower():
677
+ fail(f"{relative} is missing doctype")
678
+
679
+ inspector = HtmlInspector()
680
+ inspector.feed(html)
681
+
682
+ if inspector.script_text_segments:
683
+ fail(
684
+ f"{relative} should not contain executable scripts; "
685
+ "only inert types (e.g. application/ld+json) are allowed"
686
+ )
687
+
688
+ for tag, attrs in inspector.elements:
689
+ if tag == "script":
690
+ script_type = attrs.get("type", "").lower()
691
+ if script_type and script_type not in INERT_SCRIPT_TYPES:
692
+ fail(f"{relative}: executable script type is not allowed: {script_type}")
693
+ if attrs.get("src"):
694
+ src = attrs.get("src", "").strip()
695
+ if src.startswith(("http://", "https://", "//")):
696
+ fail(f"{relative}: remote scripts are not allowed: {src}")
697
+ target = _resolved_local_link_target(relative, src)
698
+ if target is None or not target.is_file():
699
+ fail(f"{relative}: local script is missing: {src}")
700
+ for key in attrs:
701
+ if key.lower().startswith("on"):
702
+ fail(f"{relative}: inline event handler is not allowed on <{tag}>: {key}")
703
+ if tag == "img":
704
+ if "alt" not in attrs:
705
+ fail(f"{relative}: <img> missing alt attribute (src={attrs.get('src','?')})")
706
+ if tag == "a":
707
+ href = attrs.get("href", "")
708
+ normalized_href = href.strip().lower()
709
+ if normalized_href.startswith("javascript:"):
710
+ fail(f"{relative}: unsafe javascript URL: {href}")
711
+ if href.startswith("#") and len(href) > 1 and href[1:] not in inspector.ids:
712
+ fail(f"{relative}: broken anchor link: {href}")
713
+ if attrs.get("target", "").strip().lower() == "_blank":
714
+ rel = {value.lower() for value in attrs.get("rel", "").split()}
715
+ if not {"noopener", "noreferrer"}.issubset(rel):
716
+ fail(f"{relative}: external link missing rel noopener noreferrer: {href}")
717
+
718
+ if not inspector.html_lang:
719
+ fail(f"{relative}: <html> tag must declare a lang attribute")
720
+ if not inspector.has_charset:
721
+ fail(f"{relative}: missing <meta charset>")
722
+ if not inspector.has_viewport:
723
+ fail(f"{relative}: missing <meta name=\"viewport\">")
724
+ if not inspector.has_description:
725
+ fail(f"{relative}: missing a non-empty <meta name=\"description\">")
726
+ if not inspector.has_csp:
727
+ fail(f"{relative}: missing Content-Security-Policy meta tag")
728
+
729
+ def validate_html() -> None:
730
+ for relative in HTML_FILES_TO_VALIDATE:
731
+ validate_html_file(relative)
732
+
733
+
734
+ def validate_reduced_motion_css() -> None:
735
+ for relative in HTML_FILES_TO_VALIDATE:
736
+ html = (ROOT / relative).read_text(encoding="utf-8")
737
+ if not REDUCED_MOTION_MEDIA_RE.search(html):
738
+ fail(f"{relative}: missing prefers-reduced-motion CSS rule")
739
+ if "transition: none" not in html:
740
+ fail(f"{relative}: reduced-motion rule must disable transitions")
741
+ index_html = (ROOT / "index.html").read_text(encoding="utf-8")
742
+ if "scroll-behavior: auto" not in index_html:
743
+ fail("index.html: reduced-motion rule must disable smooth scrolling")
744
+
745
+
746
+ def validate_responsive_layout_guards() -> None:
747
+ """Keep narrow layouts from expanding beyond the mobile viewport."""
748
+ required = {
749
+ "index.html": (
750
+ ".hero-grid > *, .split > *, .playground-grid > * { min-width: 0; }",
751
+ "grid-template-columns: minmax(0, 1fr)",
752
+ "overflow-wrap: anywhere",
753
+ ),
754
+ "docs/view.html": (
755
+ "grid-template-columns: minmax(0, 1fr) minmax(0, 300px)",
756
+ ".panel, aside { min-width: 0; }",
757
+ "overflow-wrap: anywhere",
758
+ ),
759
+ "examples/payment-intent-playground/index.html": (
760
+ "main > *, .grid > * { min-width: 0; }",
761
+ ),
762
+ "examples/receipt-verifier-playground/index.html": (
763
+ "main > *, .grid > * { min-width:0; }",
764
+ ),
765
+ "examples/transaction-status-playground/index.html": (
766
+ "main > *, .grid > * { min-width:0; }",
767
+ ),
768
+ "examples/receipt-viewer/index.html": (
769
+ "main > *, .grid > * { min-width:0; }",
770
+ "overflow-wrap:anywhere",
771
+ ),
772
+ "examples/agent-commerce-components/index.html": (
773
+ ".wrap > *, .grid > *, .cards > * { min-width: 0; }",
774
+ ".actions { flex-wrap: wrap; }",
775
+ ),
776
+ "examples/arc-agent-treasury-lab/index.html": (
777
+ "grid-template-columns: minmax(320px, .78fr) minmax(420px, 1.22fr)",
778
+ "@media (max-width: 980px) { .layout { grid-template-columns: 1fr; }",
779
+ "overflow-wrap: anywhere",
780
+ ),
781
+ }
782
+ for relative, markers in required.items():
783
+ text = (ROOT / relative).read_text(encoding="utf-8")
784
+ for marker in markers:
785
+ if marker not in text:
786
+ fail(f"{relative}: missing responsive layout guard: {marker}")
787
+
788
+
789
+ def validate_public_inventory_counts() -> None:
790
+ """Keep the landing-page repository counts tied to committed surfaces."""
791
+ index = (ROOT / "index.html").read_text(encoding="utf-8")
792
+ counts = {
793
+ "Docs": len(list((ROOT / "docs").glob("*.md"))),
794
+ "Examples": len([path for path in (ROOT / "examples").iterdir() if path.is_dir()]),
795
+ "Prompt packs": len([path for path in (ROOT / "prompts").iterdir() if path.is_file()]),
796
+ }
797
+ for label, count in counts.items():
798
+ marker = f'<div class="metric"><span>{label}</span><strong>{count}</strong></div>'
799
+ if marker not in index:
800
+ fail(f"index.html: stale public inventory count for {label}: expected {count}")
801
+
802
+
803
+ def _resolved_local_link_target(source_relative: str, href: str) -> Path | None:
804
+ href = href.strip()
805
+ if (
806
+ not href
807
+ or href.startswith("#")
808
+ or href.startswith(("http://", "https://", "mailto:", "tel:", "data:"))
809
+ ):
810
+ return None
811
+
812
+ href_without_fragment = href.split("#", 1)[0]
813
+ if href_without_fragment.startswith(SITE_BASE_PATH):
814
+ href_without_fragment = href_without_fragment[len(SITE_BASE_PATH):]
815
+ target = ROOT / href_without_fragment
816
+ elif href_without_fragment.startswith("/"):
817
+ target = ROOT / href_without_fragment.lstrip("/")
818
+ else:
819
+ target = ROOT / source_relative
820
+ target = target.parent / href_without_fragment
821
+
822
+ if href.endswith("/"):
823
+ target = target / "index.html"
824
+ return target.resolve()
825
+
826
+
827
+ def validate_local_links() -> None:
828
+ root_resolved = ROOT.resolve()
829
+ for relative in HTML_FILES_TO_VALIDATE:
830
+ html = (ROOT / relative).read_text(encoding="utf-8")
831
+ inspector = HtmlInspector()
832
+ inspector.feed(html)
833
+ for tag, attrs in inspector.elements:
834
+ if tag != "a":
835
+ continue
836
+ href = attrs.get("href", "")
837
+ target = _resolved_local_link_target(relative, href)
838
+ if target is None:
839
+ continue
840
+ if root_resolved not in (target, *target.parents):
841
+ fail(f"{relative}: local link escapes repository root: {href}")
842
+ if not target.is_file():
843
+ fail(f"{relative}: broken local link: {href}")
844
+
845
+
846
+ def validate_markdown_local_links() -> None:
847
+ """Ensure committed relative links in public Markdown resolve locally."""
848
+ root_resolved = ROOT.resolve()
849
+ link_re = re.compile(r"!?\[[^\]]*\]\(([^)\s]+)(?:\s+['\"][^'\"]*['\"])?\)")
850
+ for path in ROOT.rglob("*.md"):
851
+ if ".git" in path.parts or ".hermes" in path.parts:
852
+ continue
853
+ relative = path.relative_to(ROOT).as_posix()
854
+ text = path.read_text(encoding="utf-8")
855
+ for href in link_re.findall(text):
856
+ href = href.strip("<>")
857
+ target = _resolved_local_link_target(relative, href)
858
+ if target is None:
859
+ continue
860
+ if href.split("#", 1)[0].endswith("/") and target.name == "index.html":
861
+ target = target.parent
862
+ if root_resolved not in (target, *target.parents):
863
+ fail(f"{relative}: Markdown link escapes repository root: {href}")
864
+ if not target.exists():
865
+ fail(f"{relative}: broken Markdown link: {href}")
866
+
867
+
868
+ def validate_no_raw_markdown_links() -> None:
869
+ """Keep user-facing HTML Markdown links on the styled viewer, not raw files."""
870
+ raw_markdown_link_re = re.compile(r"href=[\"']([^\"']+\.md(?:#[^\"']*)?)[\"']", re.IGNORECASE)
871
+ for relative in HTML_FILES_TO_VALIDATE:
872
+ html = (ROOT / relative).read_text(encoding="utf-8")
873
+ for href in raw_markdown_link_re.findall(html):
874
+ if "docs/view.html#" not in href:
875
+ fail(f"{relative}: link to raw Markdown should use docs/view.html: {href}")
876
+
877
+
878
+ def validate_docs_viewer_registry() -> None:
879
+ """Ensure every public Markdown page is reachable through the styled viewer."""
880
+ viewer = (ROOT / "docs/viewer.js").read_text(encoding="utf-8")
881
+ expected_page_ids = [
882
+ path.replace("docs/", "")
883
+ for path in REQUIRED_FILES
884
+ if path.startswith("docs/") and path.endswith(".md")
885
+ ] + ["security.md", "contributing.md", "code-of-conduct.md"]
886
+ for page_id in expected_page_ids:
887
+ if f"id: '{page_id}'" not in viewer:
888
+ fail(f"docs/viewer.js: missing styled viewer page id: {page_id}")
889
+ for source_path in ("../SECURITY.md", "../CONTRIBUTING.md", "../CODE_OF_CONDUCT.md"):
890
+ if f"path: '{source_path}'" not in viewer:
891
+ fail(f"docs/viewer.js: missing community source path: {source_path}")
892
+ for marker in (
893
+ "const DOC_TIMEOUT_MS = 8_000;",
894
+ "const MAX_DOC_BYTES = 1_000_000;",
895
+ "async function fetchDocText(path)",
896
+ "new AbortController()",
897
+ "new TextEncoder().encode(markdown).byteLength",
898
+ "window.clearTimeout(timeout)",
899
+ "const markdown = await fetchDocText(page.path);",
900
+ ):
901
+ if marker not in viewer:
902
+ fail(f"docs/viewer.js: missing bounded document fetch marker: {marker}")
903
+
904
+
905
+ def validate_completion_contract() -> None:
906
+ """Keep the public completion claim measurable and discoverable."""
907
+ contract_relative = "docs/completion-contract.md"
908
+ checker_relative = "scripts/check_completion.py"
909
+ contract = (ROOT / contract_relative).read_text(encoding="utf-8")
910
+ checker = (ROOT / checker_relative).read_text(encoding="utf-8")
911
+ readme = (ROOT / "README.md").read_text(encoding="utf-8")
912
+ index = (ROOT / "index.html").read_text(encoding="utf-8")
913
+ viewer = (ROOT / "docs/viewer.js").read_text(encoding="utf-8")
914
+
915
+ for marker in (
916
+ "# Safe-scope completion contract",
917
+ "## Definition of complete",
918
+ "## Acceptance criteria",
919
+ "## Explicit non-goals",
920
+ "## Canonical verification",
921
+ "no private keys",
922
+ "no transaction broadcast on page load",
923
+ ):
924
+ if marker.lower() not in contract.lower():
925
+ fail(f"{contract_relative}: missing completion marker: {marker}")
926
+ for marker in (
927
+ "def check_required_surfaces",
928
+ "def check_canonical_suite",
929
+ "def check_safety_boundary",
930
+ "def main",
931
+ ):
932
+ if marker not in checker:
933
+ fail(f"{checker_relative}: missing completion check marker: {marker}")
934
+ for surface, text in (
935
+ ("README.md", readme),
936
+ ("index.html", index),
937
+ ("docs/viewer.js", viewer),
938
+ ):
939
+ if "completion-contract.md" not in text:
940
+ fail(f"{surface}: missing completion contract link")
941
+
942
+
943
+ def validate_demo_safety_copy() -> None:
944
+ relative = "examples/payment-intent-demo/index.html"
945
+ html = (ROOT / relative).read_text(encoding="utf-8").lower()
946
+ for marker in DEMO_SAFETY_MARKERS:
947
+ if marker not in html:
948
+ fail(f"{relative}: missing safety copy marker: {marker}")
949
+
950
+
951
+ def validate_public_launch_packet() -> None:
952
+ """Keep public distribution copy discoverable and non-overclaiming."""
953
+ doc_relative = "docs/public-launch-packet.md"
954
+ viewer_relative = "docs/viewer.js"
955
+ index_relative = "index.html"
956
+
957
+ doc = (ROOT / doc_relative).read_text(encoding="utf-8")
958
+ doc_lower = doc.lower()
959
+ viewer = (ROOT / viewer_relative).read_text(encoding="utf-8")
960
+ index = (ROOT / index_relative).read_text(encoding="utf-8")
961
+
962
+ for marker in (
963
+ "# Public launch packet",
964
+ "## Launch verdict",
965
+ "## Do not post automatically",
966
+ "## Russian Telegram draft",
967
+ "## X draft under 280 chars",
968
+ "## Discord / Arc House update",
969
+ "## Submission checklist",
970
+ "## Claims to avoid",
971
+ ):
972
+ if marker not in doc:
973
+ fail(f"{doc_relative}: missing launch packet marker: {marker}")
974
+ for marker in (
975
+ "do not post automatically",
976
+ "no wallet",
977
+ "no private keys",
978
+ "no custody",
979
+ "no transaction broadcast",
980
+ "not an official arc product",
981
+ "local-only",
982
+ "public-ready arc builder kit",
983
+ ):
984
+ if marker not in doc_lower:
985
+ fail(f"{doc_relative}: missing safety wording marker: {marker}")
986
+ if "public-launch-packet.md" not in viewer:
987
+ fail(f"{viewer_relative}: missing public launch packet route")
988
+ if "./docs/view.html#public-launch-packet.md" not in index:
989
+ fail(f"{index_relative}: missing public launch packet link")
990
+
991
+
992
+ def validate_arc_release_packet() -> None:
993
+ """Keep the local release packet generator and its test discoverable."""
994
+ for relative in (
995
+ "scripts/generate_arc_release_packet.py",
996
+ "scripts/test_arc_release_packet.py",
997
+ ):
998
+ if not (ROOT / relative).is_file():
999
+ fail(f"{relative}: missing release packet surface")
1000
+
1001
+ test_all = (ROOT / "scripts/test_all.py").read_text(encoding="utf-8")
1002
+ if "scripts/test_arc_release_packet.py" not in test_all:
1003
+ fail("scripts/test_all.py: missing release packet regression entry for scripts/test_arc_release_packet.py")
1004
+
1005
+ gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8")
1006
+ if ".arc-release-packet/" not in gitignore:
1007
+ fail(".gitignore: missing ignored output directory for release packet")
1008
+
1009
+
1010
+ def validate_x402_boundary_demo() -> None:
1011
+ """Keep the x402 example explicitly local-only and verifier-shaped."""
1012
+ server = (ROOT / "examples/x402-local-challenge-server/server.py").read_text(encoding="utf-8")
1013
+ readme = (ROOT / "examples/x402-local-challenge-server/README.md").read_text(encoding="utf-8").lower()
1014
+ required_server_markers = (
1015
+ "class PaymentVerifier(Protocol)",
1016
+ "class LocalDemoVerifier",
1017
+ "HTTPStatus.PAYMENT_REQUIRED",
1018
+ '"transactionBroadcast": False',
1019
+ '"mainnetEnabled": config.mainnet_enabled',
1020
+ 'DEFAULT_NETWORK = "arc-testnet"',
1021
+ 'DEFAULT_ASSET = "USDC"',
1022
+ "def from_env",
1023
+ "validate_payment_config",
1024
+ "human approval must remain required in this demo",
1025
+ "verifier mode must stay local-simulation in this demo",
1026
+ "request jsonrpc must be exactly 2.0",
1027
+ "request id must be a string, integer, or null",
1028
+ "request method must be a string",
1029
+ "validate_bind_target",
1030
+ "LOCAL_BIND_HOSTS",
1031
+ "X402_DEMO_MAINNET_ENABLED",
1032
+ "PaymentConfig.from_env()",
1033
+ "MAX_MCP_LINE_BYTES = 1_000_000",
1034
+ "MAX_PAYMENT_PROOF_BYTES = 4_096",
1035
+ "def require_exact_keys",
1036
+ "def validate_payment_proof",
1037
+ "def extract_payment_proof",
1038
+ "exactly one X-Payment header is required",
1039
+ "X-Payment proof exceeds the 4 KB safety limit",
1040
+ '"error": "payment_verifier_unavailable"',
1041
+ '"error": "invalid_verifier_result"',
1042
+ '"error": "unsafe_verifier_result"',
1043
+ "object_pairs_hook=reject_duplicate_json_keys",
1044
+ )
1045
+ for marker in required_server_markers:
1046
+ if marker not in server:
1047
+ fail(f"examples/x402-local-challenge-server/server.py: missing marker: {marker}")
1048
+ for marker in ("local-only", "never opens a wallet", "transactionbroadcast", "mainnetenabled"):
1049
+ if marker not in readme:
1050
+ fail(f"examples/x402-local-challenge-server/README.md: missing safety marker: {marker}")
1051
+
1052
+
1053
+ def validate_arc_production_deployment_assets() -> None:
1054
+ """Keep production-facing Arc/x402 docs secret-free and smoke-testable."""
1055
+ runbook_relative = "docs/arc-production-deployment.md"
1056
+ env_relative = "examples/x402-local-challenge-server/.env.example"
1057
+ smoke_relative = "scripts/live_arc_gateway_smoke.py"
1058
+ test_relative = "scripts/test_arc_production_deployment.py"
1059
+
1060
+ runbook = (ROOT / runbook_relative).read_text(encoding="utf-8").lower()
1061
+ env_example = (ROOT / env_relative).read_text(encoding="utf-8")
1062
+ smoke = (ROOT / smoke_relative).read_text(encoding="utf-8")
1063
+ tests = (ROOT / test_relative).read_text(encoding="utf-8")
1064
+
1065
+ for marker in (
1066
+ "arc_paid_agent_url",
1067
+ "arc_live_x_payment",
1068
+ "--expect-402-only",
1069
+ "circle gateway",
1070
+ "x402",
1071
+ "rollback",
1072
+ "human approval",
1073
+ "no private keys",
1074
+ "no seed phrases",
1075
+ "does not create payments",
1076
+ ):
1077
+ if marker not in runbook:
1078
+ fail(f"{runbook_relative}: missing production runbook marker: {marker}")
1079
+ for marker in (
1080
+ "ARC_PAID_AGENT_URL=",
1081
+ "ARC_LIVE_X_PAYMENT=",
1082
+ "CIRCLE_GATEWAY_API_KEY=",
1083
+ "X402_GATEWAY_VERIFIER_URL=",
1084
+ "EXPECT_402_ONLY=true",
1085
+ "Placeholder only",
1086
+ ):
1087
+ if marker not in env_example:
1088
+ fail(f"{env_relative}: missing placeholder env marker: {marker}")
1089
+ for marker in (
1090
+ "ARC_PAID_AGENT_URL",
1091
+ "ARC_LIVE_X_PAYMENT",
1092
+ "X-Payment",
1093
+ "NoRedirectHandler",
1094
+ "redirects are disabled for live smoke requests",
1095
+ "MAX_RESPONSE_BYTES",
1096
+ 'first.get("asset") != "USDC"',
1097
+ 'payload.get("mainnetEnabled") is not False',
1098
+ "--expect-402-only",
1099
+ "No payments were created",
1100
+ "transactionBroadcast",
1101
+ "humanApprovalRequired",
1102
+ "arc-testnet",
1103
+ ):
1104
+ if marker not in smoke:
1105
+ fail(f"{smoke_relative}: missing safe smoke marker: {marker}")
1106
+ for marker in (
1107
+ "test_live_smoke_fails_safely_without_target_url",
1108
+ "test_live_smoke_accepts_local_402_only_mode",
1109
+ "test_live_smoke_rejects_unsupported_url_scheme_without_proof",
1110
+ "test_live_smoke_rejects_url_credentials_and_invalid_timeout",
1111
+ "test_production_runbook_documents_safe_gateway_handoff",
1112
+ ):
1113
+ if marker not in tests:
1114
+ fail(f"{test_relative}: missing regression test marker: {marker}")
1115
+ for forbidden in ("local-demo:", "-----BEGIN", "sk-"):
1116
+ if forbidden in env_example:
1117
+ fail(f"{env_relative}: forbidden placeholder content: {forbidden}")
1118
+
1119
+
1120
+ def validate_arc_testnet_status_helper() -> None:
1121
+ """Keep the first Arc Testnet helper read-only and source-fact grounded."""
1122
+ relative = "scripts/check_arc_testnet_status.py"
1123
+ helper = (ROOT / relative).read_text(encoding="utf-8")
1124
+ required_markers = (
1125
+ "EXPECTED_CHAIN_ID_DECIMAL = 5042002",
1126
+ "EXPECTED_CHAIN_ID_HEX = hex(EXPECTED_CHAIN_ID_DECIMAL)",
1127
+ 'DEFAULT_RPC_URL = "https://rpc.testnet.arc.network"',
1128
+ 'DEFAULT_EXPLORER_URL = "https://testnet.arcscan.app"',
1129
+ '"nativeGasAsset": "USDC"',
1130
+ '"nativeGasDecimals": 18',
1131
+ '"erc20UsdcAddress": "0x3600000000000000000000000000000000000000"',
1132
+ '"erc20UsdcDecimals": 6',
1133
+ '"rpcChainIdMatchesArcTestnet": expected',
1134
+ '"signingRequiresWalletChainGateAndHumanApproval": True',
1135
+ "validate_endpoint",
1136
+ "validate_timeout",
1137
+ "MAX_RESPONSE_BYTES",
1138
+ "decode_json_object",
1139
+ )
1140
+ for marker in required_markers:
1141
+ if marker not in helper:
1142
+ fail(f"{relative}: missing Arc Testnet status marker: {marker}")
1143
+ forbidden_markers = ("PRIVATE_KEY", "seed phrase", "safeToUseForSigning")
1144
+ for marker in forbidden_markers:
1145
+ if marker in helper:
1146
+ fail(f"{relative}: forbidden wallet/signing marker: {marker}")
1147
+
1148
+
1149
+ def validate_payment_intent_playground_status_panel() -> None:
1150
+ """Keep the interactive playground status panel local-only and Arc-grounded."""
1151
+ html_relative = "examples/payment-intent-playground/index.html"
1152
+ js_relative = "examples/payment-intent-playground/playground.js"
1153
+ html = (ROOT / html_relative).read_text(encoding="utf-8")
1154
+ js = (ROOT / js_relative).read_text(encoding="utf-8")
1155
+ for marker in (
1156
+ 'id="arc-status-panel"',
1157
+ 'id="arc-chain-id"',
1158
+ 'id="arc-rpc-url"',
1159
+ 'id="arc-readonly-state"',
1160
+ "Arc Testnet status",
1161
+ "Read-only RPC probe",
1162
+ ):
1163
+ if marker not in html:
1164
+ fail(f"{html_relative}: missing Arc status panel marker: {marker}")
1165
+ for marker in (
1166
+ "const ARC_TESTNET_STATUS",
1167
+ "expectedChainIdDecimal: 5042002",
1168
+ "expectedChainIdHex: '0x4cef52'",
1169
+ "rpcUrl: 'https://rpc.testnet.arc.network'",
1170
+ "walletConnected: false",
1171
+ "transactionBroadcast: false",
1172
+ "signingRequiresWalletChainGateAndHumanApproval: true",
1173
+ "!/^0x0{40}$/.test(normalized)",
1174
+ "normalized !== ARC_TESTNET_STATUS.erc20UsdcAddress.toLowerCase()",
1175
+ "renderArcStatusPanel()",
1176
+ ):
1177
+ if marker not in js:
1178
+ fail(f"{js_relative}: missing Arc status panel marker: {marker}")
1179
+ for marker in ("fetch(", "XMLHttpRequest", "WebSocket", "ethereum.request", "sendTransaction", "signTransaction", "PRIVATE_KEY"):
1180
+ if marker in js:
1181
+ fail(f"{js_relative}: forbidden network/wallet marker: {marker}")
1182
+
1183
+
1184
+ def validate_receipt_verifier_playground() -> None:
1185
+ """Keep the receipt verifier playground static, local-only, and Arc-grounded."""
1186
+ html_relative = "examples/receipt-verifier-playground/index.html"
1187
+ js_relative = "examples/receipt-verifier-playground/verifier.js"
1188
+ html = (ROOT / html_relative).read_text(encoding="utf-8")
1189
+ js = (ROOT / js_relative).read_text(encoding="utf-8")
1190
+ for marker in (
1191
+ 'id="receipt-json"',
1192
+ 'id="verify-receipt"',
1193
+ 'id="verdict-pill"',
1194
+ 'id="receipt-check-list"',
1195
+ 'id="normalized-receipt"',
1196
+ "Receipt Verifier Playground",
1197
+ "No wallet connection",
1198
+ "No transaction broadcast",
1199
+ ):
1200
+ if marker not in html:
1201
+ fail(f"{html_relative}: missing receipt verifier marker: {marker}")
1202
+ for marker in (
1203
+ "const ARC_RECEIPT_EXPECTATIONS",
1204
+ "expectedChainId: 5042002",
1205
+ "expectedChainIdHex: '0x4cef52'",
1206
+ "asset: 'USDC'",
1207
+ "assetDecimals: 6",
1208
+ "function normalizeReceipt(rawReceipt)",
1209
+ "function verifyReceipt(receipt)",
1210
+ "function isValidAddress(value)",
1211
+ "!/^0x0{40}$/.test(normalized)",
1212
+ "normalized !== '0x3600000000000000000000000000000000000000'",
1213
+ "walletConnected: false",
1214
+ "backendCalls: false",
1215
+ "transactionBroadcast: false",
1216
+ "signingEnabled: false",
1217
+ "localOnly: true",
1218
+ ):
1219
+ if marker not in js:
1220
+ fail(f"{js_relative}: missing receipt verifier marker: {marker}")
1221
+ for marker in ("fetch(", "XMLHttpRequest", "WebSocket", "ethereum.request", "sendTransaction", "signTransaction", "PRIVATE_KEY", "seed phrase"):
1222
+ if marker in js:
1223
+ fail(f"{js_relative}: forbidden network/wallet marker: {marker}")
1224
+
1225
+
1226
+ def validate_payment_intent_receipt_matcher() -> None:
1227
+ """Keep the payment-intent receipt matcher pinned to Arc Testnet USDC and read-only."""
1228
+ html_relative = "examples/payment-intent-receipt-matcher/index.html"
1229
+ js_relative = "examples/payment-intent-receipt-matcher/matcher.js"
1230
+ test_relative = "scripts/test_payment_intent_receipt_matcher.py"
1231
+ harness_relative = "scripts/payment_intent_receipt_matcher_behavior_harness.mjs"
1232
+ html = (ROOT / html_relative).read_text(encoding="utf-8")
1233
+ js = (ROOT / js_relative).read_text(encoding="utf-8")
1234
+ tests = (ROOT / test_relative).read_text(encoding="utf-8")
1235
+ harness = (ROOT / harness_relative).read_text(encoding="utf-8")
1236
+
1237
+ # HTML structure and policy
1238
+ for marker in (
1239
+ "Payment Intent Receipt Matcher",
1240
+ 'id="payment-intent"',
1241
+ 'id="transaction-hash"',
1242
+ 'id="match-receipt"',
1243
+ 'id="reset-matcher"',
1244
+ 'id="status-pill"',
1245
+ 'id="match-summary-list"',
1246
+ 'id="transfer-log-list"',
1247
+ 'id="match-json"',
1248
+ "Read-only Arc Testnet RPC",
1249
+ "USDC Transfer logs",
1250
+ "No wallet connection",
1251
+ "No signing",
1252
+ "No transaction broadcast",
1253
+ "connect-src 'self' https://rpc.testnet.arc.network",
1254
+ 'crossorigin="anonymous"',
1255
+ ):
1256
+ if marker not in html:
1257
+ fail(f"{html_relative}: missing payment-intent receipt matcher marker: {marker}")
1258
+
1259
+ # JS pinning and read-only scope
1260
+ for marker in (
1261
+ "const ARC_MATCHER = Object.freeze",
1262
+ "expectedChainId: 5042002",
1263
+ "expectedChainIdHex: '0x4cef52'",
1264
+ "rpcUrl: 'https://rpc.testnet.arc.network'",
1265
+ "explorerUrl: 'https://testnet.arcscan.app'",
1266
+ "usdcAddress: '0x3600000000000000000000000000000000000000'",
1267
+ "usdcDecimals: 6",
1268
+ "const ZERO_ADDRESS",
1269
+ "function isNonZeroAddress",
1270
+ "function parseAmountBaseUnits",
1271
+ "function usdcBaseUnitsFromDecimal",
1272
+ "network !== 'Arc Testnet'",
1273
+ "'arc-testnet'",
1274
+ "chainId !== ARC_MATCHER.expectedChainId",
1275
+ "asset !== 'USDC'",
1276
+ "token !== ARC_MATCHER.usdcAddress.toLowerCase()",
1277
+ "decimals !== ARC_MATCHER.usdcDecimals",
1278
+ "recipient must include a valid non-zero 20-byte recipient address",
1279
+ "recipient must not be the USDC token contract",
1280
+ "amount and amountBaseUnits do not match",
1281
+ "fractionPart.length > ARC_MATCHER.usdcDecimals",
1282
+ "const RPC_TIMEOUT_MS = 15_000",
1283
+ "const MAX_RPC_RESPONSE_BYTES = 1_000_000",
1284
+ "const RPC_REQUEST_ID = 'arc-payment-intent-receipt-matcher-read-only'",
1285
+ "const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'",
1286
+ "method: 'eth_chainId'",
1287
+ "method: 'eth_getTransactionReceipt'",
1288
+ "new AbortController()",
1289
+ "new TextEncoder().encode(responseText).byteLength",
1290
+ "window.clearTimeout(timeout)",
1291
+ "Request timed out after 15 seconds.",
1292
+ "RPC response must be a JSON object",
1293
+ "RPC response envelope did not match the request",
1294
+ "RPC response must contain exactly one result or error field",
1295
+ "function decodeUsdcTransferLog(log)",
1296
+ "function extractUsdcTransferLogs(receipt)",
1297
+ "function classifyMatch",
1298
+ "function parseIntent",
1299
+ "invalid_local_input",
1300
+ "intent_receipt_match_observed",
1301
+ "intent_receipt_mismatch",
1302
+ "reverted_receipt_observed",
1303
+ "receipt_not_found",
1304
+ "unknown_wrong_chain",
1305
+ "unknown_hash_mismatch",
1306
+ "settlementProven: false",
1307
+ "businessAcceptanceProven: false",
1308
+ ):
1309
+ if marker not in js:
1310
+ fail(f"{js_relative}: missing payment-intent receipt matcher marker: {marker}")
1311
+
1312
+ if "eth_getTransactionByHash" in js:
1313
+ fail(f"{js_relative}: matcher must not fetch full transactions")
1314
+
1315
+ for marker in (
1316
+ "window.ethereum",
1317
+ "ethereum.request",
1318
+ "personal_sign",
1319
+ "eth_sendTransaction",
1320
+ "eth_sendRawTransaction",
1321
+ "wallet_switchEthereumChain",
1322
+ "sendTransaction",
1323
+ "signTransaction",
1324
+ "PRIVATE_KEY",
1325
+ "localStorage",
1326
+ "sessionStorage",
1327
+ ):
1328
+ if marker in html or marker in js:
1329
+ fail(f"{html_relative}/{js_relative}: forbidden payment-intent receipt matcher marker: {marker}")
1330
+
1331
+ # Regression tests cover the hardening
1332
+ for marker in (
1333
+ "test_payment_intent_receipt_matcher_script_tag_has_matching_sri",
1334
+ "test_payment_intent_receipt_matcher_js_pins_arc_testnet_chain",
1335
+ "test_payment_intent_receipt_matcher_js_pins_usdc_token",
1336
+ "test_payment_intent_receipt_matcher_js_enforces_six_decimals",
1337
+ "test_payment_intent_receipt_matcher_js_rejects_amount_precision_errors",
1338
+ "test_payment_intent_receipt_matcher_js_rejects_zero_address",
1339
+ "test_payment_intent_receipt_matcher_js_rejects_mismatched_amount_fields",
1340
+ "test_payment_intent_receipt_matcher_js_rejects_recipient_equal_to_usdc_contract",
1341
+ "test_payment_intent_receipt_matcher_harness_tests_invalid_intent_cases",
1342
+ "payment_intent_receipt_matcher_behavior_harness.mjs",
1343
+ ):
1344
+ if marker not in tests:
1345
+ fail(f"{test_relative}: missing payment-intent matcher regression marker: {marker}")
1346
+
1347
+ # Behavior harness exercises invalid local intent cases without RPC
1348
+ for marker in (
1349
+ "testInvalidLocalIntentAvoidsRpc",
1350
+ "wrong chainId",
1351
+ "wrong network",
1352
+ "wrong asset",
1353
+ "non-USDC token",
1354
+ "wrong decimals",
1355
+ "zero recipient",
1356
+ "recipient is USDC contract",
1357
+ "too many fractional digits",
1358
+ "zero amount",
1359
+ "negative amount",
1360
+ "hex amountBaseUnits",
1361
+ "mismatched amount/baseUnits",
1362
+ "invalid_local_input",
1363
+ ):
1364
+ if marker not in harness:
1365
+ fail(f"{harness_relative}: missing payment-intent matcher behavior marker: {marker}")
1366
+
1367
+
1368
+
1369
+ def validate_receipt_viewer() -> None:
1370
+ """Keep the receipt viewer read-only, receipt-scoped, and wallet-free."""
1371
+ html_relative = "examples/receipt-viewer/index.html"
1372
+ js_relative = "examples/receipt-viewer/receipt-viewer.js"
1373
+ test_relative = "scripts/test_receipt_viewer.py"
1374
+ harness_relative = "scripts/receipt_viewer_behavior_harness.mjs"
1375
+ html = (ROOT / html_relative).read_text(encoding="utf-8")
1376
+ js = (ROOT / js_relative).read_text(encoding="utf-8")
1377
+ tests = (ROOT / test_relative).read_text(encoding="utf-8")
1378
+ harness = (ROOT / harness_relative).read_text(encoding="utf-8")
1379
+ for marker in (
1380
+ "Agent Payment Receipt Viewer",
1381
+ 'id="transaction-hash"',
1382
+ 'id="load-receipt"',
1383
+ 'id="reset-receipt"',
1384
+ 'id="status-pill"',
1385
+ 'id="receipt-summary-list"',
1386
+ 'id="transfer-log-list"',
1387
+ 'id="receipt-json"',
1388
+ "Read-only Arc Testnet RPC",
1389
+ "USDC Transfer logs",
1390
+ "No wallet connection",
1391
+ "No transaction broadcast",
1392
+ "connect-src 'self' https://rpc.testnet.arc.network",
1393
+ 'crossorigin="anonymous"',
1394
+ ):
1395
+ if marker not in html:
1396
+ fail(f"{html_relative}: missing receipt viewer marker: {marker}")
1397
+ for marker in (
1398
+ "const ARC_RECEIPT_VIEWER = Object.freeze",
1399
+ "expectedChainId: 5042002",
1400
+ "expectedChainIdHex: '0x4cef52'",
1401
+ "rpcUrl: 'https://rpc.testnet.arc.network'",
1402
+ "explorerUrl: 'https://testnet.arcscan.app'",
1403
+ "usdcAddress: '0x3600000000000000000000000000000000000000'",
1404
+ "usdcDecimals: 6",
1405
+ "const RPC_TIMEOUT_MS = 15_000",
1406
+ "const MAX_RPC_RESPONSE_BYTES = 1_000_000",
1407
+ "const RPC_REQUEST_ID = 'arc-receipt-viewer-read-only'",
1408
+ "const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'",
1409
+ "method: 'eth_chainId'",
1410
+ "method: 'eth_getTransactionReceipt'",
1411
+ "new AbortController()",
1412
+ "new TextEncoder().encode(responseText).byteLength",
1413
+ "window.clearTimeout(timeout)",
1414
+ "Request timed out after 15 seconds.",
1415
+ "RPC response must be a JSON object",
1416
+ "RPC response envelope did not match the request",
1417
+ "RPC response must contain exactly one result or error field",
1418
+ "function decodeUsdcTransferLog(log)",
1419
+ "function extractUsdcTransferLogs(receipt)",
1420
+ "function classifyReceiptStatus(chainIdHex, receipt, expectedTransactionHash)",
1421
+ "success_receipt_observed",
1422
+ "reverted_receipt_observed",
1423
+ "receipt_not_found",
1424
+ "unknown_wrong_chain",
1425
+ "unknown_hash_mismatch",
1426
+ "settlementProven: false",
1427
+ "businessAcceptanceProven: false",
1428
+ ):
1429
+ if marker not in js:
1430
+ fail(f"{js_relative}: missing receipt viewer marker: {marker}")
1431
+ if "eth_getTransactionByHash" in js:
1432
+ fail(f"{js_relative}: receipt viewer must not fetch full transactions")
1433
+ for marker in (
1434
+ "window.ethereum",
1435
+ "ethereum.request",
1436
+ "personal_sign",
1437
+ "eth_sendTransaction",
1438
+ "eth_sendRawTransaction",
1439
+ "wallet_switchEthereumChain",
1440
+ "sendTransaction",
1441
+ "signTransaction",
1442
+ "PRIVATE_KEY",
1443
+ "localStorage",
1444
+ "sessionStorage",
1445
+ ):
1446
+ if marker in html or marker in js:
1447
+ fail(f"{html_relative}/{js_relative}: forbidden receipt viewer marker: {marker}")
1448
+ for marker in (
1449
+ "test_receipt_viewer_page_has_read_only_receipt_ui",
1450
+ "test_receipt_viewer_script_tag_has_matching_sri",
1451
+ "test_receipt_viewer_js_uses_receipt_only_read_only_rpc",
1452
+ "test_receipt_viewer_forbids_wallet_signing_storage_or_broadcast_surface",
1453
+ "test_actual_receipt_viewer_javascript_behavior",
1454
+ "receipt_viewer_behavior_harness.mjs",
1455
+ ):
1456
+ if marker not in tests:
1457
+ fail(f"{test_relative}: missing receipt viewer regression marker: {marker}")
1458
+ for marker in (
1459
+ "testSuccessfulReceiptHighlightsUsdcTransfer",
1460
+ "testRevertedReceiptAndNullReceipt",
1461
+ "testWrongChainStopsBeforeReceipt",
1462
+ "testInvalidHashAvoidsRpc",
1463
+ "testRpcEnvelopeAndHashBindingFailClosed",
1464
+ "testTimeoutFailsClosed",
1465
+ "success_receipt_observed",
1466
+ "reverted_receipt_observed",
1467
+ "unknown_wrong_chain",
1468
+ "unknown_hash_mismatch",
1469
+ ):
1470
+ if marker not in harness:
1471
+ fail(f"{harness_relative}: missing receipt viewer behavior marker: {marker}")
1472
+
1473
+
1474
+
1475
+ def validate_transaction_status_playground() -> None:
1476
+ """Keep the transaction status playground read-only and wallet-free."""
1477
+ html_relative = "examples/transaction-status-playground/index.html"
1478
+ js_relative = "examples/transaction-status-playground/status.js"
1479
+ html = (ROOT / html_relative).read_text(encoding="utf-8")
1480
+ js = (ROOT / js_relative).read_text(encoding="utf-8")
1481
+ for marker in (
1482
+ 'id="transaction-hash"',
1483
+ 'id="expected-recipient"',
1484
+ 'id="expected-amount"',
1485
+ 'id="check-transaction"',
1486
+ 'id="status-pill"',
1487
+ 'id="status-check-list"',
1488
+ 'id="transaction-status-json"',
1489
+ "Transaction Status Playground",
1490
+ "Read-only Arc Testnet RPC",
1491
+ "No wallet connection",
1492
+ "No transaction broadcast",
1493
+ "connect-src 'self' https://rpc.testnet.arc.network",
1494
+ ):
1495
+ if marker not in html:
1496
+ fail(f"{html_relative}: missing transaction status marker: {marker}")
1497
+ for marker in (
1498
+ "const ARC_TRANSACTION_STATUS = Object.freeze",
1499
+ "expectedChainId: 5042002",
1500
+ "expectedChainIdHex: '0x4cef52'",
1501
+ "rpcUrl: 'https://rpc.testnet.arc.network'",
1502
+ "explorerUrl: 'https://testnet.arcscan.app'",
1503
+ "usdcAddress: '0x3600000000000000000000000000000000000000'",
1504
+ "usdcDecimals: 6",
1505
+ "const TRANSFER_SELECTOR = 'a9059cbb'",
1506
+ "method: 'eth_chainId'",
1507
+ "method: 'eth_getTransactionByHash'",
1508
+ "method: 'eth_getTransactionReceipt'",
1509
+ "const RPC_TIMEOUT_MS = 10_000",
1510
+ "const MAX_RPC_RESPONSE_BYTES = 1_000_000",
1511
+ "const RPC_REQUEST_ID = 'arc-transaction-status-read-only'",
1512
+ "new AbortController()",
1513
+ "new TextEncoder().encode(responseText).byteLength",
1514
+ "window.clearTimeout(timeout)",
1515
+ "RPC response must be a JSON object",
1516
+ "RPC response envelope did not match the request",
1517
+ "RPC response must contain exactly one result or error field",
1518
+ "function hashMatchesExpected(value, expectedHash)",
1519
+ "rpcObjectHashesMatch",
1520
+ "unknown_hash_mismatch",
1521
+ "readOnlyRpcCheckOnly: true",
1522
+ "transactionBroadcast: false",
1523
+ "autonomousSpending: false",
1524
+ "humanApprovalRequired: true",
1525
+ "signingRequiresWalletChainGateAndHumanApproval: true",
1526
+ "function buildExpectedTransfer()",
1527
+ "function decodeTransferCalldata(data)",
1528
+ "function reviewExpectedTransfer(transaction, expectedTransfer)",
1529
+ "function withTransferEvidence(result, transaction, expectedTransfer)",
1530
+ "function classifyTransactionStatus(chainIdHex, transaction, receipt, expectedTransfer, expectedTransactionHash)",
1531
+ "evidenceVerdict",
1532
+ "settlementProven: false",
1533
+ "businessAcceptanceProven: false",
1534
+ "state: 'not_checked'",
1535
+ "state: 'pending'",
1536
+ "state: 'confirmed'",
1537
+ "state: 'failed'",
1538
+ "state: 'unknown'",
1539
+ ):
1540
+ if marker not in js:
1541
+ fail(f"{js_relative}: missing transaction status marker: {marker}")
1542
+ for marker in ("window.ethereum", "personal_sign", "eth_sendTransaction", "wallet_switchEthereumChain", "signTransaction", "PRIVATE_KEY", "localStorage"):
1543
+ if marker in js:
1544
+ fail(f"{js_relative}: forbidden wallet/signing marker: {marker}")
1545
+ behavior_test = (ROOT / "scripts/test_transaction_status_behavior.py").read_text(encoding="utf-8")
1546
+ behavior_harness = (ROOT / "scripts/transaction_status_behavior_harness.mjs").read_text(encoding="utf-8")
1547
+ for marker in ("shutil.which(\"node\")", "transaction_status_behavior_harness.mjs", "timeout=30"):
1548
+ if marker not in behavior_test:
1549
+ fail(f"scripts/test_transaction_status_behavior.py: missing behavior test marker: {marker}")
1550
+ for marker in (
1551
+ "testConfirmedExpectedTransferShape",
1552
+ "testMismatchAndWrongChainFailClosed",
1553
+ "testInvalidExpectedFieldsAvoidRpc",
1554
+ "testRpcEnvelopeAndHashBindingFailClosed",
1555
+ "confirmed_expected_transfer_shape",
1556
+ "mismatch_expected_transfer",
1557
+ "unknown_wrong_chain",
1558
+ "unknown_hash_mismatch",
1559
+ ):
1560
+ if marker not in behavior_harness:
1561
+ fail(f"scripts/transaction_status_behavior_harness.mjs: missing behavior marker: {marker}")
1562
+
1563
+
1564
+ def validate_guarded_wallet_send_gate() -> None:
1565
+ """Keep the only write-capable browser surface narrow and fail-closed."""
1566
+ html_relative = "examples/arc-testnet-wallet-send-gate/index.html"
1567
+ js_relative = "examples/arc-testnet-wallet-send-gate/wallet-send-gate.js"
1568
+ runbook_relative = "docs/guarded-wallet-send-runbook.md"
1569
+ gates_relative = "docs/custody-and-mainnet-gates.md"
1570
+ policy_relative = "examples/arc-testnet-wallet-send-gate/live-infrastructure-policy.example.json"
1571
+ html = (ROOT / html_relative).read_text(encoding="utf-8")
1572
+ js = (ROOT / js_relative).read_text(encoding="utf-8")
1573
+ runbook = (ROOT / runbook_relative).read_text(encoding="utf-8").lower()
1574
+ gates = (ROOT / gates_relative).read_text(encoding="utf-8").lower()
1575
+ policy = (ROOT / policy_relative).read_text(encoding="utf-8")
1576
+
1577
+ for marker in (
1578
+ "Arc Testnet Wallet Send Gate",
1579
+ 'id="risk-acknowledgement"',
1580
+ 'id="connect-wallet"',
1581
+ 'id="switch-network"',
1582
+ 'id="freeze-intent"',
1583
+ 'id="send-transaction"',
1584
+ 'id="confirmation-phrase"',
1585
+ 'id="final-send-confirmation"',
1586
+ "Disabled by default",
1587
+ "Arc Testnet only",
1588
+ "One attempt per page load",
1589
+ "No custody",
1590
+ "No private keys",
1591
+ ):
1592
+ if marker not in html:
1593
+ fail(f"{html_relative}: missing guarded send marker: {marker}")
1594
+ for marker in (
1595
+ "const ARC_TESTNET = Object.freeze",
1596
+ "chainId: 5042002",
1597
+ "chainIdHex: '0x4cef52'",
1598
+ "rpcUrl: 'https://rpc.testnet.arc.network'",
1599
+ "explorerUrl: 'https://testnet.arcscan.app'",
1600
+ "usdcAddress: '0x3600000000000000000000000000000000000000'",
1601
+ "usdcDecimals: 6",
1602
+ "maxAmountBaseUnits: 1000000n",
1603
+ "enableArcTestnetSend",
1604
+ "reviewed-testnet-only",
1605
+ "function parseUsdcAmount",
1606
+ "function encodeTransferCalldata",
1607
+ "function decodeTransferCalldata",
1608
+ "function buildGuardReport",
1609
+ "function canAttemptSend",
1610
+ "const ALLOWED_WALLET_METHODS = new Set",
1611
+ "if (!ALLOWED_WALLET_METHODS.has(request.method))",
1612
+ "topLevelContext: window.top === window.self",
1613
+ "['top-level-context', state.topLevelContext",
1614
+ "function isNonZeroAddress",
1615
+ "Recipient cannot be the pinned USDC token contract address.",
1616
+ "const failedPrerequisite = report.checks.find",
1617
+ "throw new Error(failedPrerequisite.detail",
1618
+ "Risk acknowledgement cleared. Freeze and review the intent again.",
1619
+ "sendAttempted = true",
1620
+ "method: 'eth_requestAccounts'",
1621
+ "method: 'wallet_switchEthereumChain'",
1622
+ "method: 'wallet_addEthereumChain'",
1623
+ "method: 'eth_sendTransaction'",
1624
+ "No automatic retry",
1625
+ ):
1626
+ if marker not in js:
1627
+ fail(f"{js_relative}: missing guarded send marker: {marker}")
1628
+ for forbidden in (
1629
+ "personal_sign",
1630
+ "eth_sign",
1631
+ "signTransaction",
1632
+ "eth_sendRawTransaction",
1633
+ "PRIVATE_KEY",
1634
+ "seed phrase",
1635
+ "localStorage",
1636
+ "sessionStorage",
1637
+ "fetch(",
1638
+ "XMLHttpRequest",
1639
+ "WebSocket",
1640
+ "mainnet",
1641
+ ):
1642
+ if forbidden in js:
1643
+ fail(f"{js_relative}: forbidden guarded send marker: {forbidden}")
1644
+ for marker in (
1645
+ "injected user-controlled browser wallet",
1646
+ "wallet confirmation dialog is the only signing path",
1647
+ "no automatic retry",
1648
+ "one attempt per page load",
1649
+ "arc testnet only",
1650
+ "rollback",
1651
+ ):
1652
+ if marker not in runbook:
1653
+ fail(f"{runbook_relative}: missing guarded runbook marker: {marker}")
1654
+ for marker in (
1655
+ "non-custodial",
1656
+ "static site",
1657
+ "secret manager",
1658
+ "mainnet remains blocked",
1659
+ "upcoming",
1660
+ "separate security review",
1661
+ "no fake mainnet constants",
1662
+ ):
1663
+ if marker not in gates:
1664
+ fail(f"{gates_relative}: missing custody/mainnet gate marker: {marker}")
1665
+ for marker in (
1666
+ '"activeProfile": "arc-testnet-injected-wallet"',
1667
+ '"enabled": false',
1668
+ '"status": "blocked_official_configuration_upcoming"',
1669
+ '"implemented": false',
1670
+ '"mode": "non-custodial"',
1671
+ '"staticSiteMayHoldSecrets": false',
1672
+ '"maxAttemptsPerPageLoad": 1',
1673
+ '"transactionChainIdRequired": "0x4cef52"',
1674
+ '"topLevelBrowsingContextRequired": true',
1675
+ '"zeroAddressAllowed": false',
1676
+ '"tokenContractRecipientAllowed": false',
1677
+ '"automaticRetry": false',
1678
+ ):
1679
+ if marker not in policy:
1680
+ fail(f"{policy_relative}: missing fail-closed policy marker: {marker}")
1681
+ policy_validator = (ROOT / "scripts/validate_live_infrastructure_policy.py").read_text(encoding="utf-8")
1682
+ for marker in ("require_exact_keys", "reject_duplicate_keys", "object_pairs_hook=reject_duplicate_keys"):
1683
+ if marker not in policy_validator:
1684
+ fail(f"scripts/validate_live_infrastructure_policy.py: missing strict policy marker: {marker}")
1685
+ behavior_test = (ROOT / "scripts/test_arc_testnet_wallet_send_behavior.py").read_text(encoding="utf-8")
1686
+ behavior_harness = (ROOT / "scripts/wallet_send_behavior_harness.mjs").read_text(encoding="utf-8")
1687
+ for marker in ("shutil.which(\"node\")", "wallet_send_behavior_harness.mjs", "timeout=30"):
1688
+ if marker not in behavior_test:
1689
+ fail(f"scripts/test_arc_testnet_wallet_send_behavior.py: missing behavior test marker: {marker}")
1690
+ for marker in (
1691
+ "vm.runInContext(SOURCE",
1692
+ "testDefaultDisabled",
1693
+ "testExactOneShotSend",
1694
+ "testWrongChainAndAccountChangeBlock",
1695
+ "testRejectionKeepsOneShotLock",
1696
+ "eth_sendTransaction",
1697
+ "EXPECTED_DATA",
1698
+ ):
1699
+ if marker not in behavior_harness:
1700
+ fail(f"scripts/wallet_send_behavior_harness.mjs: missing fake-provider marker: {marker}")
1701
+
1702
+
1703
+ def validate_job_escrow_simulator() -> None:
1704
+ """Keep the job escrow simulator local-only and review-gated."""
1705
+ html_relative = "examples/job-escrow-simulator/index.html"
1706
+ js_relative = "examples/job-escrow-simulator/simulator.js"
1707
+ test_relative = "scripts/test_job_escrow_simulator.py"
1708
+ html = (ROOT / html_relative).read_text(encoding="utf-8")
1709
+ js = (ROOT / js_relative).read_text(encoding="utf-8")
1710
+ tests = (ROOT / test_relative).read_text(encoding="utf-8")
1711
+ for marker in (
1712
+ 'id="request-changes"',
1713
+ 'id="revise-work"',
1714
+ 'id="reject-work"',
1715
+ 'id="open-dispute"',
1716
+ 'id="expire-job"',
1717
+ 'id="cancel-job"',
1718
+ 'id="revision-note"',
1719
+ 'id="dispute-note"',
1720
+ "Request changes",
1721
+ "Revise work",
1722
+ "Reject no payout",
1723
+ "Open dispute",
1724
+ "Expire job",
1725
+ "Cancel job",
1726
+ "changes and disputes are simulated review notes",
1727
+ "terminal local review states with no payout release",
1728
+ ):
1729
+ if marker not in html:
1730
+ fail(f"{html_relative}: missing job escrow review marker: {marker}")
1731
+ for marker in (
1732
+ "changes_requested",
1733
+ "changesRequestedCount",
1734
+ "latestRevisionNote",
1735
+ "latestCloseNote",
1736
+ "terminalNoPayoutStates",
1737
+ "rejected_no_payout",
1738
+ "disputed_manual_review",
1739
+ "expired_no_payout",
1740
+ "cancelled_no_payout",
1741
+ "payoutReleased: status === 'payout_approved_simulation' ? 'simulated_only' : false",
1742
+ "contactsArbitrator: false",
1743
+ "contactsValidator: false",
1744
+ "payoutRelease: 'simulated_only_after_human_approval'",
1745
+ "arcTestnetChainId: 5042002",
1746
+ "arcTestnetChainIdHex: '0x4cef52'",
1747
+ "walletActionEnabled: false",
1748
+ "signingEnabled: false",
1749
+ "transactionBroadcast: false",
1750
+ "localOnly: true",
1751
+ "realEscrowContract: false",
1752
+ "buttons.requestChanges.disabled = status !== 'work_submitted'",
1753
+ "buttons.revise.disabled = status !== 'changes_requested'",
1754
+ "buttons.reject.disabled = status !== 'work_submitted'",
1755
+ "buttons.dispute.disabled = !['work_submitted', 'changes_requested'].includes(status)",
1756
+ "buttons.expire.disabled = !['posted', 'accepted_by_agent', 'escrow_funded_simulation', 'changes_requested'].includes(status)",
1757
+ "buttons.cancel.disabled = !['draft', 'posted', 'accepted_by_agent'].includes(status)",
1758
+ ):
1759
+ if marker not in js:
1760
+ fail(f"{js_relative}: missing job escrow safety marker: {marker}")
1761
+ for marker in (
1762
+ "test_job_escrow_review_loop_controls_are_present",
1763
+ "test_job_escrow_json_exposes_review_and_arc_safety_flags",
1764
+ "test_job_escrow_state_machine_allows_revisions_before_payout",
1765
+ "test_job_escrow_state_machine_allows_terminal_no_payout_paths",
1766
+ "test_job_escrow_simulator_forbids_wallet_network_and_secret_surface",
1767
+ ):
1768
+ if marker not in tests:
1769
+ fail(f"{test_relative}: missing job escrow regression marker: {marker}")
1770
+ for marker in ("fetch(", "XMLHttpRequest", "WebSocket", "window.ethereum", "ethereum.request", "eth_sendTransaction", "wallet_switchEthereumChain", "sendTransaction", "signTransaction", "PRIVATE_KEY", "localStorage"):
1771
+ if marker in html or marker in js:
1772
+ fail(f"{html_relative}/{js_relative}: forbidden network/wallet marker: {marker}")
1773
+
1774
+
1775
+ def validate_arc_agent_treasury_lab() -> None:
1776
+ """Keep the local self-funding-agent product exact, bounded, and wallet-free."""
1777
+ html_relative = "examples/arc-agent-treasury-lab/index.html"
1778
+ js_relative = "examples/arc-agent-treasury-lab/treasury.js"
1779
+ test_relative = "scripts/test_arc_agent_treasury_lab.py"
1780
+ harness_relative = "scripts/arc_agent_treasury_behavior_harness.mjs"
1781
+ html = (ROOT / html_relative).read_text(encoding="utf-8")
1782
+ js = (ROOT / js_relative).read_text(encoding="utf-8")
1783
+ tests = (ROOT / test_relative).read_text(encoding="utf-8")
1784
+ harness = (ROOT / harness_relative).read_text(encoding="utf-8")
1785
+ for marker in (
1786
+ "Arc Agent Treasury Lab",
1787
+ 'id="review-task"',
1788
+ 'id="run-loop"',
1789
+ 'id="ledger"',
1790
+ 'id="snapshot"',
1791
+ "No wallet, custody, mainnet, backend, signing, settlement, or transaction broadcast.",
1792
+ ):
1793
+ if marker not in html:
1794
+ fail(f"{html_relative}: missing treasury product marker: {marker}")
1795
+ for marker in (
1796
+ "const MICRO_USDC = 1_000_000",
1797
+ "request_replay_detected",
1798
+ "receipt_replay_detected",
1799
+ "single_task_cap_exceeded",
1800
+ "daily_spend_cap_exceeded",
1801
+ "protected_reserve_would_be_breached",
1802
+ "minimum_profit_not_met",
1803
+ "Runtime spend preflight failed closed",
1804
+ "settled: false",
1805
+ "transactionBroadcast: false",
1806
+ "autonomousSpendingEnabled: false",
1807
+ "mainnetEnabled: false",
1808
+ "custodyEnabled: false",
1809
+ ):
1810
+ if marker not in js:
1811
+ fail(f"{js_relative}: missing treasury safety marker: {marker}")
1812
+ for marker in (
1813
+ "test_product_surface_is_complete",
1814
+ "test_domain_exposes_fail_closed_policy_and_loop",
1815
+ "test_local_lab_forbids_wallet_network_storage_and_secrets",
1816
+ "test_actual_javascript_behavior",
1817
+ ):
1818
+ if marker not in tests:
1819
+ fail(f"{test_relative}: missing treasury regression marker: {marker}")
1820
+ for marker in (
1821
+ "exact micro-USDC",
1822
+ "request replay reason missing",
1823
+ "single-task cap must fail closed",
1824
+ "reserve breach must fail closed",
1825
+ "runtime policy drift must fail closed before spend",
1826
+ ):
1827
+ if marker not in harness:
1828
+ fail(f"{harness_relative}: missing treasury behavior marker: {marker}")
1829
+ for marker in ("fetch(", "XMLHttpRequest", "WebSocket", "window.ethereum", "ethereum.request", "eth_sendTransaction", "eth_sendRawTransaction", "personal_sign", "signTypedData", "PRIVATE_KEY", "localStorage", "sessionStorage"):
1830
+ if marker in html or marker in js:
1831
+ fail(f"{html_relative}/{js_relative}: forbidden treasury network/wallet marker: {marker}")
1832
+
1833
+
1834
+ def validate_agentic_maintainer_loop() -> None:
1835
+ """Keep the maintainer-agent loop discoverable and safety-anchored."""
1836
+ doc_relative = "docs/agentic-maintainer-loop.md"
1837
+ readme = (ROOT / "README.md").read_text(encoding="utf-8")
1838
+ index = (ROOT / "index.html").read_text(encoding="utf-8")
1839
+ viewer = (ROOT / "docs/viewer.js").read_text(encoding="utf-8")
1840
+ doc = (ROOT / doc_relative).read_text(encoding="utf-8")
1841
+
1842
+ for marker in (
1843
+ "Loop 1: task execution",
1844
+ "Loop 2: verification",
1845
+ "Loop 3: event-driven maintenance",
1846
+ "Loop 4: improvement",
1847
+ "Human approval gates",
1848
+ "no custody",
1849
+ "no mainnet",
1850
+ "no private keys",
1851
+ "no signing",
1852
+ "no broadcast",
1853
+ ):
1854
+ if marker not in doc:
1855
+ fail(f"{doc_relative}: missing maintainer loop marker: {marker}")
1856
+ for surface, text in (
1857
+ ("README.md", readme),
1858
+ ("index.html", index),
1859
+ ("docs/viewer.js", viewer),
1860
+ ):
1861
+ if "agentic-maintainer-loop.md" not in text:
1862
+ fail(f"{surface}: missing agentic maintainer loop link")
1863
+ for marker in ("ethereum.request", "eth_sendTransaction", "wallet_switchEthereumChain", "sendTransaction", "signTransaction", "PRIVATE_KEY"):
1864
+ if marker in index:
1865
+ fail(f"index.html: forbidden maintainer loop live-wallet marker: {marker}")
1866
+
1867
+
1868
+ def validate_arc_testnet_send_readiness_gate() -> None:
1869
+ """Keep the Arc Testnet send handoff narrow, explicit, and guard-first."""
1870
+ doc_relative = "docs/arc-testnet-send-readiness-gate.md"
1871
+ readme = (ROOT / "README.md").read_text(encoding="utf-8")
1872
+ index = (ROOT / "index.html").read_text(encoding="utf-8")
1873
+ viewer = (ROOT / "docs/viewer.js").read_text(encoding="utf-8")
1874
+ doc = (ROOT / doc_relative).read_text(encoding="utf-8")
1875
+
1876
+ for marker in (
1877
+ "Arc Testnet Send Readiness Gate",
1878
+ "5042002",
1879
+ "0x4cef52",
1880
+ "unsigned transaction draft",
1881
+ "disabled by default",
1882
+ "eth_sendTransaction",
1883
+ "external wallet confirmation dialog is the only signing path",
1884
+ "top-level browsing context",
1885
+ "Zero addresses",
1886
+ "pinned USDC token contract",
1887
+ "one-attempt lock",
1888
+ "No private keys",
1889
+ "No mainnet profile",
1890
+ "Rollback criteria",
1891
+ ):
1892
+ if marker not in doc:
1893
+ fail(f"{doc_relative}: missing send readiness marker: {marker}")
1894
+ for surface, text in (
1895
+ ("README.md", readme),
1896
+ ("index.html", index),
1897
+ ("docs/viewer.js", viewer),
1898
+ ):
1899
+ if "arc-testnet-send-readiness-gate.md" not in text:
1900
+ fail(f"{surface}: missing Arc Testnet send readiness gate link")
1901
+ for marker in ("ethereum.request", "eth_sendTransaction", "wallet_switchEthereumChain", "sendTransaction", "signTransaction", "PRIVATE_KEY"):
1902
+ if marker in index:
1903
+ fail(f"index.html: forbidden send readiness live-wallet marker: {marker}")
1904
+
1905
+
1906
+ def validate_arc_testnet_operator_runbook() -> None:
1907
+ """Keep the operator handoff manual, Arc-only, and fail-closed."""
1908
+ doc_relative = "docs/arc-testnet-operator-runbook.md"
1909
+ readme = (ROOT / "README.md").read_text(encoding="utf-8")
1910
+ index = (ROOT / "index.html").read_text(encoding="utf-8")
1911
+ viewer = (ROOT / "docs/viewer.js").read_text(encoding="utf-8")
1912
+ doc = (ROOT / doc_relative).read_text(encoding="utf-8")
1913
+
1914
+ for marker in (
1915
+ "Arc Testnet Operator Runbook",
1916
+ "5042002",
1917
+ "0x4cef52",
1918
+ "manual review",
1919
+ "separate guarded Arc Testnet send lab",
1920
+ "no private keys",
1921
+ "no custody",
1922
+ "no mainnet",
1923
+ "no automatic retry",
1924
+ "no transaction request on page load",
1925
+ "top-level tab",
1926
+ "pinned-token-contract recipients fail closed",
1927
+ ):
1928
+ if marker not in doc:
1929
+ fail(f"{doc_relative}: missing operator runbook marker: {marker}")
1930
+ for surface, text in (
1931
+ ("README.md", readme),
1932
+ ("index.html", index),
1933
+ ("docs/viewer.js", viewer),
1934
+ ):
1935
+ if "arc-testnet-operator-runbook.md" not in text:
1936
+ fail(f"{surface}: missing Arc Testnet operator runbook link")
1937
+ for marker in ("ethereum.request", "eth_sendTransaction", "wallet_switchEthereumChain", "sendTransaction", "signTransaction", "PRIVATE_KEY"):
1938
+ if marker in index:
1939
+ fail(f"index.html: forbidden operator runbook live-wallet marker: {marker}")
1940
+
1941
+
1942
+ def validate_arc_testnet_operator_evidence() -> None:
1943
+ """Keep the operator evidence packet strict, discoverable, and fail-closed."""
1944
+ doc_relative = "docs/arc-testnet-operator-evidence.md"
1945
+ example_relative = "examples/arc-testnet-operator-evidence/evidence.example.json"
1946
+ validator_relative = "scripts/validate_operator_evidence.py"
1947
+ test_relative = "scripts/test_operator_evidence.py"
1948
+ generator_relative = "scripts/generate_operator_evidence_draft.py"
1949
+ draft_test_relative = "scripts/test_operator_evidence_draft.py"
1950
+ reporter_relative = "scripts/report_operator_evidence.py"
1951
+ report_test_relative = "scripts/test_operator_evidence_report.py"
1952
+ readme = (ROOT / "README.md").read_text(encoding="utf-8")
1953
+ index = (ROOT / "index.html").read_text(encoding="utf-8")
1954
+ viewer = (ROOT / "docs/viewer.js").read_text(encoding="utf-8")
1955
+ doc = (ROOT / doc_relative).read_text(encoding="utf-8")
1956
+ example = (ROOT / example_relative).read_text(encoding="utf-8")
1957
+ validator = (ROOT / validator_relative).read_text(encoding="utf-8")
1958
+ tests = (ROOT / test_relative).read_text(encoding="utf-8")
1959
+ generator = (ROOT / generator_relative).read_text(encoding="utf-8")
1960
+ draft_tests = (ROOT / draft_test_relative).read_text(encoding="utf-8")
1961
+ reporter = (ROOT / reporter_relative).read_text(encoding="utf-8")
1962
+ report_tests = (ROOT / report_test_relative).read_text(encoding="utf-8")
1963
+ gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8")
1964
+ test_all = (ROOT / "scripts/test_all.py").read_text(encoding="utf-8")
1965
+
1966
+ for marker in (
1967
+ "Arc Testnet Operator Evidence Packet",
1968
+ "arc-mcp-builder-assistant.arcTestnet.operatorEvidence.v1",
1969
+ "5042002",
1970
+ "0x4cef52",
1971
+ "blocked_pending_separate_guarded_pr",
1972
+ "pre_send_readiness_baseline",
1973
+ "eth_sendTransaction",
1974
+ "separate guarded PR",
1975
+ "python scripts/validate_operator_evidence.py",
1976
+ "--expect-commit",
1977
+ "generate_operator_evidence_draft.py",
1978
+ "report_operator_evidence.py",
1979
+ ):
1980
+ if marker not in doc:
1981
+ fail(f"{doc_relative}: missing operator evidence marker: {marker}")
1982
+ for marker in (
1983
+ '"schema": "arc-mcp-builder-assistant.arcTestnet.operatorEvidence.v1"',
1984
+ '"chainId": 5042002',
1985
+ '"chainIdHex": "0x4cef52"',
1986
+ '"reviewedSurface": "pre_send_readiness_baseline"',
1987
+ '"transactionBroadcast": false',
1988
+ '"ethSendTransactionForbidden": true',
1989
+ '"separateGuardedPrRequired": true',
1990
+ '"status": "blocked_pending_separate_guarded_pr"',
1991
+ ):
1992
+ if marker not in example:
1993
+ fail(f"{example_relative}: missing safe evidence marker: {marker}")
1994
+ for marker in (
1995
+ "require_exact_keys",
1996
+ "validate_references",
1997
+ "validate_expected_commit",
1998
+ "SECRET_VALUE_PATTERNS",
1999
+ "MAX_PACKET_BYTES",
2000
+ "reject_duplicate_keys",
2001
+ "controls.{field} must be false",
2002
+ "decision.status must be blocked_pending_separate_guarded_pr",
2003
+ ):
2004
+ if marker not in validator:
2005
+ fail(f"{validator_relative}: missing fail-closed validator marker: {marker}")
2006
+ for marker in (
2007
+ "test_wrong_chain_fails_closed",
2008
+ "test_placeholder_commit_fails_closed",
2009
+ "test_non_string_commit_fails_closed",
2010
+ "test_broadcast_enabled_fails_closed",
2011
+ "test_unknown_field_fails_closed",
2012
+ "test_missing_evidence_fails_closed",
2013
+ "test_decision_cannot_approve_live_send",
2014
+ "test_non_repository_reference_fails_closed",
2015
+ "test_duplicate_reference_fails_closed",
2016
+ "test_credential_like_value_fails_closed",
2017
+ "test_cli_missing_packet_has_clear_error",
2018
+ "test_cli_accepts_matching_expected_commit",
2019
+ "test_cli_rejects_mismatched_expected_commit",
2020
+ "test_cli_rejects_malformed_expected_commit",
2021
+ ):
2022
+ if marker not in tests:
2023
+ fail(f"{test_relative}: missing evidence regression marker: {marker}")
2024
+ for marker in (
2025
+ 'resolved.open("x"',
2026
+ "resolved_relative",
2027
+ "strictValidationReady",
2028
+ "existingFileOverwritten",
2029
+ "transactionBroadcast",
2030
+ "draft_operator_evidence",
2031
+ "manualSecretReviewComplete",
2032
+ "blocked_pending_separate_guarded_pr",
2033
+ "LOCAL_DRAFT_SUFFIX",
2034
+ ):
2035
+ if marker not in generator:
2036
+ fail(f"{generator_relative}: missing safe draft generator marker: {marker}")
2037
+ for marker in (
2038
+ "test_draft_intentionally_fails_strict_validation",
2039
+ "test_cli_creates_ignored_local_draft",
2040
+ "test_cli_refuses_to_overwrite_existing_file",
2041
+ "test_cli_rejects_output_outside_repository",
2042
+ "test_cli_requires_local_draft_suffix",
2043
+ "test_cli_rejects_git_metadata_output",
2044
+ "test_cli_rejects_malformed_reviewed_commit",
2045
+ ):
2046
+ if marker not in draft_tests:
2047
+ fail(f"{draft_test_relative}: missing draft generator regression marker: {marker}")
2048
+ for marker in (
2049
+ "strict_validation_ready",
2050
+ "incomplete_or_unsafe",
2051
+ "credentialLikeValueDetected",
2052
+ '"liveSendApproved": False',
2053
+ "validate_packet",
2054
+ "validate_commit_sha",
2055
+ ):
2056
+ if marker not in reporter:
2057
+ fail(f"{reporter_relative}: missing read-only readiness report marker: {marker}")
2058
+ for marker in (
2059
+ "test_complete_example_is_strictly_ready",
2060
+ "test_draft_lists_all_incomplete_gates",
2061
+ "test_expected_commit_mismatch_is_reported",
2062
+ "test_malformed_expected_commit_exits_two",
2063
+ "test_malformed_json_exits_two",
2064
+ "test_credential_like_value_is_reported_without_echoing_it",
2065
+ "test_credential_like_key_is_reported_without_echoing_it",
2066
+ "test_report_does_not_expose_absolute_workspace_path",
2067
+ "test_report_redacts_credential_like_filename",
2068
+ ):
2069
+ if marker not in report_tests:
2070
+ fail(f"{report_test_relative}: missing readiness report regression marker: {marker}")
2071
+ if "*.operator-evidence.local.json" not in gitignore:
2072
+ fail(".gitignore: missing local operator evidence draft rule")
2073
+ if "scripts/test_operator_evidence.py" not in test_all:
2074
+ fail("scripts/test_all.py: missing operator evidence regression command")
2075
+ if "scripts/test_operator_evidence_draft.py" not in test_all:
2076
+ fail("scripts/test_all.py: missing operator evidence draft regression command")
2077
+ if "scripts/test_operator_evidence_report.py" not in test_all:
2078
+ fail("scripts/test_all.py: missing operator evidence report regression command")
2079
+ for forbidden in (
2080
+ "write_text(",
2081
+ "write_bytes(",
2082
+ '.open("w',
2083
+ ".open('w",
2084
+ "subprocess",
2085
+ "urllib",
2086
+ "socket",
2087
+ "requests",
2088
+ ):
2089
+ if forbidden in reporter:
2090
+ fail(f"{reporter_relative}: forbidden non-read-only marker: {forbidden}")
2091
+ for surface, text in (
2092
+ ("README.md", readme),
2093
+ ("index.html", index),
2094
+ ("docs/viewer.js", viewer),
2095
+ ):
2096
+ if "arc-testnet-operator-evidence.md" not in text:
2097
+ fail(f"{surface}: missing Arc Testnet operator evidence link")
2098
+
2099
+
2100
+ def validate_robots_txt() -> None:
2101
+ relative = "robots.txt"
2102
+ text = (ROOT / relative).read_text(encoding="utf-8")
2103
+ lowered = text.lower()
2104
+ if "user-agent:" not in lowered:
2105
+ fail(f"{relative}: missing User-agent directive")
2106
+ if "sitemap:" not in lowered:
2107
+ fail(f"{relative}: missing Sitemap directive")
2108
+ if CANONICAL_BASE_URL + "sitemap.xml" not in text:
2109
+ fail(
2110
+ f"{relative}: Sitemap directive must point at "
2111
+ f"{CANONICAL_BASE_URL}sitemap.xml"
2112
+ )
2113
+
2114
+
2115
+ def validate_sitemap_xml() -> None:
2116
+ relative = "sitemap.xml"
2117
+ text = (ROOT / relative).read_text(encoding="utf-8")
2118
+ if "<urlset" not in text or "sitemaps.org/schemas/sitemap" not in text:
2119
+ fail(f"{relative}: must be a sitemap 0.9 urlset document")
2120
+ for location in SITEMAP_REQUIRED_LOCATIONS:
2121
+ if f"<loc>{location}</loc>" not in text:
2122
+ fail(f"{relative}: missing <loc>{location}</loc>")
2123
+
2124
+
2125
+ def validate_installed_package() -> None:
2126
+ """Validate the self-contained wheel without requiring a git checkout."""
2127
+ missing = [relative for relative in INSTALLED_REQUIRED_FILES if not (ROOT / relative).is_file()]
2128
+ if missing:
2129
+ fail("installed package is missing required files: " + ", ".join(missing))
2130
+
2131
+ try:
2132
+ facts = json.loads((ROOT / "config/arc_testnet.facts.json").read_text(encoding="utf-8"))
2133
+ policy = json.loads(
2134
+ (
2135
+ ROOT
2136
+ / "examples/arc-testnet-wallet-send-gate/live-infrastructure-policy.example.json"
2137
+ ).read_text(encoding="utf-8")
2138
+ )
2139
+ except (OSError, ValueError) as exc:
2140
+ fail(f"installed package contains invalid reviewed JSON: {exc}")
2141
+
2142
+ expected_facts = {
2143
+ ("network", "chainId"): 5042002,
2144
+ ("network", "chainIdHex"): "0x4cef52",
2145
+ ("network", "rpcUrl"): "https://rpc.testnet.arc.network",
2146
+ ("network", "explorerUrl"): "https://testnet.arcscan.app",
2147
+ ("erc20Usdc", "address"): "0x3600000000000000000000000000000000000000",
2148
+ ("erc20Usdc", "decimals"): 6,
2149
+ ("nativeGas", "decimals"): 18,
2150
+ }
2151
+ for (section, key), expected in expected_facts.items():
2152
+ actual = facts.get(section, {}).get(key)
2153
+ if actual != expected:
2154
+ fail(
2155
+ f"installed Arc Testnet fact drift at {section}.{key}: "
2156
+ f"expected {expected!r}, got {actual!r}"
2157
+ )
2158
+
2159
+ if facts.get("policy", {}).get("mainnetSupported") is not False:
2160
+ fail("installed package must keep Arc mainnet disabled")
2161
+ if policy.get("profiles", {}).get("arcMainnet", {}).get("enabled") is not False:
2162
+ fail("installed wallet policy must keep Arc mainnet disabled")
2163
+ if policy.get("custody", {}).get("implemented") is not False:
2164
+ fail("installed wallet policy must keep custody disabled")
2165
+
2166
+ print("installed package validation passed", file=sys.stdout)
2167
+
2168
+
2169
+ def main() -> None:
2170
+ if not IS_SOURCE_CHECKOUT:
2171
+ validate_installed_package()
2172
+ return
2173
+ validate_required_files()
2174
+ validate_workflow_security()
2175
+ validate_no_secrets()
2176
+ validate_public_text_integrity()
2177
+ validate_repository_line_ending_policy()
2178
+ validate_documented_secret_handling()
2179
+ validate_html()
2180
+ validate_reduced_motion_css()
2181
+ validate_responsive_layout_guards()
2182
+ validate_public_inventory_counts()
2183
+ validate_local_links()
2184
+ validate_markdown_local_links()
2185
+ validate_no_raw_markdown_links()
2186
+ validate_docs_viewer_registry()
2187
+ validate_completion_contract()
2188
+ validate_demo_safety_copy()
2189
+ validate_public_launch_packet()
2190
+ validate_arc_release_packet()
2191
+ validate_x402_boundary_demo()
2192
+ validate_arc_production_deployment_assets()
2193
+ validate_arc_testnet_status_helper()
2194
+ validate_payment_intent_playground_status_panel()
2195
+ validate_receipt_verifier_playground()
2196
+ validate_receipt_viewer()
2197
+ validate_payment_intent_receipt_matcher()
2198
+ validate_transaction_status_playground()
2199
+ validate_guarded_wallet_send_gate()
2200
+ validate_job_escrow_simulator()
2201
+ validate_arc_agent_treasury_lab()
2202
+ validate_agentic_maintainer_loop()
2203
+ validate_arc_testnet_send_readiness_gate()
2204
+ validate_arc_testnet_operator_runbook()
2205
+ validate_arc_testnet_operator_evidence()
2206
+ validate_robots_txt()
2207
+ validate_sitemap_xml()
2208
+ print("validation passed", file=sys.stdout)
2209
+
2210
+
2211
+ if __name__ == "__main__":
2212
+ main()