capsule-engine 0.1.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 (288) hide show
  1. capsule_engine/__init__.py +17 -0
  2. capsule_engine/bundle_viewer/__init__.py +21 -0
  3. capsule_engine/bundle_viewer/static/offline_shell.html +1509 -0
  4. capsule_engine/bundle_viewer/viewer.py +57 -0
  5. capsule_engine/cli/__init__.py +5 -0
  6. capsule_engine/cli/bundle_cmd.py +269 -0
  7. capsule_engine/cli/console_cmd.py +70 -0
  8. capsule_engine/cli/constraints_cmd.py +74 -0
  9. capsule_engine/cli/format.py +173 -0
  10. capsule_engine/cli/guard_cmds.py +361 -0
  11. capsule_engine/cli/init_cmds.py +169 -0
  12. capsule_engine/cli/main.py +120 -0
  13. capsule_engine/cli/packs_cmds.py +96 -0
  14. capsule_engine/cli/telemetry_cmd.py +80 -0
  15. capsule_engine/cli/tenant_cmds.py +199 -0
  16. capsule_engine/cli/verify_cmd.py +110 -0
  17. capsule_engine/compiler/__init__.py +13 -0
  18. capsule_engine/compiler/effect_model.py +79 -0
  19. capsule_engine/compiler/vocabulary.py +253 -0
  20. capsule_engine/console/__init__.py +10 -0
  21. capsule_engine/console/api.py +251 -0
  22. capsule_engine/console/components.css +406 -0
  23. capsule_engine/console/console.css +130 -0
  24. capsule_engine/console/console.html +51 -0
  25. capsule_engine/console/console.js +330 -0
  26. capsule_engine/console/gallery.html +274 -0
  27. capsule_engine/console/server.py +137 -0
  28. capsule_engine/console/tokens.css +68 -0
  29. capsule_engine/conversation/__init__.py +88 -0
  30. capsule_engine/conversation/capsules.py +268 -0
  31. capsule_engine/conversation/exchange.py +279 -0
  32. capsule_engine/conversation/merkle.py +80 -0
  33. capsule_engine/conversation/session.py +101 -0
  34. capsule_engine/envcompat.py +15 -0
  35. capsule_engine/events/__init__.py +14 -0
  36. capsule_engine/events/capsule.py +133 -0
  37. capsule_engine/examples/__init__.py +9 -0
  38. capsule_engine/examples/data/tau2_airline/LICENSE-tau2-bench +21 -0
  39. capsule_engine/examples/data/tau2_airline/PROVENANCE.json +70 -0
  40. capsule_engine/examples/data/tau2_airline/hand_labels.json +893 -0
  41. capsule_engine/examples/data/tau2_airline/pilot1_live_gemini-2-5-flash_airline_shift1.jsonl +190 -0
  42. capsule_engine/examples/data/tau2_airline/tau2_committed_claude-3-7-sonnet_airline_trial0.jsonl +556 -0
  43. capsule_engine/examples/data/tau2_airline/tau2_committed_gpt-4-1-mini_airline_trial0.jsonl +480 -0
  44. capsule_engine/examples/data/tau2_airline/tau2_committed_gpt-4-1_airline_trial0.jsonl +496 -0
  45. capsule_engine/examples/data/tau2_airline/tau2_committed_o4-mini_airline_trial0.jsonl +392 -0
  46. capsule_engine/examples/data/tau2_airline/tau2_conversations_claude-3-7-sonnet_airline_4trials.jsonl +200 -0
  47. capsule_engine/examples/tau2_airline_reference.py +460 -0
  48. capsule_engine/examples/tau2_conversation_exchange.py +92 -0
  49. capsule_engine/folds/__init__.py +104 -0
  50. capsule_engine/folds/account_core.py +89 -0
  51. capsule_engine/folds/approval_latency.py +86 -0
  52. capsule_engine/folds/catalog.py +79 -0
  53. capsule_engine/folds/catalog_defs/actions.count_by_developer.yaml +10 -0
  54. capsule_engine/folds/catalog_defs/actions.executed_count.yaml +16 -0
  55. capsule_engine/folds/catalog_defs/actions.last_decision.yaml +12 -0
  56. capsule_engine/folds/catalog_defs/counterparty.seen_before.v2.yaml +34 -0
  57. capsule_engine/folds/catalog_defs/counterparty.seen_before.v3.yaml +38 -0
  58. capsule_engine/folds/catalog_defs/counterparty.seen_before.yaml +28 -0
  59. capsule_engine/folds/catalog_defs/disclosure.class_seen.yaml +29 -0
  60. capsule_engine/folds/catalog_defs/disclosure.statement_seen.yaml +32 -0
  61. capsule_engine/folds/catalog_defs/hold.active_exposure.yaml +44 -0
  62. capsule_engine/folds/catalog_defs/spend.weekly.v2.yaml +38 -0
  63. capsule_engine/folds/catalog_defs/spend.weekly.v3.1.yaml +71 -0
  64. capsule_engine/folds/catalog_defs/spend.weekly.v3.yaml +76 -0
  65. capsule_engine/folds/catalog_defs/spend.weekly.yaml +33 -0
  66. capsule_engine/folds/counterparty_signals.py +179 -0
  67. capsule_engine/folds/definition.py +421 -0
  68. capsule_engine/folds/duration.py +16 -0
  69. capsule_engine/folds/engine.py +436 -0
  70. capsule_engine/folds/errors.py +44 -0
  71. capsule_engine/folds/loader.py +24 -0
  72. capsule_engine/folds/ordering.py +143 -0
  73. capsule_engine/folds/paths.py +25 -0
  74. capsule_engine/folds/record_type_coverage.py +58 -0
  75. capsule_engine/folds/reducers.py +75 -0
  76. capsule_engine/folds/retention_continuity.py +117 -0
  77. capsule_engine/folds/set_membership.py +59 -0
  78. capsule_engine/folds/taxonomy.py +231 -0
  79. capsule_engine/guards/__init__.py +59 -0
  80. capsule_engine/guards/action.py +446 -0
  81. capsule_engine/guards/action_taxonomy.json +27 -0
  82. capsule_engine/guards/action_taxonomy_v2.json +27 -0
  83. capsule_engine/guards/action_taxonomy_v3.json +27 -0
  84. capsule_engine/guards/action_taxonomy_v4.json +27 -0
  85. capsule_engine/guards/action_taxonomy_v5.json +27 -0
  86. capsule_engine/guards/capsule.py +377 -0
  87. capsule_engine/guards/checks/__init__.py +268 -0
  88. capsule_engine/guards/checks/action_class_gate.py +199 -0
  89. capsule_engine/guards/checks/base.py +33 -0
  90. capsule_engine/guards/checks/caps.py +517 -0
  91. capsule_engine/guards/checks/channel_change.py +64 -0
  92. capsule_engine/guards/checks/commitment.py +41 -0
  93. capsule_engine/guards/checks/counterparty_identity_change.py +95 -0
  94. capsule_engine/guards/checks/counterparty_list.py +176 -0
  95. capsule_engine/guards/checks/counterparty_seen_before.py +160 -0
  96. capsule_engine/guards/checks/credential_pattern.py +73 -0
  97. capsule_engine/guards/checks/dedupe.py +259 -0
  98. capsule_engine/guards/checks/destination_rail.py +120 -0
  99. capsule_engine/guards/checks/field_change_count.py +100 -0
  100. capsule_engine/guards/checks/offer_expiry.py +134 -0
  101. capsule_engine/guards/checks/plan_containment.py +180 -0
  102. capsule_engine/guards/checks/price_floor.py +211 -0
  103. capsule_engine/guards/checks/promise_never.py +236 -0
  104. capsule_engine/guards/checks/promise_requires_approval.py +214 -0
  105. capsule_engine/guards/checks/recipient_role.py +117 -0
  106. capsule_engine/guards/checks/recurring_charge.py +49 -0
  107. capsule_engine/guards/checks/refundability.py +46 -0
  108. capsule_engine/guards/checks/release_on_acceptance.py +286 -0
  109. capsule_engine/guards/checks/required_disclosure.py +233 -0
  110. capsule_engine/guards/checks/single_commitment.py +221 -0
  111. capsule_engine/guards/checks/task_authority.py +182 -0
  112. capsule_engine/guards/checks/upfront_amount.py +71 -0
  113. capsule_engine/guards/checks/verify_before_dispatch.py +75 -0
  114. capsule_engine/guards/classes.py +393 -0
  115. capsule_engine/guards/engine.py +926 -0
  116. capsule_engine/guards/history_state.py +127 -0
  117. capsule_engine/guards/plan.py +237 -0
  118. capsule_engine/guards/revocation.py +120 -0
  119. capsule_engine/guards/signing.py +60 -0
  120. capsule_engine/guards/statements.py +62 -0
  121. capsule_engine/guards/tool_call.py +83 -0
  122. capsule_engine/guards/wickets/__init__.py +23 -0
  123. capsule_engine/guards/wickets/catalog.py +75 -0
  124. capsule_engine/guards/wickets/catalog_defs/action_class_gate.v1.1.yaml +38 -0
  125. capsule_engine/guards/wickets/catalog_defs/action_class_gate.v1.2.yaml +52 -0
  126. capsule_engine/guards/wickets/catalog_defs/action_class_gate.yaml +34 -0
  127. capsule_engine/guards/wickets/catalog_defs/caps.v2.yaml +30 -0
  128. capsule_engine/guards/wickets/catalog_defs/caps.v3.yaml +49 -0
  129. capsule_engine/guards/wickets/catalog_defs/caps.v4.yaml +45 -0
  130. capsule_engine/guards/wickets/catalog_defs/caps.v5.1.yaml +53 -0
  131. capsule_engine/guards/wickets/catalog_defs/caps.v5.yaml +43 -0
  132. capsule_engine/guards/wickets/catalog_defs/caps.yaml +17 -0
  133. capsule_engine/guards/wickets/catalog_defs/caps_holds.yaml +18 -0
  134. capsule_engine/guards/wickets/catalog_defs/channel_change.v1.1.yaml +18 -0
  135. capsule_engine/guards/wickets/catalog_defs/channel_change.yaml +10 -0
  136. capsule_engine/guards/wickets/catalog_defs/counterparty_identity_change.yaml +9 -0
  137. capsule_engine/guards/wickets/catalog_defs/counterparty_list.yaml +20 -0
  138. capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v2.yaml +15 -0
  139. capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v3.yaml +23 -0
  140. capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v4.yaml +26 -0
  141. capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.yaml +14 -0
  142. capsule_engine/guards/wickets/catalog_defs/credential_pattern.yaml +14 -0
  143. capsule_engine/guards/wickets/catalog_defs/dedupe.yaml +19 -0
  144. capsule_engine/guards/wickets/catalog_defs/destination_rail.seller.v1.1.yaml +32 -0
  145. capsule_engine/guards/wickets/catalog_defs/destination_rail.seller.yaml +10 -0
  146. capsule_engine/guards/wickets/catalog_defs/destination_rail.yaml +9 -0
  147. capsule_engine/guards/wickets/catalog_defs/hold_reconcile.yaml +14 -0
  148. capsule_engine/guards/wickets/catalog_defs/material_fields_changed.v1.1.yaml +22 -0
  149. capsule_engine/guards/wickets/catalog_defs/material_fields_changed.yaml +14 -0
  150. capsule_engine/guards/wickets/catalog_defs/offer_expiry.v1.1.yaml +31 -0
  151. capsule_engine/guards/wickets/catalog_defs/offer_expiry.yaml +24 -0
  152. capsule_engine/guards/wickets/catalog_defs/offer_fields_changed.v1.1.yaml +22 -0
  153. capsule_engine/guards/wickets/catalog_defs/offer_fields_changed.yaml +10 -0
  154. capsule_engine/guards/wickets/catalog_defs/price_floor.yaml +32 -0
  155. capsule_engine/guards/wickets/catalog_defs/promise_never.v1.1.yaml +64 -0
  156. capsule_engine/guards/wickets/catalog_defs/promise_never.yaml +16 -0
  157. capsule_engine/guards/wickets/catalog_defs/promise_requires_approval.v1.1.yaml +26 -0
  158. capsule_engine/guards/wickets/catalog_defs/promise_requires_approval.yaml +21 -0
  159. capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.v1.1.yaml +23 -0
  160. capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.v1.2.yaml +24 -0
  161. capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.yaml +10 -0
  162. capsule_engine/guards/wickets/catalog_defs/recipient_role.v1.1.yaml +26 -0
  163. capsule_engine/guards/wickets/catalog_defs/recipient_role.yaml +11 -0
  164. capsule_engine/guards/wickets/catalog_defs/recipient_seen_before.yaml +16 -0
  165. capsule_engine/guards/wickets/catalog_defs/recurring_charge.yaml +9 -0
  166. capsule_engine/guards/wickets/catalog_defs/refundability.v1.1.yaml +15 -0
  167. capsule_engine/guards/wickets/catalog_defs/refundability.yaml +7 -0
  168. capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.v1.1.yaml +44 -0
  169. capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.v1.2.yaml +63 -0
  170. capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.yaml +36 -0
  171. capsule_engine/guards/wickets/catalog_defs/required_disclosure.v1.1.yaml +36 -0
  172. capsule_engine/guards/wickets/catalog_defs/required_disclosure.v1.2.yaml +42 -0
  173. capsule_engine/guards/wickets/catalog_defs/required_disclosure.yaml +25 -0
  174. capsule_engine/guards/wickets/catalog_defs/single_commitment.seller.yaml +14 -0
  175. capsule_engine/guards/wickets/catalog_defs/task_authority.v1.1.yaml +20 -0
  176. capsule_engine/guards/wickets/catalog_defs/task_authority.yaml +8 -0
  177. capsule_engine/guards/wickets/catalog_defs/upfront_amount.v1.1.yaml +19 -0
  178. capsule_engine/guards/wickets/catalog_defs/upfront_amount.yaml +11 -0
  179. capsule_engine/guards/wickets/catalog_defs/verify_before_dispatch.yaml +9 -0
  180. capsule_engine/guards/wickets/definition.py +164 -0
  181. capsule_engine/guards/wickets/errors.py +18 -0
  182. capsule_engine/guards/wickets/loader.py +26 -0
  183. capsule_engine/guards/wickets/retired.py +62 -0
  184. capsule_engine/holds/__init__.py +35 -0
  185. capsule_engine/holds/capsules.py +266 -0
  186. capsule_engine/holds/engine.py +476 -0
  187. capsule_engine/holds/errors.py +29 -0
  188. capsule_engine/holds/policy.py +62 -0
  189. capsule_engine/holds/scope.py +47 -0
  190. capsule_engine/mcp/__init__.py +16 -0
  191. capsule_engine/mcp/config.py +117 -0
  192. capsule_engine/mcp/server.py +338 -0
  193. capsule_engine/mcp/tools.py +424 -0
  194. capsule_engine/packaging.py +60 -0
  195. capsule_engine/packs/__init__.py +45 -0
  196. capsule_engine/packs/backfill_coverage.py +238 -0
  197. capsule_engine/packs/catalog/airline-engagement/AI-BOOTSTRAP.md +53 -0
  198. capsule_engine/packs/catalog/airline-engagement/folds/session_close.yaml +8 -0
  199. capsule_engine/packs/catalog/airline-engagement/pack.yaml +146 -0
  200. capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/good_execution.json +36 -0
  201. capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/oversight_declared.json +36 -0
  202. capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/tampered_execution.json +36 -0
  203. capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/trusted_issuer.jwk.json +5 -0
  204. capsule_engine/packs/catalog/eu-ai-act/obligations-pack-note.md +68 -0
  205. capsule_engine/packs/catalog/eu-ai-act/obligations-register.yaml +761 -0
  206. capsule_engine/packs/catalog/eu-ai-act/pack.yaml +88 -0
  207. capsule_engine/packs/catalog/eu-ai-act/register.yaml +96 -0
  208. capsule_engine/packs/catalog/eu-ai-act-deterministic/fixtures/synthetic_internal_assist_sample.jsonl +6 -0
  209. capsule_engine/packs/catalog/eu-ai-act-deterministic/pack.yaml +580 -0
  210. capsule_engine/packs/catalog/everyday/AI-BOOTSTRAP.md +194 -0
  211. capsule_engine/packs/catalog/everyday/fixtures/mini_ledger.jsonl +45 -0
  212. capsule_engine/packs/catalog/everyday/pack.yaml +480 -0
  213. capsule_engine/packs/catalog/payments-safety/AI-BOOTSTRAP.md +83 -0
  214. capsule_engine/packs/catalog/payments-safety/fixtures/mini_ledger.jsonl +8 -0
  215. capsule_engine/packs/catalog/payments-safety/folds/spend_weekly.yaml +29 -0
  216. capsule_engine/packs/catalog/payments-safety/pack.yaml +112 -0
  217. capsule_engine/packs/catalog/seller/fixtures/mini_ledger.jsonl +83 -0
  218. capsule_engine/packs/catalog/seller/pack.yaml +503 -0
  219. capsule_engine/packs/catalog/standard-vendor/AI-BOOTSTRAP.md +70 -0
  220. capsule_engine/packs/catalog/standard-vendor/folds/session_close.yaml +8 -0
  221. capsule_engine/packs/catalog/standard-vendor/pack.yaml +466 -0
  222. capsule_engine/packs/contract_diff.py +809 -0
  223. capsule_engine/packs/contract_validate.py +185 -0
  224. capsule_engine/packs/corpus_verify.py +126 -0
  225. capsule_engine/packs/enforce.py +81 -0
  226. capsule_engine/packs/errors.py +159 -0
  227. capsule_engine/packs/eu_ai_act_trace_field_map.py +46 -0
  228. capsule_engine/packs/install.py +266 -0
  229. capsule_engine/packs/loader.py +1462 -0
  230. capsule_engine/packs/measurability_report.py +280 -0
  231. capsule_engine/packs/obligation_results.py +72 -0
  232. capsule_engine/packs/pins.py +92 -0
  233. capsule_engine/packs/schema.py +1053 -0
  234. capsule_engine/packs/thresholds.py +146 -0
  235. capsule_engine/packs/trace_attainment.py +188 -0
  236. capsule_engine/packs/trace_reader.py +104 -0
  237. capsule_engine/policy/__init__.py +37 -0
  238. capsule_engine/policy/activation.py +87 -0
  239. capsule_engine/policy/catalog_defs/default.yaml +23 -0
  240. capsule_engine/policy/catalog_defs/holds.yaml +27 -0
  241. capsule_engine/policy/errors.py +46 -0
  242. capsule_engine/policy/limits.py +287 -0
  243. capsule_engine/policy/loader.py +26 -0
  244. capsule_engine/policy/manifest.py +289 -0
  245. capsule_engine/policy/profile.py +281 -0
  246. capsule_engine/policy/resolve.py +224 -0
  247. capsule_engine/register/__init__.py +39 -0
  248. capsule_engine/register/compiler.py +219 -0
  249. capsule_engine/register/errors.py +40 -0
  250. capsule_engine/register/loader.py +247 -0
  251. capsule_engine/register/schema.py +129 -0
  252. capsule_engine/registry/__init__.py +19 -0
  253. capsule_engine/registry/conventions.json +162 -0
  254. capsule_engine/registry/conventions.py +188 -0
  255. capsule_engine/registry/cpb_registry.json +78 -0
  256. capsule_engine/report/__init__.py +62 -0
  257. capsule_engine/report/build.py +267 -0
  258. capsule_engine/report/coverage.py +777 -0
  259. capsule_engine/report/errors.py +35 -0
  260. capsule_engine/report/live_check.py +126 -0
  261. capsule_engine/report/live_history.py +420 -0
  262. capsule_engine/report/model.py +95 -0
  263. capsule_engine/report/render.py +467 -0
  264. capsule_engine/report/replay.py +1677 -0
  265. capsule_engine/report/result.py +504 -0
  266. capsule_engine/report/result_from_folds.py +222 -0
  267. capsule_engine/report/result_from_layers.py +670 -0
  268. capsule_engine/report/sale_bundle.py +864 -0
  269. capsule_engine/report/static/verify.js +451 -0
  270. capsule_engine/schemas/evidence-contract-v0.json +383 -0
  271. capsule_engine/schemas/vendor/epistemic-types.json +21 -0
  272. capsule_engine/schemas/vendor/evidence-result-v0.json +463 -0
  273. capsule_engine/telemetry/__init__.py +9 -0
  274. capsule_engine/telemetry/consent.py +39 -0
  275. capsule_engine/telemetry/events.py +93 -0
  276. capsule_engine/telemetry/fixtures/synthetic_events.jsonl +45 -0
  277. capsule_engine/telemetry/funnel.py +147 -0
  278. capsule_engine/telemetry/record.py +106 -0
  279. capsule_engine/telemetry/sink.py +61 -0
  280. capsule_engine/telemetry/state.py +60 -0
  281. capsule_engine/tenants.py +327 -0
  282. capsule_engine-0.1.0.dist-info/METADATA +47 -0
  283. capsule_engine-0.1.0.dist-info/RECORD +288 -0
  284. capsule_engine-0.1.0.dist-info/WHEEL +5 -0
  285. capsule_engine-0.1.0.dist-info/entry_points.txt +2 -0
  286. capsule_engine-0.1.0.dist-info/licenses/LICENSE +202 -0
  287. capsule_engine-0.1.0.dist-info/licenses/capsule_engine/examples/data/tau2_airline/LICENSE-tau2-bench +21 -0
  288. capsule_engine-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1509 @@
1
+ <!--
2
+ Vendored from action-state-group/scitt-cose, hosted_profiles/hosted.py,
3
+ render_bundle_page(offline=True), commit 32e93a03966d63cf4698fbaa0d4f3bc7ee8438c3.
4
+ Apache-2.0, same license as this repo. Do not hand-edit -- re-run
5
+ scripts/vendor_bundle_viewer.py against a scitt-cose checkout instead.
6
+ The one inline <script> tag right after <body> that sets the page's
7
+ bundle-fragment global is the real embed slot, swapped in at
8
+ bundle-creation time by capsule_ledger/bundle_viewer/viewer.py with the
9
+ real bundle fragment. Other appearances of that same placeholder text
10
+ further down are BUNDLE_JS's own placeholder-detection/re-splice logic
11
+ (the download button) -- do not touch those when substituting.
12
+ -->
13
+ <!DOCTYPE html>
14
+ <html lang="en">
15
+ <head>
16
+ <meta charset="UTF-8">
17
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
18
+ <title>Ledger bundle verifier — capsule-ledger</title>
19
+ <style>
20
+
21
+ :root{
22
+ --ink:#0B0E14; --ink-2:#161B25; --paper:#FCFCFA; --paper-2:#F4F4F0;
23
+ --line:#E3E3DC; --line-2:#2A313F;
24
+ --muted:#5C6573; --muted-2:#9AA3B2;
25
+ --accent:#3A5BD9; --accent-soft:#EAEEFC;
26
+ --pass:#127A52; --pass-soft:#E6F2EC;
27
+ --fail:#B3261E; --fail-soft:#FBEAE8;
28
+ --mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
29
+ }
30
+ *{margin:0;padding:0;box-sizing:border-box}
31
+ body{font-family:system-ui,sans-serif;background:var(--paper);color:var(--ink);line-height:1.6;-webkit-font-smoothing:antialiased}
32
+ a{color:inherit}
33
+ .wrap{max-width:980px;margin:0 auto;padding:0 32px}
34
+ .mono{font-family:var(--mono)}
35
+
36
+ nav{position:sticky;top:0;z-index:50;background:rgba(252,252,250,0.9);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
37
+ .nav-in{max-width:980px;margin:0 auto;padding:14px 32px;display:flex;align-items:center;justify-content:space-between;gap:18px}
38
+ .brand{display:flex;align-items:center;gap:10px;font-weight:600;font-size:15px;letter-spacing:-0.2px;text-decoration:none;color:var(--ink)}
39
+ .brand .glyph{width:22px;height:22px;border:1.5px solid var(--ink);border-radius:5px;position:relative;flex-shrink:0}
40
+ .brand .glyph::after{content:'';position:absolute;inset:4px;border-left:1.5px solid var(--accent);border-bottom:1.5px solid var(--accent);transform:rotate(-45deg) translate(1px,-1px)}
41
+ .brand .svc{font-family:var(--mono);font-size:11px;font-weight:500;letter-spacing:1px;text-transform:uppercase;color:var(--muted);border-left:1px solid var(--line);padding-left:10px;margin-left:2px}
42
+ .nav-links{display:flex;gap:22px;align-items:center}
43
+ .nav-links a{font-size:13.5px;color:var(--muted);text-decoration:none;transition:color .15s;white-space:nowrap}
44
+ .nav-links a:hover{color:var(--ink)}
45
+ .nav-links a.active{color:var(--ink);font-weight:600}
46
+ .nav-ghost{font-family:var(--mono);font-size:13px;border:1px solid var(--line);padding:7px 14px;border-radius:7px;color:var(--ink)!important}
47
+ .nav-ghost:hover{border-color:var(--ink)}
48
+
49
+ .hero{padding:46px 0 26px}
50
+ .pill{display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:12px;letter-spacing:.5px;text-transform:uppercase;color:var(--pass);background:var(--pass-soft);padding:6px 13px;border-radius:100px;margin-bottom:18px}
51
+ .hero h1{font-size:clamp(26px,3.6vw,38px);letter-spacing:-1px;font-weight:700;line-height:1.12;max-width:22ch;margin-bottom:14px}
52
+ .hero p{font-size:16.5px;color:var(--muted);max-width:64ch}
53
+
54
+ .tool{border:1px solid var(--line);border-radius:16px;background:#fff;overflow:hidden;margin:28px 0 8px;box-shadow:0 2px 18px rgba(11,14,20,.04)}
55
+ .tool-head{display:flex;border-bottom:1px solid var(--line)}
56
+ .tab{flex:1;padding:14px 18px;font-size:13.5px;font-weight:500;color:var(--muted);background:var(--paper-2);border:none;cursor:pointer;border-right:1px solid var(--line);font-family:inherit;transition:all .15s}
57
+ .tab:last-child{border-right:none}
58
+ .tab.active{background:#fff;color:var(--ink);box-shadow:inset 0 -2px 0 var(--accent)}
59
+ .tool-body{padding:24px}
60
+ .panel{display:none;flex-direction:column;gap:16px}
61
+ .panel.active{display:flex}
62
+ .field label{display:block;font-size:13px;font-weight:600;margin-bottom:6px}
63
+ .field label .opt{font-weight:400;color:var(--muted-2);font-family:var(--mono);font-size:11px;margin-left:6px}
64
+ .field .hint{font-size:12px;color:var(--muted);margin-top:5px}
65
+ textarea{width:100%;min-height:84px;resize:vertical;font-family:var(--mono);font-size:12.5px;line-height:1.5;color:var(--ink);background:var(--paper);border:1px solid var(--line);border-radius:9px;padding:11px 13px;outline:none;transition:border-color .15s}
66
+ textarea:focus{border-color:var(--accent)}
67
+ textarea::placeholder{color:var(--muted-2)}
68
+ .filerow{display:flex;align-items:center;gap:10px;margin-top:7px}
69
+ .fbtn{font-family:var(--mono);font-size:12px;border:1px solid var(--line);background:#fff;border-radius:7px;padding:6px 12px;cursor:pointer;color:var(--ink)}
70
+ .fbtn:hover{border-color:var(--ink)}
71
+ .fname{font-family:var(--mono);font-size:12px;color:var(--muted)}
72
+ .actions{display:flex;align-items:center;gap:14px;margin-top:4px}
73
+ .verify-btn{display:inline-flex;align-items:center;gap:9px;background:var(--ink);color:var(--paper);border:none;font-size:14.5px;font-weight:600;font-family:inherit;padding:13px 26px;border-radius:10px;cursor:pointer;transition:background .15s}
74
+ .verify-btn:hover{background:var(--accent)}
75
+ .verify-btn:disabled{opacity:.55;cursor:default}
76
+ .clear-btn{font-size:13px;color:var(--muted);background:none;border:none;cursor:pointer;font-family:inherit;text-decoration:underline;text-underline-offset:2px}
77
+
78
+ .verdict{display:none;margin-top:20px;border-radius:14px;overflow:hidden;border:1px solid var(--line)}
79
+ .verdict.show{display:block}
80
+ .vhead{padding:18px 22px;display:flex;align-items:center;gap:14px}
81
+ .vhead .badge{font-family:var(--mono);font-weight:600;font-size:13px;letter-spacing:.5px;padding:7px 15px;border-radius:8px}
82
+ .vhead .vtext{font-size:15px;font-weight:600}
83
+ .verdict.ok .vhead{background:var(--pass-soft)} .verdict.ok .badge{background:var(--pass);color:#fff} .verdict.ok .vtext{color:var(--pass)}
84
+ .verdict.no .vhead{background:var(--fail-soft)} .verdict.no .badge{background:var(--fail);color:#fff} .verdict.no .vtext{color:var(--fail)}
85
+ .verdict.err .vhead{background:var(--paper-2)} .verdict.err .badge{background:var(--muted);color:#fff} .verdict.err .vtext{color:var(--muted)}
86
+ .vbody{padding:8px 22px 20px;background:#fff}
87
+ .vcard{border-top:1px solid var(--line);padding:16px 0}
88
+ .vcard:first-child{border-top:none}
89
+ .vcard h4{font-family:var(--mono);font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--muted);margin-bottom:10px}
90
+ .kv{display:grid;grid-template-columns:150px 1fr;gap:6px 14px;font-size:13px}
91
+ .kv dt{color:var(--muted);font-family:var(--mono);font-size:12px}
92
+ .kv dd{font-family:var(--mono);font-size:12.5px;word-break:break-all}
93
+ .kv dd.t{color:var(--pass);font-weight:600} .kv dd.f{color:var(--fail);font-weight:600}
94
+ .reasons{list-style:none;display:flex;flex-direction:column;gap:7px}
95
+ .reasons li{font-size:13px;color:var(--ink);padding-left:18px;position:relative;font-family:var(--mono)}
96
+ .reasons li::before{content:'!';position:absolute;left:0;color:var(--fail);font-weight:700}
97
+ .verdict.ok .reasons li::before{content:'✓';color:var(--pass)}
98
+
99
+ section.band{padding:48px 0;border-top:1px solid var(--line)}
100
+ .sec-eyebrow{font-family:var(--mono);font-size:12px;font-weight:500;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px}
101
+ .sec-title{font-size:21px;font-weight:700;letter-spacing:-0.4px;margin-bottom:18px}
102
+ table.boundary{border-collapse:collapse;width:100%;font-size:13.5px;border:1px solid var(--line);border-radius:10px;overflow:hidden}
103
+ table.boundary th,table.boundary td{padding:11px 14px;text-align:left;vertical-align:top;border-bottom:1px solid var(--line)}
104
+ table.boundary thead th{font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);background:var(--paper-2)}
105
+ table.boundary tbody th{font-weight:600;white-space:nowrap;width:160px}
106
+ table.boundary tbody td{font-family:var(--mono);font-size:12.5px;color:var(--muted)}
107
+ table.boundary tr:last-child th,table.boundary tr:last-child td{border-bottom:none}
108
+ .twocol{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:6px}
109
+ .lst h5{font-size:13px;font-weight:600;margin-bottom:10px}
110
+ .lst ul{list-style:none;display:flex;flex-direction:column;gap:8px}
111
+ .lst li{font-size:13.5px;color:var(--muted);padding-left:18px;position:relative}
112
+ .lst.does li::before{content:'+';position:absolute;left:0;color:var(--pass);font-weight:700}
113
+ .lst.dont li::before{content:'–';position:absolute;left:0;color:var(--muted-2);font-weight:700}
114
+ .note{background:var(--paper-2);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:8px;padding:14px 18px;font-size:14px;color:var(--ink);margin-top:6px}
115
+ .note code{font-family:var(--mono);font-size:12.5px;background:#fff;border:1px solid var(--line);border-radius:5px;padding:1px 6px}
116
+ .privacy-lst{list-style:none;display:flex;flex-direction:column;gap:8px;margin-top:10px}
117
+ .privacy-lst li{font-size:13px;color:var(--muted);padding-left:18px;position:relative;font-family:var(--mono)}
118
+ .privacy-lst li::before{content:'+';position:absolute;left:0;color:var(--pass);font-weight:700}
119
+
120
+ footer{padding:48px 0 56px}
121
+ .foot-in{display:flex;justify-content:space-between;gap:24px;flex-wrap:wrap;align-items:flex-start}
122
+ .foot-brand{max-width:38ch}
123
+ .foot-brand p{font-size:13px;color:var(--muted);margin-top:12px}
124
+ .foot-cols{display:flex;gap:48px;flex-wrap:wrap}
125
+ .foot-col h5{font-family:var(--mono);font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--muted-2);margin-bottom:14px}
126
+ .foot-col a{display:block;font-size:13.5px;color:var(--muted);text-decoration:none;margin-bottom:9px}
127
+ .foot-col a:hover{color:var(--ink)}
128
+ .foot-note{margin-top:40px;padding-top:24px;border-top:1px solid var(--line);font-size:12.5px;color:var(--muted-2);font-family:var(--mono)}
129
+
130
+ @media(max-width:780px){
131
+ .twocol{grid-template-columns:1fr}
132
+ .kv{grid-template-columns:1fr}
133
+ .kv dt{margin-top:6px}
134
+ .nav-in{gap:12px}
135
+ .nav-links{gap:16px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
136
+ .nav-links::-webkit-scrollbar{display:none}
137
+ }
138
+
139
+
140
+ .anchor-banner{padding:12px 18px;border-radius:10px;font-size:13.5px;margin-bottom:20px;border:1px solid var(--line)}
141
+ .anchor-banner.anchor-ok{background:var(--pass-soft);border-color:var(--pass);color:var(--pass)}
142
+ .anchor-banner.anchor-none{background:var(--paper-2);color:var(--muted)}
143
+ .anchor-banner.anchor-offline{background:var(--paper-2);color:var(--muted);border-style:dashed}
144
+ .anchor-banner.anchor-loading{color:var(--muted);background:var(--paper-2)}
145
+ .anchor-banner.anchor-fail{background:var(--fail-soft);border-color:var(--fail);border-width:2px;color:var(--fail);font-weight:700}
146
+ .anchor-banner.rung-standalone{background:var(--paper-2);color:var(--muted)}
147
+ .anchor-banner.rung-countersigned{background:var(--paper-2);color:var(--muted)}
148
+ .anchor-banner.rung-local-anchored{background:var(--pass-soft);border-color:var(--pass);color:var(--pass)}
149
+ .anchor-banner.rung-counterparty-visible{background:var(--pass-soft);border-color:var(--pass);color:var(--pass)}
150
+ .anchor-banner.rung-publicly-anchored{background:var(--pass-soft);border-color:var(--pass);color:var(--pass)}
151
+ .anchor-ok{color:var(--pass);font-weight:700}
152
+ .anchor-offline{color:var(--muted);font-weight:600}
153
+ .anchor-none{color:var(--muted)}
154
+ .anchor-fail{color:var(--fail);font-weight:700}
155
+ .anchor-fail-detail{font-family:var(--mono);font-size:11.5px;color:var(--fail);margin-top:6px;word-break:break-all;font-weight:400}
156
+ .ritual-stages{border:1px solid var(--line);border-radius:12px;overflow:hidden}
157
+ .ritual-stage{display:flex;align-items:baseline;gap:10px;padding:10px 16px;border-bottom:1px solid var(--line);font-size:13px}
158
+ .ritual-stage:last-child{border-bottom:none}
159
+ .ritual-mark{width:16px;height:16px;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0}
160
+ .ritual-pass .ritual-mark{background:var(--pass);color:#fff}
161
+ .ritual-fail .ritual-mark{background:var(--fail);color:#fff}
162
+ .ritual-skip .ritual-mark{background:transparent;border:1.5px solid var(--line);color:var(--muted-2)}
163
+ .ritual-name{font-weight:600;width:110px;flex-shrink:0}
164
+ .ritual-detail{color:var(--muted);font-weight:400}
165
+ .ritual-fail .ritual-detail{color:var(--fail)}
166
+ .finding-panel{margin-top:16px;border-radius:12px;padding:14px 18px;border:1px solid var(--line)}
167
+ .finding-panel.finding-fail{background:var(--fail-soft);border-color:var(--fail)}
168
+ .finding-panel.finding-gap{background:var(--fail-soft);border-color:var(--fail)}
169
+ .finding-panel.finding-summary{background:transparent;border-color:var(--line)}
170
+ .finding-summary .finding-label{color:var(--muted)}
171
+ .finding-label{font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:1px;text-transform:uppercase;color:var(--fail);margin-bottom:6px}
172
+ .finding-text{font-size:13.5px;color:var(--ink);margin-bottom:6px}
173
+ .finding-meta{font-family:var(--mono);font-size:11.5px;color:var(--muted)}
174
+ .records-table{border-collapse:collapse;width:100%;font-size:12.5px}
175
+ .records-table th,.records-table td{padding:8px 10px;border:1px solid var(--line);text-align:left}
176
+ .records-table th{background:var(--paper-2);font-family:var(--mono);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em}
177
+ .rec-row.rec-altered{background:var(--fail-soft)}
178
+ .rec-row.rec-flagged{background:rgba(179,38,30,0.03)}
179
+ .rec-row.rec-gap{background:var(--fail-soft);font-style:italic;color:var(--muted)}
180
+ .g-nodes{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px}
181
+ .gn{border:1px solid var(--line);border-radius:10px;padding:12px 16px;min-width:200px;max-width:340px;background:#fff}
182
+ .gn-capsule{border-color:var(--accent);background:var(--accent-soft)}
183
+ .gn-offer_terms{border-color:var(--pass);background:var(--pass-soft)}
184
+ .gn-wicket_manifest{border-color:#f59e0b;background:#fffbeb}
185
+ .gn-opaque{border-style:dashed;opacity:.85}
186
+ .gn-type{display:block;font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;color:var(--muted);margin-bottom:6px}
187
+ .gn-digest{display:block;font-size:11px;word-break:break-all;color:var(--ink)}
188
+ .opaque-badge{font-size:10px;font-weight:700;letter-spacing:.5px;background:var(--muted);color:#fff;padding:1px 6px;border-radius:4px;vertical-align:middle}
189
+ .opaque-note{font-size:12px;color:var(--muted);margin-top:6px;font-style:normal}
190
+ .g-edges{margin-top:4px}
191
+ .etable{border-collapse:collapse;width:100%;font-size:12.5px;margin-bottom:20px}
192
+ .etable th,.etable td{padding:6px 10px;border:1px solid var(--line);text-align:left}
193
+ .etable th{background:var(--paper-2);font-family:var(--mono);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em}
194
+ .etype{font-family:var(--mono);font-weight:600}
195
+ .etype-attests_over{color:var(--pass)}
196
+ .etype-chains_to{color:var(--accent)}
197
+ .etype-commits_to{color:#f59e0b}
198
+ .etype-effect_response{color:var(--muted)}
199
+ .pltable{border-collapse:collapse;width:100%;font-size:12.5px}
200
+ .pltable th,.pltable td{padding:7px 10px;border:1px solid var(--line);text-align:left;vertical-align:top}
201
+ .pltable th{background:var(--paper-2);font-family:var(--mono);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em}
202
+ .pl-withheld{color:#f59e0b;font-weight:700;font-family:var(--mono);font-size:12px}
203
+ .pl-revealed{color:var(--pass);font-weight:600;font-family:var(--mono);font-size:12px}
204
+ .pl-match{color:var(--pass);font-weight:700;font-family:var(--mono);font-size:12px}
205
+ .pl-mismatch{color:var(--fail);font-weight:700;font-family:var(--mono);font-size:12px}
206
+ .pl-ctx{color:var(--muted);font-family:var(--mono);font-size:11px}
207
+ .pl-payload{max-width:340px}
208
+ .pl-payload-details summary{cursor:pointer;color:var(--muted);font-family:var(--mono);font-size:11.5px}
209
+ .pl-payload-details summary code{color:var(--ink)}
210
+ .pl-payload-full{margin-top:6px}
211
+ .pl-payload-full pre{white-space:pre-wrap;word-break:break-word;background:var(--paper-2);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:11.5px;max-height:280px;overflow:auto;margin:0}
212
+ .pl-payload-truncated{color:#f59e0b;font-size:11px;margin:6px 0 0}
213
+ .pl-payload-digests{display:flex;flex-direction:column;gap:2px;margin-top:6px;font-size:11px;color:var(--muted);font-family:var(--mono)}
214
+ .pl-payload-digests code{color:var(--ink)}
215
+ .chain-table{border-collapse:collapse;width:100%;font-size:13px;margin-bottom:16px}
216
+ .chain-table th,.chain-table td{padding:8px 12px;border:1px solid var(--line);text-align:left;vertical-align:middle}
217
+ .chain-table th{background:var(--paper-2);font-family:var(--mono);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em}
218
+ .chain-table tr.chain-row{cursor:pointer}
219
+ .chain-table tr.chain-active td{background:var(--accent-soft);font-weight:600}
220
+ .chain-table tr.chain-row:hover td{background:var(--paper-2)}
221
+ .chain-nav-btns{display:flex;align-items:center;gap:16px;margin-top:12px}
222
+ .chain-pos{font-size:13px;color:var(--muted);flex:1;text-align:center}
223
+ .gn-capsule-prev .gn-digest{color:var(--accent);text-decoration:underline;cursor:pointer}
224
+ .reg-panel{margin:24px 0;border:1px solid var(--line);border-radius:12px;overflow:hidden}
225
+ .reg-panel summary{padding:14px 18px;cursor:pointer;font-size:14px;font-weight:600;background:var(--paper-2);list-style:none;display:flex;align-items:center;gap:10px;user-select:none}
226
+ .reg-panel summary::-webkit-details-marker{display:none}
227
+ .reg-panel summary::before{content:'▶';font-size:10px;color:var(--muted);transition:transform .15s;flex-shrink:0}
228
+ .reg-panel[open] summary::before{transform:rotate(90deg)}
229
+ .reg-panel-body{padding:16px 18px}
230
+ .reg-disclaimer{font-size:12.5px;color:var(--muted);font-style:italic;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid var(--line)}
231
+ .reg-table{border-collapse:collapse;width:100%;font-size:12.5px}
232
+ .reg-table th,.reg-table td{padding:8px 10px;border:1px solid var(--line);text-align:left;vertical-align:top}
233
+ .reg-table th{background:var(--paper-2);font-family:var(--mono);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em}
234
+ .reg-prop{font-family:var(--mono);font-size:11px;background:var(--paper-2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;white-space:nowrap;display:inline-block;margin-bottom:2px}
235
+
236
+
237
+ .share-row{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:20px}
238
+ .share-row .mono{font-size:11.5px;color:var(--muted);word-break:break-all}
239
+ .share-row .btnrow{display:flex;gap:10px;flex-shrink:0}
240
+ .completeness-card{border:1px solid var(--line);border-radius:12px;padding:16px 20px;margin-bottom:20px}
241
+ .completeness-card.status-pass{background:var(--pass-soft);border-color:var(--pass)}
242
+ .completeness-card.status-fail{background:var(--fail-soft);border-color:var(--fail)}
243
+ .completeness-card.status-skip{background:var(--paper-2);color:var(--muted)}
244
+ .completeness-title{font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:1px;text-transform:uppercase;margin-bottom:6px}
245
+ .completeness-card.status-pass .completeness-title{color:var(--pass)}
246
+ .completeness-card.status-fail .completeness-title{color:var(--fail)}
247
+ .completeness-card.status-skip .completeness-title{color:var(--muted)}
248
+ .completeness-detail{font-size:13.5px;color:var(--ink)}
249
+ .bundle-empty{border:1px dashed var(--line);border-radius:12px;padding:28px;text-align:center;color:var(--muted);font-size:14px;margin-bottom:20px}
250
+
251
+ </style>
252
+ </head>
253
+ <body>
254
+ <script>window.__BUNDLE_FRAGMENT_B64U__="@@BUNDLE_FRAGMENT@@";</script>
255
+ <nav>
256
+ <div class="nav-in">
257
+ <a class="brand" href="https://agentactioncapsule.org">
258
+ <span class="glyph"></span> Agent Action Capsule <span class="svc">Verifier</span>
259
+ </a>
260
+ <div class="nav-links">
261
+ <a href="https://agentactioncapsule.org">Standard</a>
262
+ <a href="https://anchor.agentactioncapsule.org">Transparency Log</a>
263
+ <a href="/">Verifier</a>
264
+ <a href="/bundle" class="active">Bundle</a>
265
+ <a href="https://agentactioncapsule.org/docs/">Docs</a>
266
+ </div>
267
+ </div>
268
+ </nav>
269
+
270
+ <div class="wrap" style="padding:32px 0 16px">
271
+ <div class="pill">ledger bundle · completeness certificate</div>
272
+ <h1 style="margin-top:12px">Ledger bundle verifier</h1>
273
+ <p class="mono" id="bundleSummary" style="font-size:13px;color:var(--muted);margin-top:6px">No bundle loaded yet.</p>
274
+ </div>
275
+
276
+ <div class="wrap">
277
+ <div class="share-row">
278
+ <span class="mono" id="permalinkText">(open a bundle to see its permalink)</span>
279
+ <span class="btnrow">
280
+ <button class="verify-btn" id="copyLinkBtn" disabled style="opacity:.5;padding:9px 16px;font-size:13px">Copy permalink</button>
281
+ <button class="verify-btn" id="downloadBtn" disabled style="opacity:.5;padding:9px 16px;font-size:13px">Download self-contained copy</button>
282
+ </span>
283
+ </div>
284
+ </div>
285
+
286
+ <div class="wrap" id="emptyState" style="display:none">
287
+ <div class="bundle-empty">No bundle data in this URL. Open the full shared link (the part after
288
+ <code class="mono">#</code>) that <code class="mono">capsule bundle</code> printed, or paste bundle
289
+ JSON below — this viewer never fetches bundle data from a server.</div>
290
+ </div>
291
+
292
+ <div class="wrap" id="completenessMount"></div>
293
+
294
+ <section id="ritualSection" class="band" style="padding-top:16px">
295
+ <div class="wrap">
296
+ <div class="sec-eyebrow">Verification ritual</div>
297
+ <h2 class="sec-title">Integrity · Sequence · Completeness · Cross-check</h2>
298
+ <p style="font-size:14px;color:var(--muted);margin-bottom:16px">
299
+ Failure is precise: the stage that failed is named, and everything that still verifies keeps its
300
+ verdict. A missing completeness certificate is reported honestly as unavailable, never as a pass.
301
+ </p>
302
+ <div id="ritualMount"></div>
303
+ <div style="margin-top:16px" id="recordsTableContent"></div>
304
+ </div>
305
+ </section>
306
+
307
+ <section id="privlogSection" class="band" style="display:none">
308
+ <div class="wrap">
309
+ <div class="sec-eyebrow">Privilege Log</div>
310
+ <h2 class="sec-title">Disclosed vs. withheld artifacts — all records</h2>
311
+ <p style="font-size:14px;color:var(--muted);margin-bottom:16px">
312
+ WITHHELD = digest committed in the record; payload not provided here.<br>
313
+ REVEALED = payload provided; hash recomputed and checked against the committed digest.
314
+ </p>
315
+ <div id="privlogContent"></div>
316
+ </div>
317
+ </section>
318
+
319
+ <section class="band">
320
+ <div class="wrap">
321
+ <div class="sec-eyebrow">Bundle data</div>
322
+ <h2 class="sec-title">Paste bundle JSON to render</h2>
323
+ <p style="font-size:14px;color:var(--muted);margin-bottom:16px">
324
+ The JSON goes into the URL fragment only — never sent to this server.
325
+ </p>
326
+ <div class="tool">
327
+ <div class="tool-body">
328
+ <div class="field">
329
+ <label>Bundle JSON <span class="opt">output of <code>capsule bundle</code></span></label>
330
+ <textarea id="bundleJson" style="min-height:120px"
331
+ placeholder='{"bundle_version":"1","records":[...],"range":[1,4],"checkpoint":{"tree_size":4}}'></textarea>
332
+ </div>
333
+ <p id="parseErr" style="color:var(--fail);font-family:var(--mono);font-size:12px;margin:8px 0;min-height:18px"></p>
334
+ <div class="actions">
335
+ <button class="verify-btn" id="loadBtn">Load bundle &#x2192;</button>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </div>
340
+ </section>
341
+
342
+ <footer>
343
+ <div class="wrap">
344
+ <div class="foot-in">
345
+ <div class="foot-brand">
346
+ <a class="brand" href="https://agentactioncapsule.org">
347
+ <span class="glyph"></span> Agent Action Capsule <span class="svc">Verifier</span>
348
+ </a>
349
+ <p>Stateless public verification surface for capsule-ledger bundles.
350
+ Free and neutral for any capsule-ledger installation — self-hosted or hosted, no account, no gating.</p>
351
+ </div>
352
+ <div class="foot-cols">
353
+ <div class="foot-col">
354
+ <h5>Standard</h5>
355
+ <a href="https://agentactioncapsule.org">Overview</a>
356
+ <a href="https://agentactioncapsule.org/docs/">Docs</a>
357
+ </div>
358
+ <div class="foot-col">
359
+ <h5>Services</h5>
360
+ <a href="https://anchor.agentactioncapsule.org">Transparency Log</a>
361
+ <a href="/">Verifier</a>
362
+ </div>
363
+ <div class="foot-col">
364
+ <h5>Privacy</h5>
365
+ <a href="https://github.com/action-state-group/scitt-cose">Source &#x2197;</a>
366
+ </div>
367
+ </div>
368
+ </div>
369
+ <div class="foot-note">Stateless &#xb7; retains nothing &#xb7; Apache-2.0 &#xb7; Action State Group</div>
370
+ </div>
371
+ </footer>
372
+ <script>
373
+ var MMR = (function(){
374
+ "use strict";
375
+
376
+ var DIGEST_LEN = 32;
377
+ var MAX_MMR_SIZE = Math.pow(2, 50);
378
+
379
+ function MmrError(msg){ this.message = msg; this.name = "MmrError"; }
380
+ MmrError.prototype = Object.create(Error.prototype);
381
+
382
+ function isNonNegInt(n){ return typeof n === "number" && Number.isInteger(n) && n >= 0; }
383
+
384
+ function requireNonNegInt(n, what){
385
+ if(!isNonNegInt(n)) throw new MmrError(what + " must be a non-negative integer: " + n);
386
+ }
387
+
388
+ function hexToBytes(hex){
389
+ if(typeof hex !== "string" || hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)){
390
+ throw new MmrError("not a valid hex string");
391
+ }
392
+ var out = new Uint8Array(hex.length / 2);
393
+ for(var i = 0; i < out.length; i++){
394
+ out[i] = parseInt(hex.substr(i * 2, 2), 16);
395
+ }
396
+ return out;
397
+ }
398
+
399
+ function bytesToHex(bytes){
400
+ var s = "";
401
+ for(var i = 0; i < bytes.length; i++){
402
+ s += bytes[i].toString(16).padStart(2, "0");
403
+ }
404
+ return s;
405
+ }
406
+
407
+ function assertDigest(b, what){
408
+ if(!(b instanceof Uint8Array) || b.length !== DIGEST_LEN){
409
+ throw new MmrError((what || "digest") + " must be " + DIGEST_LEN + " bytes");
410
+ }
411
+ }
412
+
413
+ function parseDigestHex(h){
414
+ if(typeof h !== "string") throw new MmrError("proof element is not a hex string");
415
+ var b = hexToBytes(h);
416
+ if(b.length !== DIGEST_LEN) throw new MmrError("proof element has wrong digest length: " + b.length);
417
+ return b;
418
+ }
419
+
420
+ function concatBytes(){
421
+ var total = 0;
422
+ for(var i = 0; i < arguments.length; i++) total += arguments[i].length;
423
+ var out = new Uint8Array(total);
424
+ var off = 0;
425
+ for(var j = 0; j < arguments.length; j++){
426
+ out.set(arguments[j], off);
427
+ off += arguments[j].length;
428
+ }
429
+ return out;
430
+ }
431
+
432
+ function bytesEqual(a, b){
433
+ if(a.length !== b.length) return false;
434
+ for(var i = 0; i < a.length; i++){ if(a[i] !== b[i]) return false; }
435
+ return true;
436
+ }
437
+
438
+ async function sha256(bytes){
439
+ var digest = await crypto.subtle.digest("SHA-256", bytes);
440
+ return new Uint8Array(digest);
441
+ }
442
+
443
+ /* be64(n) -- big-endian 8-byte encoding. n stays well within
444
+ * Number.MAX_SAFE_INTEGER (our domain is < MAX_MMR_SIZE = 2**50), so plain
445
+ * float division/modulo is exact -- no BigInt needed. */
446
+ function beU64(n){
447
+ var bytes = new Uint8Array(8);
448
+ var v = n;
449
+ for(var i = 7; i >= 0; i--){
450
+ bytes[i] = v % 256;
451
+ v = Math.floor(v / 256);
452
+ }
453
+ return bytes;
454
+ }
455
+
456
+ /* leaf_hash = sha256(0x00 || body_digest) */
457
+ async function leafHash(bodyDigest){
458
+ assertDigest(bodyDigest, "body_digest");
459
+ return sha256(concatBytes(new Uint8Array([0x00]), bodyDigest));
460
+ }
461
+
462
+ /* interior_hash = sha256(be64(position+1) || left || right) */
463
+ async function interiorHash(left, right, position){
464
+ assertDigest(left, "left");
465
+ assertDigest(right, "right");
466
+ requireNonNegInt(position, "position");
467
+ return sha256(concatBytes(beU64(position + 1), left, right));
468
+ }
469
+
470
+ /* root = bagged peaks, right-to-left, NO domain-separator byte */
471
+ async function rootFromPeaks(peakHashes){
472
+ if(!peakHashes.length) return new Uint8Array(DIGEST_LEN);
473
+ for(var i = 0; i < peakHashes.length; i++) assertDigest(peakHashes[i], "peak");
474
+ var hashes = peakHashes.slice();
475
+ while(hashes.length > 1){
476
+ var right = hashes.pop();
477
+ var left = hashes.pop();
478
+ hashes.push(await sha256(concatBytes(right, left)));
479
+ }
480
+ return hashes[0];
481
+ }
482
+
483
+ function popcount(n){
484
+ var c = 0;
485
+ while(n > 0){
486
+ c += (n % 2 === 1) ? 1 : 0;
487
+ n = Math.floor(n / 2);
488
+ }
489
+ return c;
490
+ }
491
+
492
+ function heightAt(pos){
493
+ requireNonNegInt(pos, "pos");
494
+ var pos1 = pos + 1, h = 0;
495
+ while(Math.pow(2, h + 1) - 1 < pos1) h += 1;
496
+ while(h > 0){
497
+ var size = Math.pow(2, h + 1) - 1;
498
+ if(pos1 === size) return h;
499
+ var leftSize = Math.pow(2, h) - 1;
500
+ if(pos1 > leftSize) pos1 -= leftSize;
501
+ h -= 1;
502
+ }
503
+ return 0;
504
+ }
505
+
506
+ function nodeCount(leafCount_){
507
+ requireNonNegInt(leafCount_, "leaf_count");
508
+ return 2 * leafCount_ - popcount(leafCount_);
509
+ }
510
+
511
+ function peaks(size){
512
+ if(!isNonNegInt(size) || size >= MAX_MMR_SIZE) throw new MmrError("invalid MMR size: " + size);
513
+ var result = [], remaining = size, offset = 0, prevHeight = Infinity;
514
+ while(remaining > 0){
515
+ var h = 0;
516
+ while(Math.pow(2, h + 2) - 1 <= remaining) h += 1;
517
+ if(h >= prevHeight) throw new MmrError("invalid MMR size (not a valid node count): " + size);
518
+ var mSize = Math.pow(2, h + 1) - 1;
519
+ offset += mSize;
520
+ result.push(offset - 1);
521
+ remaining -= mSize;
522
+ prevHeight = h;
523
+ }
524
+ return result;
525
+ }
526
+
527
+ function leafCountFromSize(size){
528
+ var pks = peaks(size), total = 0;
529
+ for(var i = 0; i < pks.length; i++) total += Math.pow(2, heightAt(pks[i]));
530
+ return total;
531
+ }
532
+
533
+ function leafIndexToPos(leafIndex){
534
+ requireNonNegInt(leafIndex, "leaf_index");
535
+ var pos = nodeCount(leafIndex);
536
+ if(pos >= MAX_MMR_SIZE) throw new MmrError("leaf_index too large: " + leafIndex);
537
+ return pos;
538
+ }
539
+
540
+ function findContainingPeak(pos, peakPositions){
541
+ for(var i = 0; i < peakPositions.length; i++){
542
+ var peakPos = peakPositions[i];
543
+ var h = heightAt(peakPos);
544
+ var mSize = Math.pow(2, h + 1) - 1;
545
+ var start = peakPos - mSize + 1;
546
+ if(start <= pos && pos <= peakPos) return i;
547
+ }
548
+ return -1;
549
+ }
550
+
551
+ /* Bottom-up sibling path from targetPos up to (but excluding) the mountain
552
+ * root at rootPos (height `height`). Mirrors core.py's _locate_path. */
553
+ function locatePath(rootPos, height, targetPos){
554
+ var topDown = [];
555
+ var curRoot = rootPos, curHeight = height;
556
+ while(curHeight > 0 && curRoot !== targetPos){
557
+ var parentPos = curRoot;
558
+ var leftSize = Math.pow(2, curHeight) - 1;
559
+ var leftChildRoot = curRoot - leftSize - 1;
560
+ var rightChildRoot = curRoot - 1;
561
+ var step;
562
+ if(targetPos <= leftChildRoot){
563
+ step = {siblingPos: rightChildRoot, targetIsRight: false, parentPos: parentPos};
564
+ curRoot = leftChildRoot;
565
+ }else{
566
+ step = {siblingPos: leftChildRoot, targetIsRight: true, parentPos: parentPos};
567
+ curRoot = rightChildRoot;
568
+ }
569
+ topDown.push(step);
570
+ curHeight -= 1;
571
+ }
572
+ topDown.reverse();
573
+ return topDown;
574
+ }
575
+
576
+ /* Pure, total-order-stable inclusion verification. Never throws -- any
577
+ * malformed input resolves to false. */
578
+ async function verifyInclusion(rootHex, size, leafIndex, bodyDigestHex, proof){
579
+ try{
580
+ var root = hexToBytes(rootHex);
581
+ var bodyDigest = hexToBytes(bodyDigestHex);
582
+ assertDigest(root, "root");
583
+ assertDigest(bodyDigest, "body_digest");
584
+ if(!proof || proof.v !== 1 || proof.kind !== "inclusion") return false;
585
+ if(proof.size !== size || proof.leaf_index !== leafIndex) return false;
586
+ if(!isNonNegInt(size) || size >= MAX_MMR_SIZE) return false;
587
+ if(!isNonNegInt(leafIndex)) return false;
588
+ if(!Array.isArray(proof.witness) || !Array.isArray(proof.peaks_left) || !Array.isArray(proof.peaks_right)){
589
+ return false;
590
+ }
591
+
592
+ var lc = leafCountFromSize(size);
593
+ if(leafIndex >= lc) return false;
594
+
595
+ var leafPos = leafIndexToPos(leafIndex);
596
+ var pks = peaks(size);
597
+ var peakIdx = findContainingPeak(leafPos, pks);
598
+ if(peakIdx === -1) return false;
599
+
600
+ var peakPos = pks[peakIdx];
601
+ var peakHeight = heightAt(peakPos);
602
+ var path = locatePath(peakPos, peakHeight, leafPos);
603
+
604
+ if(proof.witness.length !== path.length) return false;
605
+ if(proof.peaks_left.length !== peakIdx) return false;
606
+ if(proof.peaks_right.length !== pks.length - peakIdx - 1) return false;
607
+
608
+ var witnessBytes = proof.witness.map(parseDigestHex);
609
+ var peaksLeftBytes = proof.peaks_left.map(parseDigestHex);
610
+ var peaksRightBytes = proof.peaks_right.map(parseDigestHex);
611
+
612
+ var acc = await leafHash(bodyDigest);
613
+ for(var i = 0; i < path.length; i++){
614
+ var step = path[i], sib = witnessBytes[i];
615
+ acc = step.targetIsRight
616
+ ? await interiorHash(sib, acc, step.parentPos)
617
+ : await interiorHash(acc, sib, step.parentPos);
618
+ }
619
+
620
+ var allPeaks = peaksLeftBytes.concat([acc]).concat(peaksRightBytes);
621
+ var computedRoot = await rootFromPeaks(allPeaks);
622
+ return bytesEqual(computedRoot, root);
623
+ }catch(e){
624
+ return false;
625
+ }
626
+ }
627
+
628
+ /* Pure consistency verification. Never throws. */
629
+ async function verifyConsistency(rootAHex, sizeA, rootBHex, sizeB, proof){
630
+ try{
631
+ var rootA = hexToBytes(rootAHex);
632
+ var rootB = hexToBytes(rootBHex);
633
+ assertDigest(rootA, "root_a");
634
+ assertDigest(rootB, "root_b");
635
+ if(!proof || proof.v !== 1 || proof.kind !== "consistency") return false;
636
+ if(proof.size_a !== sizeA || proof.size_b !== sizeB) return false;
637
+ if(!isNonNegInt(sizeA)) return false;
638
+ if(!isNonNegInt(sizeB) || sizeB < sizeA) return false;
639
+ if(!Array.isArray(proof.old_peaks) || !Array.isArray(proof.new_peaks) || !Array.isArray(proof.witness)){
640
+ return false;
641
+ }
642
+
643
+ var oldPeakPositions = peaks(sizeA);
644
+ var newPeakPositions = peaks(sizeB);
645
+
646
+ if(proof.old_peaks.length !== oldPeakPositions.length) return false;
647
+ if(proof.new_peaks.length !== newPeakPositions.length) return false;
648
+ if(proof.witness.length !== oldPeakPositions.length) return false;
649
+
650
+ var oldPeaksBytes = proof.old_peaks.map(parseDigestHex);
651
+ var newPeaksBytes = proof.new_peaks.map(parseDigestHex);
652
+
653
+ var computedRootA = await rootFromPeaks(oldPeaksBytes);
654
+ if(!bytesEqual(computedRootA, rootA)) return false;
655
+ var computedRootB = await rootFromPeaks(newPeaksBytes);
656
+ if(!bytesEqual(computedRootB, rootB)) return false;
657
+
658
+ for(var i = 0; i < oldPeakPositions.length; i++){
659
+ var p = oldPeakPositions[i];
660
+ var containingIdx = findContainingPeak(p, newPeakPositions);
661
+ if(containingIdx === -1) return false;
662
+
663
+ var newPeakPos = newPeakPositions[containingIdx];
664
+ var newPeakHeight = heightAt(newPeakPos);
665
+ var path = locatePath(newPeakPos, newPeakHeight, p);
666
+
667
+ var w = proof.witness[i];
668
+ if(!Array.isArray(w) || w.length !== path.length) return false;
669
+ var wBytes = w.map(parseDigestHex);
670
+
671
+ var acc = oldPeaksBytes[i];
672
+ for(var j = 0; j < path.length; j++){
673
+ var step = path[j], sib = wBytes[j];
674
+ acc = step.targetIsRight
675
+ ? await interiorHash(sib, acc, step.parentPos)
676
+ : await interiorHash(acc, sib, step.parentPos);
677
+ }
678
+ if(!bytesEqual(acc, newPeaksBytes[containingIdx])) return false;
679
+ }
680
+
681
+ return true;
682
+ }catch(e){
683
+ return false;
684
+ }
685
+ }
686
+
687
+ /* Rebuilds the hash of the subtree rooted at `pos` (height `height`,
688
+ * covering leaf indices [leafStart, leafStart + 2**height - 1]) from
689
+ * `bodyDigestsByIndex` for any leaf inside [lo, hi] and one witness hash
690
+ * per maximal subtree wholly outside [lo, hi] -- recursing into any
691
+ * subtree the range only partially covers. `cursor` is a one-element
692
+ * array used as a mutable int (JS has no pass-by-reference for numbers).
693
+ * Mirrors cll.checkpoint.core._reconstruct_range_subtree. */
694
+ async function reconstructRangeSubtree(pos, height, leafStart, lo, hi, bodyDigestsByIndex, witnessBytes, cursor){
695
+ var leafEnd = leafStart + Math.pow(2, height) - 1;
696
+ if(leafEnd < lo || leafStart > hi){
697
+ if(cursor[0] >= witnessBytes.length) throw new MmrError("range proof witness exhausted");
698
+ var w = witnessBytes[cursor[0]];
699
+ cursor[0] += 1;
700
+ return w;
701
+ }
702
+ if(height === 0){
703
+ return await leafHash(bodyDigestsByIndex[leafStart]);
704
+ }
705
+ var half = Math.pow(2, height - 1);
706
+ var left = await reconstructRangeSubtree(pos - Math.pow(2, height), height - 1, leafStart, lo, hi, bodyDigestsByIndex, witnessBytes, cursor);
707
+ var right = await reconstructRangeSubtree(pos - 1, height - 1, leafStart + half, lo, hi, bodyDigestsByIndex, witnessBytes, cursor);
708
+ return interiorHash(left, right, pos);
709
+ }
710
+
711
+ /* Pure range-membership verification. Never throws. Mirrors
712
+ * cll.checkpoint.index.verify_range / cll.checkpoint.core.verify_range:
713
+ * rebuilds every peak the range touches from `bodyDigestHexes` (one per
714
+ * leaf, bodyDigestHexes[i] for seq fromSeq + i) folded with `proof`'s
715
+ * witness hashes -- an altered, deleted, or replaced interior leaf
716
+ * changes the peak it falls under and is caught here, unlike a
717
+ * two-boundary inclusion check that never looks at any leaf strictly
718
+ * between the two endpoints. */
719
+ async function verifyRange(rootHex, fromSeq, toSeq, bodyDigestHexes, proof){
720
+ try{
721
+ var root = hexToBytes(rootHex);
722
+ assertDigest(root, "root");
723
+ if(!proof || proof.from_seq !== fromSeq || proof.to_seq !== toSeq) return false;
724
+ if(fromSeq < 1 || toSeq < fromSeq) return false;
725
+ if(leafCountFromSize(proof.size) !== toSeq) return false;
726
+
727
+ var size = proof.size;
728
+ var fromIndex = proof.from_index, toIndex = proof.to_index;
729
+ if(fromIndex !== fromSeq - 1 || toIndex !== toSeq - 1) return false;
730
+ if(!Array.isArray(proof.witness)) return false;
731
+ if(!Array.isArray(bodyDigestHexes)) return false;
732
+ if(bodyDigestHexes.length !== toIndex - fromIndex + 1) return false;
733
+
734
+ var bodyDigestsByIndex = {};
735
+ for(var i = 0; i < bodyDigestHexes.length; i++){
736
+ var d = hexToBytes(bodyDigestHexes[i]);
737
+ assertDigest(d, "body_digest");
738
+ bodyDigestsByIndex[fromIndex + i] = d;
739
+ }
740
+
741
+ var witnessBytes = proof.witness.map(parseDigestHex);
742
+
743
+ var pks = peaks(size);
744
+ var cursor = [0];
745
+ var leafStart = 0;
746
+ var reconstructedPeaks = [];
747
+ for(var pi = 0; pi < pks.length; pi++){
748
+ var p = pks[pi];
749
+ var h = heightAt(p);
750
+ reconstructedPeaks.push(await reconstructRangeSubtree(p, h, leafStart, fromIndex, toIndex, bodyDigestsByIndex, witnessBytes, cursor));
751
+ leafStart += Math.pow(2, h);
752
+ }
753
+
754
+ if(cursor[0] !== witnessBytes.length) return false; // unconsumed witnesses -- malformed/oversized proof
755
+
756
+ var computedRoot = await rootFromPeaks(reconstructedPeaks);
757
+ return bytesEqual(computedRoot, root);
758
+ }catch(e){
759
+ return false;
760
+ }
761
+ }
762
+
763
+ return {
764
+ DIGEST_LEN: DIGEST_LEN,
765
+ MAX_MMR_SIZE: MAX_MMR_SIZE,
766
+ hexToBytes: hexToBytes,
767
+ bytesToHex: bytesToHex,
768
+ leafHash: leafHash,
769
+ interiorHash: interiorHash,
770
+ rootFromPeaks: rootFromPeaks,
771
+ heightAt: heightAt,
772
+ nodeCount: nodeCount,
773
+ peaks: peaks,
774
+ leafCountFromSize: leafCountFromSize,
775
+ leafIndexToPos: leafIndexToPos,
776
+ verifyInclusion: verifyInclusion,
777
+ verifyConsistency: verifyConsistency,
778
+ verifyRange: verifyRange
779
+ };
780
+ })();
781
+ if(typeof globalThis !== "undefined"){ globalThis.MMR = MMR; }
782
+ </script>
783
+ <script>
784
+ /* === PORTED FROM CAPSULE_JS (verbatim) — see test_bundle_js_shared_helpers_match_capsule_js === */
785
+ var KNOWN_TYPES={"capsule":1,"offer_terms":1,"wicket_manifest":1,"response":1,
786
+ "gate_checks":1,"subject":1,"bilateral_subject":1,"compute_attestation":1,
787
+ "agent_input":1,"agent_output":1};
788
+ function isH64(s){return typeof s==="string"&&s.length===64&&/^[0-9a-f]+$/i.test(s);}
789
+ /* Disclosure Envelope unwrap: {"capsule":{...unmodified...},"disclosures":{...}} ->
790
+ * the unmodified capsule. A bare capsule (no "capsule" wrapper key, or a
791
+ * bilateral binding with buyer_capsule/seller_capsule) passes through unchanged. */
792
+ function unwrapEnvelope(item){return(item&&typeof item==="object"&&item.capsule&&typeof item.capsule==="object")?item.capsule:item;}
793
+ function sh(d){return d.slice(0,8)+"…"+d.slice(-4);}
794
+ function safe(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");}
795
+
796
+ /* ---------- disclosed-payload rendering (shared helper — same canonicalization as
797
+ * the digest recompute; see test_bundle_js_shared_helpers_match_capsule_js). The
798
+ * bytes hashed against the committed digest and the bytes shown to the reader MUST
799
+ * come from the same function -- a display that re-serializes by a different rule
800
+ * than JSON.stringify(p, Object.keys(p).sort()) could show content that doesn't
801
+ * match what was actually verified. */
802
+ var PAYLOAD_TRUNCATE_BYTES=8192;
803
+ function canonicalPayloadText(payload){
804
+ return typeof payload==="string"?payload:JSON.stringify(payload,Object.keys(payload).sort());
805
+ }
806
+ function payloadPreview(payload){
807
+ var t=canonicalPayloadText(payload);
808
+ return t.length>80?t.slice(0,80)+"…":t;
809
+ }
810
+ function payloadCellHtml(entry,recomputedDigest){
811
+ if(entry.withheld||entry._revPayload==null||entry.matchOk!==true)return"";
812
+ var full=typeof entry._revPayload==="string"
813
+ ?entry._revPayload
814
+ :JSON.stringify(entry._revPayload,Object.keys(entry._revPayload).sort(),2);
815
+ var bytes=new TextEncoder().encode(full);
816
+ var truncated=bytes.length>PAYLOAD_TRUNCATE_BYTES;
817
+ var shown=truncated?new TextDecoder("utf-8").decode(bytes.slice(0,PAYLOAD_TRUNCATE_BYTES)):full;
818
+ var note=truncated?"<p class='pl-payload-truncated'>truncated for display, full payload is in the URL fragment</p>":"";
819
+ return"<details class='pl-payload-details'><summary><code>"+safe(payloadPreview(entry._revPayload))+"</code></summary>"
820
+ +"<div class='pl-payload-full'><pre>"+safe(shown)+"</pre>"+note
821
+ +"<div class='pl-payload-digests'><div>committed <code>"+safe(entry.digest)+"</code></div>"
822
+ +"<div>recomputed <code>"+safe(recomputedDigest||"")+"</code></div></div></div></details>";
823
+ }
824
+
825
+ /* === PORTED FROM CAPSULE_JS (verbatim) — capsule_id recompute (RFC 8785 JCS
826
+ * + SHA-256), see test_bundle_js_shared_helpers_match_capsule_js === */
827
+ var CHAIN_LINKAGE_FIELDS={"capsule_id":1,"chain":1};
828
+
829
+ function CapsuleIdError(msg){this.message=msg;this.name="CapsuleIdError";}
830
+
831
+ function _capIdNormalize(v){
832
+ if(Array.isArray(v))return v.map(_capIdNormalize);
833
+ if(v&&typeof v==="object"){
834
+ var out={};
835
+ Object.keys(v).forEach(function(k){
836
+ var nv=_capIdNormalize(v[k]);
837
+ if(nv===null||nv===undefined)return;
838
+ if(Array.isArray(nv)&&nv.length===0)return;
839
+ if(nv&&typeof nv==="object"&&!Array.isArray(nv)&&Object.keys(nv).length===0)return;
840
+ out[k]=nv;
841
+ });
842
+ return out;
843
+ }
844
+ return v;
845
+ }
846
+
847
+ function _capIdJcsString(s){
848
+ var out=['"'];
849
+ for(var ch of s){
850
+ var o=ch.codePointAt(0);
851
+ if(ch==='"')out.push('\\"');
852
+ else if(ch==="\\")out.push("\\\\");
853
+ else if(o===0x08)out.push("\\b");
854
+ else if(o===0x09)out.push("\\t");
855
+ else if(o===0x0A)out.push("\\n");
856
+ else if(o===0x0C)out.push("\\f");
857
+ else if(o===0x0D)out.push("\\r");
858
+ else if(o<0x20)out.push("\\u"+o.toString(16).padStart(4,"0"));
859
+ else out.push(ch);
860
+ }
861
+ out.push('"');
862
+ return out.join("");
863
+ }
864
+
865
+ function _capIdJcsValue(v){
866
+ if(v===null||v===undefined)return"null";
867
+ if(v===true)return"true";
868
+ if(v===false)return"false";
869
+ if(typeof v==="string")return _capIdJcsString(v);
870
+ if(typeof v==="number"){
871
+ if(!Number.isInteger(v))throw new CapsuleIdError("float in digest-bearing field");
872
+ if(v>Number.MAX_SAFE_INTEGER||v<-Number.MAX_SAFE_INTEGER)throw new CapsuleIdError("integer outside safe range");
873
+ return String(v);
874
+ }
875
+ if(Array.isArray(v))return"["+v.map(_capIdJcsValue).join(",")+"]";
876
+ if(typeof v==="object"){
877
+ var keys=Object.keys(v).sort();
878
+ return"{"+keys.map(function(k){return _capIdJcsString(k)+":"+_capIdJcsValue(v[k]);}).join(",")+"}";
879
+ }
880
+ throw new CapsuleIdError("value not JSON-serializable: "+typeof v);
881
+ }
882
+
883
+ async function _capIdSha256Hex(bytes){
884
+ var buf=await crypto.subtle.digest("SHA-256",bytes);
885
+ return Array.from(new Uint8Array(buf)).map(function(b){return b.toString(16).padStart(2,"0");}).join("");
886
+ }
887
+
888
+ async function computeCapsuleId(capsule){
889
+ if(!capsule||typeof capsule!=="object"||Array.isArray(capsule))throw new CapsuleIdError("capsule must be a JSON object");
890
+ var canonical={};
891
+ Object.keys(capsule).forEach(function(k){if(!CHAIN_LINKAGE_FIELDS[k])canonical[k]=capsule[k];});
892
+ var jcsStr=_capIdJcsValue(_capIdNormalize(canonical));
893
+ return await _capIdSha256Hex(new TextEncoder().encode(jcsStr));
894
+ }
895
+
896
+ async function verifyCapsuleId(cap){
897
+ var c=unwrapEnvelope(cap);
898
+ var stated=(c&&c.capsule_id)||"";
899
+ if(!isH64(stated))return{ok:null,stated:stated,recomputed:null};
900
+ if(typeof crypto==="undefined"||!crypto.subtle)return{ok:null,stated:stated,recomputed:null};
901
+ try{
902
+ var recomputed=await computeCapsuleId(c);
903
+ return{ok:recomputed===stated,stated:stated,recomputed:recomputed};
904
+ }catch(ex){
905
+ return{ok:false,stated:stated,recomputed:null,error:ex.message};
906
+ }
907
+ }
908
+
909
+ function parseAac(data){
910
+ var nodes=[],edges=[],privlog=[],unk=[],seen={};
911
+ var isB=!!(data.buyer_capsule&&data.seller_capsule);
912
+
913
+ function addN(id,type,label,withheld,payload){
914
+ if(seen[id])return false;seen[id]=true;
915
+ var k=!!KNOWN_TYPES[type];
916
+ if(!k&&unk.indexOf(type)<0)unk.push(type);
917
+ nodes.push({id:id,type:type,label:label,digest:id,isKnown:k,withheld:withheld!==false,payload:payload||null});
918
+ return true;
919
+ }
920
+ function addArt(digest,type,label,ctx){
921
+ if(!isH64(digest)||!addN(digest,type,label,true,null))return;
922
+ privlog.push({id:label,type:type,digest:digest,withheld:true,isKnown:!!KNOWN_TYPES[type],matchOk:null,ctx:ctx});
923
+ }
924
+ function addEdge(f,t,lbl){
925
+ var k="_e_"+f+"_"+t+"_"+lbl;if(seen[k])return;seen[k]=true;
926
+ edges.push({from:f,to:t,label:lbl});
927
+ }
928
+ /* disclosures: the Disclosure Envelope's out-of-band {agent_input, agent_output}
929
+ * object for this capsule (draft-mih-scitt-agent-action-capsule-disclosure-envelope-00).
930
+ * NEVER read from cap.model_attestation.compute_attestation — that region is
931
+ * digest-committed, so embedding a payload there would change capsule_id. */
932
+ function extractCap(cap,capId,pfx,disclosures){
933
+ disclosures=disclosures||{};
934
+ var p=pfx?pfx+".":"";
935
+ var chain=cap.chain||{};
936
+ var prior=chain.parent_capsule_id||"";
937
+ if(isH64(prior)&&addN(prior,"capsule","prior capsule "+sh(prior),false,null))
938
+ addEdge(capId,prior,"chains_to");
939
+ var ma=cap.model_attestation||{},ca=ma.compute_attestation||{},subj=ca.subject_digest||"";
940
+ if(isH64(subj)){addArt(subj,"subject","subject",p+"compute_attestation.subject_digest");addEdge(capId,subj,"attests_over");}
941
+ var _actxW=p+"compute_attestation — payload not carried in the record";
942
+ var _actxR="payload carried in fragment; recomputed against committed digest";
943
+ var ai=ca.agent_input_digest||"",aiPre=disclosures.agent_input,aiRev=aiPre!=null;
944
+ if(isH64(ai)&&addN(ai,"agent_input","agent input "+sh(ai),!aiRev,aiRev?aiPre:null)){
945
+ privlog.push({id:"agent input",type:"agent_input",digest:ai,withheld:!aiRev,isKnown:true,matchOk:null,
946
+ ctx:aiRev?_actxR:_actxW,_revPayload:aiRev?aiPre:null});addEdge(capId,ai,"attests_over");}
947
+ var ao=ca.agent_output_digest||"",aoPre=disclosures.agent_output,aoRev=aoPre!=null;
948
+ if(isH64(ao)&&addN(ao,"agent_output","agent output "+sh(ao),!aoRev,aoRev?aoPre:null)){
949
+ privlog.push({id:"agent output",type:"agent_output",digest:ao,withheld:!aoRev,isKnown:true,matchOk:null,
950
+ ctx:aoRev?_actxR:_actxW,_revPayload:aoRev?aoPre:null});addEdge(capId,ao,"attests_over");}
951
+ var eff=cap.effect||{},resp=eff.response_digest||"";
952
+ if(isH64(resp)){addArt(resp,"response","response",p+"effect.response_digest");addEdge(capId,resp,"effect_response");}
953
+ (cap.constraints||[]).forEach(function(c){
954
+ var ev=c.evidence_digest||"",cid=c.id||"constraint";
955
+ if(isH64(ev)){addArt(ev,"wicket_manifest","manifest ["+cid+"]",p+"constraints["+cid+"].evidence_digest");addEdge(capId,ev,"commits_to");}
956
+ });
957
+ }
958
+
959
+ if(isB){
960
+ var bc=data.buyer_capsule||{},sc=data.seller_capsule||{};
961
+ var bid=bc.capsule_id||"",sid=sc.capsule_id||"",sth=data.sealed_terms_hash||"",terms=data.terms;
962
+ var bDisc=(data.disclosures&&data.disclosures.buyer)||{},sDisc=(data.disclosures&&data.disclosures.seller)||{};
963
+ if(isH64(bid))addN(bid,"capsule","buyer capsule "+sh(bid),false,null);
964
+ if(isH64(sid))addN(sid,"capsule","seller capsule "+sh(sid),false,null);
965
+ if(isH64(sth)){
966
+ var rev=terms!=null;
967
+ addN(sth,"offer_terms","offer terms "+sh(sth),!rev,rev?terms:null);
968
+ privlog.push({id:"sealed_terms_hash",type:"offer_terms",digest:sth,withheld:!rev,
969
+ isKnown:true,matchOk:null,ctx:"binding.sealed_terms_hash",_revPayload:rev?terms:null});
970
+ if(isH64(bid))addEdge(bid,sth,"attests_over");
971
+ if(isH64(sid))addEdge(sid,sth,"attests_over");
972
+ }
973
+ if(isH64(bid)&&isH64(sid))addEdge(sid,bid,"chains_to");
974
+ if(isH64(bid))extractCap(bc,bid,"buyer",bDisc);
975
+ if(isH64(sid))extractCap(sc,sid,"seller",sDisc);
976
+ }else{
977
+ /* Disclosure Envelope wrapper: {"capsule":{...unmodified...},"disclosures":{...}}.
978
+ * A bare capsule (no "capsule" wrapper key) is the legacy/WITHHELD-only shape —
979
+ * still fully supported, just with no disclosures to read. */
980
+ var envCap=(data.capsule&&typeof data.capsule==="object")?data.capsule:data;
981
+ var envDisc=(data.capsule&&typeof data.capsule==="object")?(data.disclosures||{}):{};
982
+ var cid=envCap.capsule_id||"";
983
+ if(isH64(cid)){addN(cid,"capsule","capsule "+sh(cid),false,null);extractCap(envCap,cid,"",envDisc);}
984
+ }
985
+ return{nodes:nodes,edges:edges,privlog:privlog,unk:unk,isB:isB};
986
+ }
987
+
988
+ function _capMismatched(cap){
989
+ var g=parseAac(cap);
990
+ return g.privlog.some(function(e){return e.matchOk===false;});
991
+ }
992
+
993
+ function findChainGaps(capsules){
994
+ /* Unwrap first: a Disclosure-Envelope-wrapped bundle item carries capsule_id
995
+ * and chain nested under "capsule", not at the top level — without this,
996
+ * every envelope-wrapped item is invisible to gap detection. */
997
+ var caps=capsules.map(unwrapEnvelope);
998
+ var ids={};
999
+ caps.forEach(function(c){if(isH64(c.capsule_id))ids[c.capsule_id]=true;});
1000
+ var gaps=[];
1001
+ for(var i=1;i<caps.length;i++){
1002
+ var parent=((caps[i].chain)||{}).parent_capsule_id||"";
1003
+ if(isH64(parent)&&!ids[parent])gaps.push({beforeIdx:i-1,afterIdx:i,missingParent:parent});
1004
+ }
1005
+ return gaps;
1006
+ }
1007
+
1008
+ function annotateRecords(capsules,integrity){
1009
+ /* Same unwrap as findChainGaps — _capMismatched still receives the raw
1010
+ * (possibly enveloped) item, since parseAac needs the sibling "disclosures"
1011
+ * key to check revealed-payload digests. */
1012
+ var caps=capsules.map(unwrapEnvelope);
1013
+ var alteredIds={};
1014
+ capsules.forEach(function(c,i){
1015
+ var cid=caps[i].capsule_id||"";
1016
+ if(!isH64(cid))return;
1017
+ var idr=integrity&&integrity[i];
1018
+ if((idr&&idr.ok===false)||_capMismatched(c))alteredIds[cid]=true;
1019
+ });
1020
+ var byId={};
1021
+ caps.forEach(function(c){if(isH64(c.capsule_id))byId[c.capsule_id]=c;});
1022
+ return caps.map(function(cap){
1023
+ var cid=cap.capsule_id||"";
1024
+ if(alteredIds[cid])return{note:"digest_mismatch",isAltered:true,citesAltered:false};
1025
+ var cites=false,seen={},cur=cap;
1026
+ while(true){
1027
+ var parent=((cur.chain)||{}).parent_capsule_id||"";
1028
+ if(!isH64(parent)||seen[parent])break;
1029
+ seen[parent]=true;
1030
+ if(alteredIds[parent]){cites=true;break;}
1031
+ cur=byId[parent];if(!cur)break;
1032
+ }
1033
+ return{note:cites?"cites an altered record":"verifies",isAltered:false,citesAltered:cites};
1034
+ });
1035
+ }
1036
+ /* === END PORTED FROM CAPSULE_JS === */
1037
+
1038
+ /* Everything from here down to the render/DOM section is pure (no
1039
+ * document/window access) and directly callable from a Node harness --
1040
+ * see tests/js_harness_bundle.mjs / tests/test_bundle_page.py. */
1041
+
1042
+ /* ---------- base64url fragment codec ----------
1043
+ * capsule-ledger's `capsule bundle` encodes the fragment as
1044
+ * base64.urlsafe_b64encode(json.dumps(bundle, separators=(",",":"), sort_keys=True)).rstrip("=")
1045
+ * -- URL-safe alphabet, no padding. Not the same alphabet plain atob/btoa use,
1046
+ * so this is decoded/encoded explicitly rather than reusing capsule.js's
1047
+ * plain-base64 helpers. */
1048
+ function b64uToStd(s){return s.replace(/-/g,"+").replace(/_/g,"/");}
1049
+ function stdToB64u(s){return s.replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");}
1050
+ function decodeFragment(hash){
1051
+ var std=b64uToStd(hash);
1052
+ var pad=std.length%4; if(pad)std+="=".repeat(4-pad);
1053
+ var bin=atob(std);
1054
+ var bytes=new Uint8Array(bin.length);
1055
+ for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);
1056
+ return JSON.parse(new TextDecoder("utf-8").decode(bytes));
1057
+ }
1058
+ function encodeFragment(obj){
1059
+ var bytes=new TextEncoder().encode(JSON.stringify(obj));
1060
+ var bin="";
1061
+ for(var i=0;i<bytes.length;i++)bin+=String.fromCharCode(bytes[i]);
1062
+ return stdToB64u(btoa(bin));
1063
+ }
1064
+
1065
+ /* ---------- completeness certificate ----------
1066
+ * Optional bundle field this viewer knows how to check (capsule-ledger's
1067
+ * `capsule bundle` does not populate it yet as of this viewer shipping --
1068
+ * a bundle without one is handled honestly as "not available", never a
1069
+ * fabricated pass. Schema, mirroring cll.checkpoint.index.MmrLedger's own
1070
+ * RangeProof/ConsistencyProof shapes 1:1 so capsule-engine's CLI populates
1071
+ * it directly from that module's own output:
1072
+ *
1073
+ * completeness_certificate: {
1074
+ * v: 1,
1075
+ * range_proof: {from_seq, to_seq, size, from_index, to_index, witness: [hex, ...]},
1076
+ * range_root: "<hex>", // MMR root at `range_proof.size` (the tree as it stood right after to_seq)
1077
+ * checkpoint_size: <int>, // MMR node_count(checkpoint.tree_size); omitted/equal to range_proof.size if no growth since
1078
+ * checkpoint_root: "<hex>", // MMR root at checkpoint_size
1079
+ * consistency_proof: <ConsistencyProof> | null // bridges range_root/size -> checkpoint_root/size; null if they coincide
1080
+ * }
1081
+ *
1082
+ * `range_proof` is exactly the JSON shape cll.checkpoint.index.RangeProof
1083
+ * serializes to -- a flat, seq-indexed per-record membership proof (every
1084
+ * leaf in [from_seq, to_seq] participates in rebuilding the root, not just
1085
+ * the two boundary leaves) -- see mmr.js's/this file's verifyRange.
1086
+ * `consistency_proof` is exactly the JSON shape cll.checkpoint.core's
1087
+ * ConsistencyProof dataclass serializes to (v, kind, size_a, size_b,
1088
+ * old_peaks, witness, new_peaks) -- see verifyConsistency. Boundary leaf
1089
+ * body digests are the bundle's own record capsule_ids (hex) -- MmrLedger
1090
+ * indexes leaf i's body_digest as bytes.fromhex(record.capsule_id), so no
1091
+ * extra data is needed beyond what bundle.records already carries. */
1092
+ async function checkCompleteness(bundle){
1093
+ var cc=bundle.completeness_certificate;
1094
+ var records=bundle.records||[];
1095
+ if(!records.length)return{status:"skip",detail:"empty bundle — nothing to certify"};
1096
+ if(!cc){
1097
+ return{status:"skip",detail:"no completeness certificate in this bundle — the claimed record "+
1098
+ "range and checkpoint are producer-asserted only, not cryptographically proven here. "+
1099
+ "Every capsule that IS present still verifies on its own (see Integrity, above)."};
1100
+ }
1101
+ try{
1102
+ var rp=cc.range_proof;
1103
+ if(!rp||!Array.isArray(rp.witness))return{status:"fail",detail:"malformed completeness certificate: missing range_proof"};
1104
+ if(records.length!==rp.to_seq-rp.from_seq+1){
1105
+ return{status:"fail",detail:"bundle holds "+records.length+" records but the range proof claims "+
1106
+ (rp.to_seq-rp.from_seq+1)+" (seq "+rp.from_seq+"–"+rp.to_seq+")"};
1107
+ }
1108
+ var bodyDigests=records.map(function(r){return r.capsule_id;});
1109
+ var okRange=await MMR.verifyRange(cc.range_root,rp.from_seq,rp.to_seq,bodyDigests,rp);
1110
+ if(!okRange){
1111
+ return{status:"fail",detail:"range proof did not verify — this bundle's claimed records "+
1112
+ rp.from_seq+"–"+rp.to_seq+" are not provably present and unaltered under its cited root"};
1113
+ }
1114
+ if(cc.consistency_proof){
1115
+ var okC=await MMR.verifyConsistency(cc.range_root,rp.size,cc.checkpoint_root,cc.checkpoint_size,cc.consistency_proof);
1116
+ if(!okC){
1117
+ return{status:"fail",detail:"checkpoint consistency proof did not verify — the cited checkpoint "+
1118
+ "does not provably extend this range's root"};
1119
+ }
1120
+ }
1121
+ var ckpt=bundle.checkpoint||{};
1122
+ return{status:"pass",detail:"records "+rp.from_seq+"–"+rp.to_seq+" are present, unaltered, and bound to checkpoint "+
1123
+ cc.range_root.slice(0,12)+"…"+(cc.consistency_proof?(" · extends to checkpoint #"+(ckpt.tree_size!=null?ckpt.tree_size:cc.checkpoint_size)):"")+
1124
+ " — this does not show that no other records exist"};
1125
+ }catch(e){
1126
+ return{status:"fail",detail:"completeness certificate malformed or unverifiable: "+e.message};
1127
+ }
1128
+ }
1129
+
1130
+ /* ---------- authoritative digest verification (async — crypto.subtle) ----------
1131
+ * parseAac's privlog entries carry matchOk:null until a REVEALED field's
1132
+ * payload is actually hashed and compared against its committed digest.
1133
+ * This is the single, awaited source of truth for that comparison — both
1134
+ * the displayed privilege log (buildBundlePrivlog) and the verdict logic
1135
+ * (crossCheckSelfReport, evaluateBundleRitual's Integrity stage) call this
1136
+ * rather than each re-deriving (or worse, silently skipping) it. A prior
1137
+ * version of this file computed the digest only inside the DOM-rendering
1138
+ * path — after the ritual/cross-check verdicts had already been decided —
1139
+ * so a genuine digest mismatch could never flip either verdict. Fixed here:
1140
+ * a verification check that can't reject anything isn't a check. */
1141
+ async function verifyCapsuleDigests(cap,disclosures){
1142
+ var g=parseAac(disclosures?{capsule:cap,disclosures:disclosures}:cap);
1143
+ if(typeof crypto==="undefined"||!crypto.subtle)return g; // no WebCrypto: leave matchOk null (skip, not a fabricated pass)
1144
+ for(var i=0;i<g.privlog.length;i++){
1145
+ var e=g.privlog[i];
1146
+ if(e.withheld||e._revPayload==null)continue;
1147
+ var bytes=new TextEncoder().encode(canonicalPayloadText(e._revPayload));
1148
+ var buf=await crypto.subtle.digest("SHA-256",bytes);
1149
+ var hex=Array.from(new Uint8Array(buf)).map(function(b){return b.toString(16).padStart(2,"0");}).join("");
1150
+ e.matchOk=(hex===e.digest);
1151
+ e._recomputedDigest=hex;
1152
+ }
1153
+ return g;
1154
+ }
1155
+
1156
+ /* ---------- cross-check against the bundle's own self-reported verification ----------
1157
+ * capsule-ledger's `capsule bundle` already runs each capsule through its own
1158
+ * structural verifier (agent_action_capsule.verify) and embeds the verdict
1159
+ * in bundle.verification[capsule_id] -- but a recipient should never just
1160
+ * trust a producer's self-report. This cross-checks it against OUR OWN
1161
+ * independent digest recompute (verifyCapsuleDigests, above) and flags any
1162
+ * disagreement as its own finding, rather than silently preferring either
1163
+ * source. */
1164
+ async function crossCheckSelfReport(bundle,records){
1165
+ var selfReport=bundle.verification||{};
1166
+ var bundleDisclosures=bundle.disclosures||{};
1167
+ var disagreements=[];
1168
+ for(var idx=0;idx<records.length;idx++){
1169
+ var cap=records[idx];
1170
+ var cid=cap.capsule_id;
1171
+ if(!isH64(cid))continue;
1172
+ var g=await verifyCapsuleDigests(cap,bundleDisclosures[cid]);
1173
+ var ours=!g.privlog.some(function(e){return e.matchOk===false;});
1174
+ var reported=selfReport[cid];
1175
+ if(reported&&typeof reported.ok==="boolean"&&reported.ok!==ours){
1176
+ disagreements.push({capsule_id:cid,ours:ours,reported:reported.ok});
1177
+ }
1178
+ }
1179
+ if(!disagreements.length){
1180
+ var n=Object.keys(selfReport).length;
1181
+ return{status:"pass",detail:n?("producer self-report agrees with independent recompute for all "+n+" record(s)"):"no producer self-report present; independent recompute is the only signal (see Integrity)"};
1182
+ }
1183
+ return{status:"fail",detail:disagreements.length+" record(s) where the producer's self-reported verdict "+
1184
+ "disagrees with this viewer's own independent recompute — trust the recompute, investigate the bundle"};
1185
+ }
1186
+
1187
+ /* ---------- plain-language summary (renders on EVERY bundle) ----------
1188
+ * The ritual answers "does this verify". It does not answer "what happened",
1189
+ * and until now a clean bundle said almost nothing — four terse stage lines and
1190
+ * no prose. A stranger handed a permalink could see green checks without ever
1191
+ * learning what the records claim, or which claims carry weaker assurance.
1192
+ * This states, in English, what the records say AND what they don't establish.
1193
+ * It never asserts more than the fields carry. */
1194
+ function describeBundle(capsules){
1195
+ var caps=capsules.map(unwrapEnvelope).filter(function(c){return c&&c.capsule_id;});
1196
+ if(!caps.length)return null;
1197
+ var n=caps.length,parts=[];
1198
+
1199
+ var ts=caps.map(function(c){return c.timestamp;}).filter(Boolean).sort();
1200
+ var when=ts.length?(ts[0]===ts[ts.length-1]?("at "+ts[0]):("between "+ts[0]+" and "+ts[ts.length-1])):null;
1201
+ var ops={};caps.forEach(function(c){if(c.operator)ops[c.operator]=1;});
1202
+ var opNames=Object.keys(ops);
1203
+ parts.push(n+" record"+(n===1?"":"s")+(when?", "+when:"")
1204
+ +(opNames.length===1?", from operator “"+opNames[0]+"”":
1205
+ opNames.length>1?", from "+opNames.length+" operators":"")+".");
1206
+
1207
+ var kinds={};caps.forEach(function(c){var t=c.action_type||"unspecified";kinds[t]=(kinds[t]||0)+1;});
1208
+ var kindBits=Object.keys(kinds).map(function(k){
1209
+ var label=k==="fyi"?"informational":k==="decide"?"decision":k==="act"?"action":k;
1210
+ return kinds[k]+" "+label+(kinds[k]===1?"":"s");
1211
+ });
1212
+ if(kindBits.length)parts.push(kindBits.join(", ")+".");
1213
+
1214
+ var accepted=0,rejected=0,human=0;
1215
+ caps.forEach(function(c){
1216
+ var d=c.disposition||{};
1217
+ if(d.decision==="accept")accepted++;
1218
+ else if(d.decision==="reject"||d.decision==="deny")rejected++;
1219
+ if(d.human_disposed===true)human++;
1220
+ });
1221
+ if(rejected)parts.push(rejected+" of "+n+" "+(rejected===1?"was":"were")+" refused.");
1222
+ if(accepted===n&&n>0)parts.push("All were accepted.");
1223
+ parts.push(human===0
1224
+ ? "No human approved any of them — every disposition was made by policy."
1225
+ : human+" of "+n+" carried a recorded human disposition.");
1226
+
1227
+ /* Assurance is the part a reader cannot infer and most needs. */
1228
+ var selfAtt=0,unconfirmed=0;
1229
+ caps.forEach(function(c){
1230
+ var a=c.assurance||{};
1231
+ if(a.attestation_mode==="self_attested")selfAtt++;
1232
+ if(a.effect_mode==="dispatched_unconfirmed")unconfirmed++;
1233
+ });
1234
+ if(unconfirmed)parts.push(unconfirmed+" record"+(unconfirmed===1?"":"s")+" report"+(unconfirmed===1?"s":"")
1235
+ +" the effect as dispatched but unconfirmed — the runtime says it sent the action; nothing here confirms it landed.");
1236
+ if(selfAtt===n&&n>0)parts.push("Every record is self-attested: the same party took the action and wrote the record.");
1237
+
1238
+ var withheld=0;
1239
+ caps.forEach(function(c){
1240
+ var ca=((c.model_attestation||{}).compute_attestation)||{};
1241
+ if(ca.agent_input_digest||ca.agent_output_digest)withheld++;
1242
+ });
1243
+ if(withheld)parts.push("Inputs and outputs are committed as digests only — the payloads are not in "
1244
+ +(withheld===n?"these records":"all of these records")+", so their contents cannot be read here, only matched if someone later discloses them.");
1245
+
1246
+ return{code:"summary",label:"What these records say",text:parts.join(" "),
1247
+ meta:"plain-language summary of the fields carried; it makes no claim the ritual did not check"};
1248
+ }
1249
+
1250
+ /* ---------- ritual: Integrity / Sequence / Completeness / Cross-check ---------- */
1251
+ async function evaluateBundleRitual(records,completeness,crossCheck,integrity,disclosures){
1252
+ var stages=[],finding=null;
1253
+ var alteredIds={},firstMismatch=null,firstMismatchIsBody=false;
1254
+ disclosures=disclosures||{};
1255
+ for(var idx=0;idx<records.length;idx++){
1256
+ var c=records[idx];
1257
+ if(!isH64(c.capsule_id))continue;
1258
+ var idr=integrity&&integrity[idx];
1259
+ if(idr&&idr.ok===false){
1260
+ alteredIds[c.capsule_id]=true;
1261
+ if(!firstMismatch){firstMismatch=idr;firstMismatchIsBody=true;}
1262
+ }
1263
+ var g=await verifyCapsuleDigests(c,disclosures[c.capsule_id]);
1264
+ var bad=g.privlog.filter(function(e){return e.matchOk===false;});
1265
+ if(bad.length){alteredIds[c.capsule_id]=true;if(!firstMismatch){firstMismatch=bad[0];firstMismatchIsBody=false;}}
1266
+ }
1267
+ if(Object.keys(alteredIds).length){
1268
+ if(firstMismatchIsBody){
1269
+ stages.push({name:"Integrity",status:"fail",
1270
+ detail:"capsule_id does not match the recomputed digest of its own body — stated "+sh(firstMismatch.stated)+", recomputed "+(firstMismatch.recomputed?sh(firstMismatch.recomputed):"(could not be computed)")});
1271
+ finding={label:"The finding",
1272
+ text:"This capsule's content does not hash to its stated capsule_id. The body has been altered after the id was assigned — they no longer content-address to each other.",
1273
+ meta:"failed stage: capsule_id_mismatch · stated capsule_id "+firstMismatch.stated+" · recomputed "+(firstMismatch.recomputed||"(error)")};
1274
+ }else{
1275
+ stages.push({name:"Integrity",status:"fail",
1276
+ detail:"record fails at stage digest_mismatch — "+firstMismatch.ctx+" no longer matches its fingerprint"});
1277
+ finding={label:"The finding",
1278
+ text:firstMismatch.id+" ("+firstMismatch.ctx+") is not the value that was sealed.",
1279
+ meta:"failed stage: digest_mismatch · field group: "+firstMismatch.ctx+" · digest "+firstMismatch.digest.slice(0,8)+"…"};
1280
+ }
1281
+ }else{
1282
+ stages.push({name:"Integrity",status:"pass",detail:"every record matches its fingerprint"});
1283
+ }
1284
+
1285
+ /* Same three-valued Sequence as the capsule surface: a bundle where nothing
1286
+ * declares a parent must not render as "unbroken". See CAPSULE_JS. */
1287
+ var _capsForSeq=records.map(unwrapEnvelope);
1288
+ var _declared=0;
1289
+ _capsForSeq.forEach(function(c){
1290
+ if(isH64(((c.chain)||{}).parent_capsule_id||""))_declared++;
1291
+ });
1292
+ var _expectedLinks=Math.max(0,_capsForSeq.length-1);
1293
+
1294
+ var gaps=findChainGaps(records);
1295
+ if(_capsForSeq.length<2){
1296
+ stages.push({name:"Sequence",status:"skip",detail:"single record — nothing to sequence"});
1297
+ }else if(_declared===0){
1298
+ stages.push({name:"Sequence",status:"skip",
1299
+ detail:"not checked — no record here declares a parent, so the order shown is presentation order, not an attested sequence"});
1300
+ }else if(gaps.length){
1301
+ var g0=gaps[0];
1302
+ stages.push({name:"Sequence",status:"fail",
1303
+ detail:"gap between record "+(g0.beforeIdx+1)+" and record "+(g0.afterIdx+1)+" — record "+(g0.afterIdx+1)+" names a parent that is not here"});
1304
+ if(!finding){
1305
+ finding={label:"The finding",
1306
+ text:"Whatever sits between record "+(g0.beforeIdx+1)+" and record "+(g0.afterIdx+1)+" is not in this bundle.",
1307
+ meta:"failed stage: chain_gap · missing parent "+g0.missingParent.slice(0,8)+"…"};
1308
+ }
1309
+ }else if(_declared<_expectedLinks){
1310
+ stages.push({name:"Sequence",status:"skip",
1311
+ detail:"partial — "+_declared+" of "+_expectedLinks+" expected links declared; the undeclared positions are not attested as adjacent"});
1312
+ }else{
1313
+ stages.push({name:"Sequence",status:"pass",detail:"unbroken — every record names the one before it"});
1314
+ }
1315
+
1316
+ stages.push({name:"Completeness",status:completeness.status,detail:completeness.detail});
1317
+ stages.push({name:"Cross-check",status:crossCheck.status,detail:crossCheck.detail});
1318
+
1319
+ return{stages:stages,finding:finding,summary:describeBundle(records)};
1320
+ }
1321
+
1322
+ /* ---------- privilege log (aggregated across every record) ----------
1323
+ * Awaits verifyCapsuleDigests per record first, so every REVEALED row's
1324
+ * matchOk is already resolved true/false by the time this returns — the
1325
+ * displayed log and the ritual/cross-check verdicts read the same, single
1326
+ * digest-verification pass, never two independent (and divergent) ones. */
1327
+ async function buildBundlePrivlog(records,disclosures){
1328
+ var rows=[];
1329
+ disclosures=disclosures||{};
1330
+ for(var i=0;i<records.length;i++){
1331
+ var g=await verifyCapsuleDigests(records[i],disclosures[records[i].capsule_id]);
1332
+ g.privlog.forEach(function(e){
1333
+ rows.push({record_index:i,capsule_id:records[i].capsule_id||"",entry:e});
1334
+ });
1335
+ }
1336
+ return rows;
1337
+ }
1338
+
1339
+ /* ---------- render/DOM section: everything below touches document/window --------- */
1340
+ (function(){"use strict";
1341
+
1342
+ function $(id){return document.getElementById(id);}
1343
+
1344
+ function renderPrivlog(rows){
1345
+ var el=$("privlogContent");if(!el)return;
1346
+ if(!rows.length){el.innerHTML="<p style='color:var(--muted);font-size:13px'>No committed artifacts found across these records.</p>";return;}
1347
+ var h="<table class='pltable'><thead><tr><th>#</th><th>artifact</th><th>type</th><th>digest</th><th>status</th><th>payload</th><th>context</th></tr></thead><tbody>";
1348
+ rows.forEach(function(r,idx){
1349
+ var e=r.entry;
1350
+ var st=e.withheld?"<span class='pl-withheld'>WITHHELD</span>":
1351
+ e.matchOk===true?"<span class='pl-match'>REVEALED · ✓ match</span>":
1352
+ e.matchOk===false?"<span class='pl-mismatch'>REVEALED · ✗ MISMATCH</span>":
1353
+ "<span class='pl-revealed'>REVEALED</span>";
1354
+ h+="<tr data-idx='"+idx+"' data-dig='"+safe(e.digest)+"'><td>"+(r.record_index+1)+"</td><td>"+safe(e.id)+"</td>"+
1355
+ "<td>"+safe(e.type)+(e.isKnown?"":' <em class="opaque-badge">OPAQUE</em>')+"</td>"+
1356
+ "<td><code>"+safe(e.digest.slice(0,16))+"…</code></td><td class='pl-st'>"+st+"</td>"+
1357
+ "<td class='pl-payload'>"+payloadCellHtml(e,e._recomputedDigest)+"</td><td class='pl-ctx'>"+safe(e.ctx)+"</td></tr>";
1358
+ });
1359
+ h+="</tbody></table>";
1360
+ el.innerHTML=h;
1361
+ $("privlogSection").style.display="block";
1362
+ // matchOk is already resolved (buildBundlePrivlog awaits
1363
+ // verifyCapsuleDigests before this is called) -- no second, independent
1364
+ // digest pass here; the table above already reflects the real verdict.
1365
+ }
1366
+
1367
+ function renderRecordsTable(records,integrity){
1368
+ var el=$("recordsTableContent");if(!el)return;
1369
+ var notes=annotateRecords(records,integrity);
1370
+ var gaps=findChainGaps(records),gapAt={};
1371
+ gaps.forEach(function(g){gapAt[g.afterIdx]=g;});
1372
+ var h="<table class='records-table'><thead><tr><th>#</th><th>capsule_id</th><th>action_type</th><th>note</th></tr></thead><tbody>";
1373
+ records.forEach(function(cap,i){
1374
+ if(gapAt[i]){
1375
+ var gp=gapAt[i];
1376
+ h+="<tr class='rec-row rec-gap'><td>—</td><td colspan='2'>gap — missing parent <code>"+safe(gp.missingParent.slice(0,8))+"…</code></td><td>⌗ chain_gap</td></tr>";
1377
+ }
1378
+ var n=notes[i];
1379
+ var noteText=n.note==="digest_mismatch"?"✕ digest_mismatch":
1380
+ n.note==="cites an altered record"?"✓ verifies · cites an altered record":"✓ verifies";
1381
+ h+="<tr class='rec-row"+(n.isAltered?" rec-altered":(n.citesAltered?" rec-flagged":""))+"'>"+
1382
+ "<td>"+(i+1)+"</td><td><code>"+safe((cap.capsule_id||"").slice(0,16))+"…</code></td>"+
1383
+ "<td><code>"+safe(cap.action_type||"")+"</code></td><td>"+noteText+"</td></tr>";
1384
+ });
1385
+ h+="</tbody></table>";
1386
+ el.innerHTML=h;
1387
+ }
1388
+
1389
+ function renderRitual(summary){
1390
+ var mount=$("ritualMount");if(!mount)return;
1391
+ var marks={pass:"✓",fail:"✕",skip:"–"};
1392
+ var h="<div class='ritual-stages'>";
1393
+ summary.stages.forEach(function(s){
1394
+ h+="<div class='ritual-stage ritual-"+s.status+"'><span class='ritual-mark'>"+marks[s.status]+"</span>"+
1395
+ "<span class='ritual-name'>"+safe(s.name)+"</span><span class='ritual-detail'>"+safe(s.detail)+"</span></div>";
1396
+ });
1397
+ h+="</div>";
1398
+ if(summary.summary){
1399
+ var sm=summary.summary;
1400
+ h+="<div class='finding-panel finding-summary'><div class='finding-label'>"+safe(sm.label)+"</div>"+
1401
+ "<p class='finding-text'>"+safe(sm.text)+"</p><div class='finding-meta'>"+safe(sm.meta)+"</div></div>";
1402
+ }
1403
+ if(summary.finding){
1404
+ var f=summary.finding;
1405
+ h+="<div class='finding-panel finding-fail'><div class='finding-label'>"+safe(f.label)+"</div>"+
1406
+ "<p class='finding-text'>"+safe(f.text)+"</p><div class='finding-meta'>"+safe(f.meta)+"</div></div>";
1407
+ }
1408
+ mount.innerHTML=h;
1409
+ }
1410
+
1411
+ function renderCompletenessCard(c){
1412
+ var mount=$("completenessMount");if(!mount)return;
1413
+ var label=c.status==="pass"?"Completeness — verified":c.status==="fail"?"Completeness — FAILED":"Completeness — not available";
1414
+ mount.innerHTML="<div class='completeness-card status-"+c.status+"'><div class='completeness-title'>"+safe(label)+
1415
+ "</div><div class='completeness-detail'>"+safe(c.detail)+"</div></div>";
1416
+ }
1417
+
1418
+ /* ---------- load + permalink + offline download ---------- */
1419
+ var _bundleData=null,_fragmentB64u=null;
1420
+
1421
+ function permalinkBase(){
1422
+ if(location.protocol==="file:"||location.protocol==="blob:")return "https://verify.agentactioncapsule.org/bundle";
1423
+ return location.origin+location.pathname;
1424
+ }
1425
+
1426
+ async function loadBundle(data,fragmentB64u){
1427
+ _bundleData=data;
1428
+ _fragmentB64u=fragmentB64u||encodeFragment(data);
1429
+ var records=data.records||[];
1430
+ var range=data.range||[0,-1];
1431
+ var ckpt=data.checkpoint||{};
1432
+
1433
+ $("emptyState")&&($("emptyState").style.display="none");
1434
+ $("pasteSection")&&($("pasteSection").style.display="none");
1435
+
1436
+ var summaryEl=$("bundleSummary");
1437
+ if(summaryEl){
1438
+ summaryEl.textContent=records.length+" record(s) · range "+range[0]+"–"+range[1]+
1439
+ " · checkpoint #"+(ckpt.tree_size!=null?ckpt.tree_size:"?");
1440
+ }
1441
+ var permalinkEl=$("permalinkText");
1442
+ if(permalinkEl)permalinkEl.textContent=permalinkBase()+"#"+_fragmentB64u.slice(0,24)+"…";
1443
+ var dl=$("downloadBtn");if(dl){dl.disabled=false;dl.style.opacity="1";}
1444
+ var cp=$("copyLinkBtn");if(cp){cp.disabled=false;cp.style.opacity="1";}
1445
+
1446
+ var integrity=await Promise.all(records.map(verifyCapsuleId));
1447
+
1448
+ var privlog=await buildBundlePrivlog(records,data.disclosures);
1449
+ renderPrivlog(privlog);
1450
+ renderRecordsTable(records,integrity);
1451
+
1452
+ var completeness=await checkCompleteness(data);
1453
+ renderCompletenessCard(completeness);
1454
+ var crossCheck=await crossCheckSelfReport(data,records);
1455
+ var ritual=await evaluateBundleRitual(records,completeness,crossCheck,integrity,data.disclosures);
1456
+ renderRitual(ritual);
1457
+
1458
+ try{ history.replaceState(null,"",location.pathname+location.search+"#"+_fragmentB64u); }catch(ex){}
1459
+ }
1460
+
1461
+ function bootstrapLoad(){
1462
+ if(typeof window!=="undefined"&&window.__BUNDLE_FRAGMENT_B64U__&&window.__BUNDLE_FRAGMENT_B64U__!=="@@BUNDLE_FRAGMENT@@"){
1463
+ try{
1464
+ var frag=window.__BUNDLE_FRAGMENT_B64U__;
1465
+ loadBundle(decodeFragment(frag),frag);
1466
+ return;
1467
+ }catch(ex){ $("parseErr")&&($("parseErr").textContent="Embedded bundle decode error: "+ex.message); }
1468
+ }
1469
+ var hash=location.hash.slice(1);
1470
+ if(hash){
1471
+ try{ loadBundle(decodeFragment(hash),hash); return; }
1472
+ catch(ex){ $("parseErr")&&($("parseErr").textContent="Fragment decode error: "+ex.message); }
1473
+ }
1474
+ $("emptyState")&&($("emptyState").style.display="block");
1475
+ }
1476
+
1477
+ $("loadBtn")&&$("loadBtn").addEventListener("click",function(){
1478
+ var txt=$("bundleJson").value.trim();
1479
+ try{ loadBundle(JSON.parse(txt)); }
1480
+ catch(ex){ $("parseErr").textContent="JSON error: "+ex.message; }
1481
+ });
1482
+
1483
+ $("copyLinkBtn")&&$("copyLinkBtn").addEventListener("click",function(){
1484
+ if(!_fragmentB64u||!navigator.clipboard)return;
1485
+ navigator.clipboard.writeText(permalinkBase()+"#"+_fragmentB64u).then(function(){
1486
+ var b=$("copyLinkBtn");var old=b.textContent;b.textContent="Copied!";setTimeout(function(){b.textContent=old;},2000);
1487
+ });
1488
+ });
1489
+
1490
+ $("downloadBtn")&&$("downloadBtn").addEventListener("click",function(){
1491
+ if(!_fragmentB64u)return;
1492
+ fetch("/bundle/offline-shell").then(function(r){return r.text();}).then(function(html){
1493
+ var out=html.replace("@@BUNDLE_FRAGMENT@@",_fragmentB64u);
1494
+ var blob=new Blob([out],{type:"text/html"});
1495
+ var a=document.createElement("a");
1496
+ a.href=URL.createObjectURL(blob);
1497
+ a.download="bundle-viewer.html";
1498
+ document.body.appendChild(a);a.click();document.body.removeChild(a);
1499
+ URL.revokeObjectURL(a.href);
1500
+ }).catch(function(){
1501
+ var b=$("downloadBtn");if(b){var old=b.textContent;b.textContent="Offline copy unavailable";setTimeout(function(){b.textContent=old;},2500);}
1502
+ });
1503
+ });
1504
+
1505
+ bootstrapLoad();
1506
+ })();
1507
+ </script>
1508
+ </body>
1509
+ </html>