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,137 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`capsule console`'s local HTTP server.
3
+
4
+ stdlib `http.server` only -- no new dependency (see pyproject.toml).
5
+ Single-threaded on purpose: `LedgerStore`'s sqlite3 connection is opened
6
+ once at server startup and is not safe to share across request threads, and
7
+ a local, single-operator console has no concurrency requirement that would
8
+ justify the complication. Binds to localhost by default.
9
+
10
+ Serves the static console UI -- this directory's own `tokens.css` /
11
+ `components.css` (the same component-library files the gallery uses, never
12
+ re-invented here) plus `console.css` / `console.js` / `console.html` -- and
13
+ a small JSON API over the real `LedgerAPI` (`api.py` in this package).
14
+
15
+ LOCAL ONLY: no hosted anything, no account system. Every response is
16
+ derived live from the ledger on disk; nothing here calls out to a network.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import shutil
22
+ import tempfile
23
+ import urllib.parse
24
+ from http.server import BaseHTTPRequestHandler, HTTPServer
25
+ from pathlib import Path
26
+
27
+ from cll.ledger.store import LedgerStore
28
+
29
+ from . import api
30
+
31
+ __all__ = ["ConsoleServer", "build_server"]
32
+
33
+ _STATIC_DIR = Path(__file__).resolve().parent
34
+ _STATIC_FILES = {
35
+ "/": ("console.html", "text/html; charset=utf-8"),
36
+ "/console.html": ("console.html", "text/html; charset=utf-8"),
37
+ "/console.css": ("console.css", "text/css; charset=utf-8"),
38
+ "/console.js": ("console.js", "application/javascript; charset=utf-8"),
39
+ "/tokens.css": ("tokens.css", "text/css; charset=utf-8"),
40
+ "/components.css": ("components.css", "text/css; charset=utf-8"),
41
+ }
42
+
43
+
44
+ class ConsoleRequestHandler(BaseHTTPRequestHandler):
45
+ server_version = "CapsuleConsole/0.1"
46
+
47
+ def log_message(self, fmt: str, *args: object) -> None:
48
+ pass # the CLI already echoed the one line that matters at startup
49
+
50
+ def _send_json(self, payload: dict, status: int = 200) -> None:
51
+ body = json.dumps(payload, sort_keys=True).encode("utf-8")
52
+ self.send_response(status)
53
+ self.send_header("Content-Type", "application/json; charset=utf-8")
54
+ self.send_header("Content-Length", str(len(body)))
55
+ self.end_headers()
56
+ self.wfile.write(body)
57
+
58
+ def _send_static(self, filename: str, content_type: str) -> None:
59
+ body = (_STATIC_DIR / filename).read_bytes()
60
+ self.send_response(200)
61
+ self.send_header("Content-Type", content_type)
62
+ self.send_header("Content-Length", str(len(body)))
63
+ self.end_headers()
64
+ self.wfile.write(body)
65
+
66
+ def do_GET(self) -> None: # noqa: N802 (stdlib method name)
67
+ parsed = urllib.parse.urlsplit(self.path)
68
+ path = parsed.path
69
+ query = {k: v[0] for k, v in urllib.parse.parse_qs(parsed.query).items()}
70
+ store: LedgerStore = self.server.store
71
+
72
+ if path in _STATIC_FILES:
73
+ filename, content_type = _STATIC_FILES[path]
74
+ self._send_static(filename, content_type)
75
+ return
76
+
77
+ if path == "/api/checkpoint":
78
+ self._send_json(api.checkpoint_status(store))
79
+ return
80
+
81
+ if path == "/api/records":
82
+ filters = {
83
+ "agent": query.get("agent") or None,
84
+ "since": query.get("since") or None,
85
+ "until": query.get("until") or None,
86
+ "counterparty": query.get("counterparty") or None,
87
+ "verdict": query.get("verdict") or None,
88
+ "action_type": query.get("action_type") or None,
89
+ "limit": int(query["limit"]) if query.get("limit") else None,
90
+ }
91
+ self._send_json(api.list_records(store, filters))
92
+ return
93
+
94
+ if path.startswith("/api/records/"):
95
+ capsule_id = urllib.parse.unquote(path[len("/api/records/") :])
96
+ detail = api.record_detail(store, capsule_id)
97
+ if detail is None:
98
+ self._send_json({"error": f"no such capsule {capsule_id!r}"}, status=404)
99
+ return
100
+ self._send_json(detail)
101
+ return
102
+
103
+ self.send_error(404, "not found")
104
+
105
+
106
+ class ConsoleServer(HTTPServer):
107
+ """Single-threaded (see module docstring). Owns the `LedgerStore` it was
108
+ built with and any throwaway import directory, closing/removing both on
109
+ `server_close()`."""
110
+
111
+ def __init__(self, server_address: tuple[str, int], store: LedgerStore, cleanup_dir: Path | None = None):
112
+ super().__init__(server_address, ConsoleRequestHandler)
113
+ self.store = store
114
+ self._cleanup_dir = cleanup_dir
115
+
116
+ def server_close(self) -> None:
117
+ super().server_close()
118
+ self.store.close()
119
+ if self._cleanup_dir is not None:
120
+ shutil.rmtree(self._cleanup_dir, ignore_errors=True)
121
+
122
+
123
+ def build_server(ledger_path: str, host: str = "127.0.0.1", port: int = 8420) -> ConsoleServer:
124
+ """Open `ledger_path` (a `LedgerStore` directory, or a JSONL fixture
125
+ imported once into a throwaway store -- the same convenience every
126
+ other ledger-backed verb offers via `cli/ledger_io.py`) and bind a
127
+ server to it. Does not start serving -- call `.serve_forever()`."""
128
+ p = Path(ledger_path)
129
+ if p.is_dir():
130
+ store = LedgerStore(p)
131
+ cleanup_dir = None
132
+ else:
133
+ cleanup_dir = Path(tempfile.mkdtemp(prefix="capsule-ledger-console-"))
134
+ store = LedgerStore(cleanup_dir)
135
+ store.import_jsonl(p)
136
+
137
+ return ConsoleServer((host, port), store=store, cleanup_dir=cleanup_dir)
@@ -0,0 +1,68 @@
1
+ /* SPDX-License-Identifier: Apache-2.0
2
+ *
3
+ * Design tokens for the capsule-ledger console, transcribed verbatim from
4
+ * its visual language spec (spec v1 · 2026-08-04, capsule format draft-02).
5
+ * Values are copied from that spec's inline styles, not approximated from a
6
+ * summary.
7
+ *
8
+ * No grays: every neutral in this system is warm (cream/bark/sand ladder).
9
+ * --asg-brick below is reserved for verification failure ONLY -- see
10
+ * components.css and the product-law tests in tests/test_console_*.py.
11
+ */
12
+
13
+ :root {
14
+ /* -- color: surfaces -- */
15
+ --asg-cream: #FAF7F2; /* default page bg (GC/light surfaces) */
16
+ --asg-warm-white: #FDF9F4; /* cards */
17
+ --asg-desk: #EFE9DF; /* page bg behind floating cards */
18
+ --asg-bark: #2C1A0E; /* dark surface, primary text on light */
19
+ --asg-console-bg: #231409; /* dark console page bg */
20
+
21
+ /* -- color: accent -- */
22
+ --asg-amber: #C8611A; /* THE accent: CTAs, eyebrows, model-class marks */
23
+ --asg-amber-light: #E8844A; /* hover, amber-on-dark */
24
+
25
+ /* -- color: sage (deterministic class, refusals, positive signals) -- */
26
+ --asg-sage: #7A8C6E;
27
+ --asg-sage-light: #A8B89C; /* on dark */
28
+
29
+ /* -- color: sand (hairlines/borders) -- */
30
+ --asg-sand: #E8D5B8; /* hairlines/borders on light */
31
+ --asg-sand-dark: #D4BC96;
32
+
33
+ /* -- color: text -- */
34
+ --asg-text-body: #3D2B1F;
35
+ --asg-text-muted: #7A6355; /* muted text (light) */
36
+ --asg-text-dark-1: #E8D5B8; /* text ladder on dark, brightest */
37
+ --asg-text-dark-2: #D4BC96;
38
+ --asg-text-dark-3: #A8927D; /* text ladder on dark, dimmest */
39
+
40
+ /* -- color: brick -- verification failure ONLY. Nothing else may use it. */
41
+ --asg-brick: #A93F2B;
42
+
43
+ /* -- typography -- */
44
+ --asg-font-display: 'Playfair Display', ui-serif, Georgia, serif;
45
+ --asg-font-sans: 'DM Sans', system-ui, -apple-system, sans-serif;
46
+ --asg-font-mono: 'DM Mono', ui-monospace, 'SF Mono', Consolas, monospace;
47
+
48
+ /* eyebrows: DM Sans 500/600, uppercase, letter-spacing 1.5-2.5px, 10-11px */
49
+ --asg-eyebrow-tracking: 2px;
50
+ --asg-eyebrow-size: 11px;
51
+
52
+ /* -- radii -- */
53
+ --asg-radius-pill: 100px;
54
+ --asg-radius-card-sm: 12px;
55
+ --asg-radius-card-lg: 16px;
56
+ --asg-radius-floating: 20px;
57
+ --asg-radius-chip: 6px; /* 4-9px squares for class marks */
58
+
59
+ /* -- shadows: bark-tinted only -- */
60
+ --asg-shadow-floating-dark: 0 24px 64px rgba(44, 26, 14, 0.22);
61
+ --asg-shadow-floating-light: 0 24px 64px rgba(44, 26, 14, 0.10);
62
+ --asg-shadow-amber-button: 0 4px 16px rgba(200, 97, 26, 0.30);
63
+
64
+ /* -- motion -- */
65
+ --asg-ease: cubic-bezier(0.22, 1, 0.36, 1);
66
+ --asg-motion-hover: 200ms;
67
+ --asg-motion-msg-in: 400ms;
68
+ }
@@ -0,0 +1,88 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The interim conversation-capsule profile (pre-VAC).
3
+
4
+ Per-turn ``fyi`` capsules sealed AT TURN
5
+ TIME (speaker role, turn-content digest -- never the content itself),
6
+ chained turn-to-turn so there is no unsigned window between a turn
7
+ happening and the ledger holding a signed record of it, plus one
8
+ session-close capsule binding every turn's digest into a single session
9
+ digest (Merkle-style, via ``capsule_emit.checkpoint`` -- the neutral CLL/MMR
10
+ core capsule-ledger consumes rather than forks) for selective disclosure at
11
+ TURN granularity.
12
+
13
+ This is the interim, ledger-native shape -- it migrates to Verifiable
14
+ Agent Conversations (Birkholz et al., COSE_Sign1 session records) once that
15
+ cross-party coordination lands; the no-unsigned-window property this module
16
+ establishes is itself a candidate contribution back to that draft, which
17
+ today has no equivalent guarantee.
18
+
19
+ Per-speaker judgment targeting (B3, the judge harness) reads
20
+ ``speaker_role`` off these turn capsules -- ``SPEAKER_ROLES`` is the
21
+ complete, closed vocabulary a judge may target.
22
+
23
+ Public surface:
24
+ - ``build_turn_capsule`` / ``build_session_close_capsule`` -- pure capsule
25
+ builders (no ledger I/O), same layering as ``guards.capsule``.
26
+ - ``build_turn_reference_capsule`` -- a typed cross-reference from a turn to
27
+ capsule(s) it gave rise to (e.g. a caller's own tool-call capsules),
28
+ resolved back via ``find_turn_reference``.
29
+ - ``find_session_turns`` / ``find_session_close`` / ``find_turn_reference``
30
+ -- read a session (or a cross-reference) back off any ``LedgerAPI``.
31
+ - ``session_root`` / ``turn_inclusion_proof`` / ``verify_turn_inclusion`` --
32
+ the Merkle session-digest primitives (``.merkle``).
33
+ - ``ConversationSession`` -- the stateful recorder: seals and appends each
34
+ turn immediately, then ``close()``s the session.
35
+ - ``build_conversation_exchange_capsule`` / ``digest_conversation_exchange``
36
+ -- one capsule per exchange, in the mesh inference capsule shape
37
+ (``model_attestation.compute_attestation`` with ``agent_input_digest``/
38
+ ``agent_output_digest`` plus the labeled ``reasoning_digest``/
39
+ ``tool_calls_digest`` sub-digests, ``.exchange``).
40
+ """
41
+ from __future__ import annotations
42
+
43
+ from .capsules import (
44
+ EVENT_CONVERSATION_TURN,
45
+ EVENT_SESSION_CLOSE,
46
+ EVENT_TURN_REFERENCE,
47
+ SPEAKER_ROLES,
48
+ InvalidSpeakerRole,
49
+ build_session_close_capsule,
50
+ build_turn_capsule,
51
+ build_turn_reference_capsule,
52
+ find_session_close,
53
+ find_session_turns,
54
+ find_turn_reference,
55
+ )
56
+ from .exchange import (
57
+ EVENT_CONVERSATION_EXCHANGE,
58
+ build_conversation_exchange_capsule,
59
+ build_usage,
60
+ digest_conversation_exchange,
61
+ stringify_floats,
62
+ )
63
+ from .merkle import session_root, turn_inclusion_proof, verify_turn_inclusion
64
+ from .session import ConversationSession, SessionAlreadyClosedError
65
+
66
+ __all__ = [
67
+ "SPEAKER_ROLES",
68
+ "EVENT_CONVERSATION_TURN",
69
+ "EVENT_SESSION_CLOSE",
70
+ "EVENT_TURN_REFERENCE",
71
+ "EVENT_CONVERSATION_EXCHANGE",
72
+ "InvalidSpeakerRole",
73
+ "build_turn_capsule",
74
+ "build_session_close_capsule",
75
+ "build_turn_reference_capsule",
76
+ "build_conversation_exchange_capsule",
77
+ "digest_conversation_exchange",
78
+ "build_usage",
79
+ "stringify_floats",
80
+ "find_session_turns",
81
+ "find_session_close",
82
+ "find_turn_reference",
83
+ "session_root",
84
+ "turn_inclusion_proof",
85
+ "verify_turn_inclusion",
86
+ "ConversationSession",
87
+ "SessionAlreadyClosedError",
88
+ ]
@@ -0,0 +1,268 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Builders for the conversation-capsule profile's two record types:
3
+ ``conversation_turn`` (one per turn, sealed at turn time) and
4
+ ``conversation_session_close`` (one per session, binding every turn's
5
+ digest into a single session digest).
6
+
7
+ Both are passive ``fyi`` records -- built via ``events.capsule``'s
8
+ ``build_event_capsule``, the same mechanism this codebase already uses for
9
+ degradation/recovery events and policy-manifest activations
10
+ (``policy/activation.py``) -- so a conversation capsule is an ordinary,
11
+ independently verifiable ledger record with no special-cased verification
12
+ path, same as every other capsule this codebase produces.
13
+
14
+ Record-type identity lives in ``asg_payload.event`` (never a repurposed
15
+ spec-defined field, matching the workspace's extension-field rule) --
16
+ ``EVENT_CONVERSATION_TURN`` / ``EVENT_SESSION_CLOSE`` below, discovered via
17
+ ``find_session_turns`` / ``find_session_close``, the same linear-scan-plus-
18
+ filter shape ``policy/activation.py``'s ``find_latest_activation`` uses.
19
+
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from collections.abc import Sequence
24
+
25
+ from agent_action_capsule.contracts import is_hex64
26
+ from cll.ledger.api import LedgerAPI, ScanQuery
27
+ from cll.ledger.records import LedgerRecord
28
+
29
+ from ..events.capsule import build_event_capsule
30
+ from ..guards.signing import Signer
31
+ from .merkle import session_root
32
+
33
+ __all__ = [
34
+ "SPEAKER_ROLES",
35
+ "EVENT_CONVERSATION_TURN",
36
+ "EVENT_SESSION_CLOSE",
37
+ "EVENT_TURN_REFERENCE",
38
+ "InvalidSpeakerRole",
39
+ "build_turn_capsule",
40
+ "build_session_close_capsule",
41
+ "build_turn_reference_capsule",
42
+ "find_session_turns",
43
+ "find_session_close",
44
+ "find_turn_reference",
45
+ ]
46
+
47
+ # Interim speaker-role vocabulary (pre-VAC; migrates when the Birkholz VAC
48
+ # binding lands). Deliberately CLOSED rather than registry-open: B3's
49
+ # per-speaker judgment targeting depends on this exact set, so a typo'd
50
+ # role must fail loud at seal time, not silently land as a fourth,
51
+ # unaddressable role. Widen only alongside a B3 consumer that can target
52
+ # the new value.
53
+ SPEAKER_ROLES = frozenset({"user", "assistant", "human-agent"})
54
+
55
+ EVENT_CONVERSATION_TURN = "conversation_turn"
56
+ EVENT_SESSION_CLOSE = "conversation_session_close"
57
+ EVENT_TURN_REFERENCE = "conversation_turn_reference"
58
+
59
+
60
+ class InvalidSpeakerRole(ValueError):
61
+ """``speaker_role`` is not one of ``SPEAKER_ROLES``."""
62
+
63
+
64
+ def _require_speaker_role(speaker_role: str) -> None:
65
+ if speaker_role not in SPEAKER_ROLES:
66
+ raise InvalidSpeakerRole(f"speaker_role must be one of {sorted(SPEAKER_ROLES)}; got {speaker_role!r}")
67
+
68
+
69
+ def _require_content_digest(content_digest: str) -> None:
70
+ # The turn's own content never enters the record in any mode (H2
71
+ # invariant) -- only a digest of it, same 64-hex-SHA-256 shape every
72
+ # other digest field in this codebase requires.
73
+ if not is_hex64(content_digest):
74
+ raise ValueError(f"content_digest must be a 64-hex SHA-256 digest; got {content_digest!r}")
75
+
76
+
77
+ def build_turn_capsule(
78
+ *,
79
+ session_id: str,
80
+ turn_index: int,
81
+ speaker_role: str,
82
+ content_digest: str,
83
+ operator: str,
84
+ developer: str,
85
+ signer: Signer,
86
+ previous_turn_capsule_id: str | None = None,
87
+ timestamp: str | None = None,
88
+ action_id: str | None = None,
89
+ ) -> dict:
90
+ """Seal one conversation turn as a passive ``fyi`` capsule, AT TURN TIME
91
+ -- every turn is signed and ready to append the moment it is built, so a
92
+ conversation never carries an unsigned window between what was said and
93
+ what the ledger can prove was said.
94
+
95
+ Turn 0 opens the session standalone (no chain parent); every later turn
96
+ chains to ``previous_turn_capsule_id`` with relation ``"follows"`` --
97
+ the same generic sequencing relation capsule-emit's emit path
98
+ (``capsule_emit.core``) defaults to for a non-adapter caller. None of the chain-relation
99
+ registry's seeded values (``confirms``/``supersedes``/``epoch_opens``)
100
+ describe "next turn in an ongoing conversation", so this deliberately
101
+ does not reach for one of them.
102
+ """
103
+ if turn_index < 0:
104
+ raise ValueError(f"turn_index must be >= 0; got {turn_index}")
105
+ if turn_index == 0:
106
+ if previous_turn_capsule_id is not None:
107
+ raise ValueError("turn_index=0 (the session's first turn) must not chain to a previous_turn_capsule_id")
108
+ elif previous_turn_capsule_id is None:
109
+ raise ValueError(f"turn_index={turn_index} requires previous_turn_capsule_id (only turn 0 is standalone)")
110
+
111
+ _require_speaker_role(speaker_role)
112
+ _require_content_digest(content_digest)
113
+
114
+ detail = {
115
+ "session_id": session_id,
116
+ "turn_index": turn_index,
117
+ "speaker_role": speaker_role,
118
+ "content_digest": content_digest,
119
+ }
120
+ return build_event_capsule(
121
+ operator=operator,
122
+ developer=developer,
123
+ signer=signer,
124
+ event=EVENT_CONVERSATION_TURN,
125
+ detail=detail,
126
+ timestamp=timestamp,
127
+ action_id=action_id or f"conversation.turn/{session_id}/{turn_index}",
128
+ chain_parent=previous_turn_capsule_id,
129
+ chain_relation="follows",
130
+ )
131
+
132
+
133
+ def build_session_close_capsule(
134
+ *,
135
+ session_id: str,
136
+ turn_capsule_ids: Sequence[str],
137
+ operator: str,
138
+ developer: str,
139
+ signer: Signer,
140
+ timestamp: str | None = None,
141
+ action_id: str | None = None,
142
+ ) -> dict:
143
+ """Bind every turn in ``turn_capsule_ids`` (append order) into one
144
+ session digest -- an MMR root over the ordered turn capsule ids
145
+ (``merkle.session_root``) -- and seal that binding as one more ``fyi``
146
+ capsule chained to the last turn. This is what makes selective
147
+ disclosure at TURN granularity possible: a holder can later disclose
148
+ one turn's content plus a ``merkle.turn_inclusion_proof`` against this
149
+ capsule's ``session_digest``, without ever revealing the rest of the
150
+ session.
151
+
152
+ Requires at least one turn -- there is no such thing as an empty
153
+ session's completeness certificate.
154
+ """
155
+ if not turn_capsule_ids:
156
+ raise ValueError("turn_capsule_ids must be non-empty -- cannot close a session with zero turns")
157
+
158
+ digest = session_root(turn_capsule_ids)
159
+ detail = {
160
+ "session_id": session_id,
161
+ "turn_count": len(turn_capsule_ids),
162
+ "session_digest": digest,
163
+ "turn_capsule_ids": list(turn_capsule_ids),
164
+ }
165
+ return build_event_capsule(
166
+ operator=operator,
167
+ developer=developer,
168
+ signer=signer,
169
+ event=EVENT_SESSION_CLOSE,
170
+ detail=detail,
171
+ timestamp=timestamp,
172
+ action_id=action_id or f"conversation.session_close/{session_id}",
173
+ chain_parent=turn_capsule_ids[-1],
174
+ chain_relation="follows",
175
+ )
176
+
177
+
178
+ def build_turn_reference_capsule(
179
+ *,
180
+ turn_capsule_id: str,
181
+ referenced_capsule_ids: Sequence[str],
182
+ operator: str,
183
+ developer: str,
184
+ signer: Signer,
185
+ timestamp: str | None = None,
186
+ action_id: str | None = None,
187
+ ) -> dict:
188
+ """A typed cross-reference from one turn to the capsule(s) it gave rise
189
+ to (e.g. tool-call capsules a caller's own pipeline recorded for that
190
+ turn) -- built via the same ``build_event_capsule`` every other passive
191
+ record in this module uses, not a new mechanism.
192
+
193
+ Chains to the turn with relation ``"confirms"`` (the reference only
194
+ makes sense once the turn it names already exists), so a caller that
195
+ already has ``turn_capsule_id`` from ``ConversationSession.record_turn``
196
+ can call this once per turn that produced one or more referenced
197
+ capsules -- letting a caller resolve any of those capsules back to the
198
+ turn that prompted it via ``find_turn_reference``, without requiring the
199
+ referenced capsule itself to carry the link (which is often built and
200
+ appended before the turn is even known -- e.g. a tool call dispatched
201
+ live, while the turn is only reconstructable after the fact from a full
202
+ trajectory).
203
+ """
204
+ if not referenced_capsule_ids:
205
+ raise ValueError("referenced_capsule_ids must be non-empty")
206
+
207
+ detail = {
208
+ "turn_capsule_id": turn_capsule_id,
209
+ "referenced_capsule_ids": list(referenced_capsule_ids),
210
+ }
211
+ return build_event_capsule(
212
+ operator=operator,
213
+ developer=developer,
214
+ signer=signer,
215
+ event=EVENT_TURN_REFERENCE,
216
+ detail=detail,
217
+ timestamp=timestamp,
218
+ action_id=action_id or f"conversation.turn_reference/{turn_capsule_id}",
219
+ chain_parent=turn_capsule_id,
220
+ chain_relation="confirms",
221
+ )
222
+
223
+
224
+ def _matches(record: LedgerRecord, event: str, session_id: str) -> bool:
225
+ payload = record.capsule.get("asg_payload") or {}
226
+ if payload.get("event") != event:
227
+ return False
228
+ return (payload.get("detail") or {}).get("session_id") == session_id
229
+
230
+
231
+ def find_session_turns(ledger: LedgerAPI, session_id: str) -> list[LedgerRecord]:
232
+ """Every turn capsule recorded for ``session_id``, sorted by
233
+ ``turn_index`` (not append order -- defensive against an out-of-order
234
+ replay/import; a live ``ConversationSession`` always appends in order).
235
+ """
236
+ matches = [r for r in ledger.scan(ScanQuery(action_type="fyi")) if _matches(r, EVENT_CONVERSATION_TURN, session_id)]
237
+ matches.sort(key=lambda r: (r.capsule.get("asg_payload") or {}).get("detail", {}).get("turn_index", 0))
238
+ return matches
239
+
240
+
241
+ def find_session_close(ledger: LedgerAPI, session_id: str) -> LedgerRecord | None:
242
+ """The session-close capsule for ``session_id``, or ``None`` if the
243
+ session is still open (or never existed). ``scan()`` is append-ordered
244
+ and a session closes at most once, so the last match wins if more than
245
+ one is ever found (defensive; not an expected shape).
246
+ """
247
+ latest: LedgerRecord | None = None
248
+ for record in ledger.scan(ScanQuery(action_type="fyi")):
249
+ if _matches(record, EVENT_SESSION_CLOSE, session_id):
250
+ latest = record
251
+ return latest
252
+
253
+
254
+ def find_turn_reference(ledger: LedgerAPI, capsule_id: str) -> LedgerRecord | None:
255
+ """The turn-reference capsule (if any) that names ``capsule_id`` among
256
+ its ``referenced_capsule_ids`` -- lets a caller resolve any capsule
257
+ (e.g. a tool-call capsule from an unrelated pipeline) back to the
258
+ conversation turn that gave rise to it. ``None`` if no such reference
259
+ was ever recorded.
260
+ """
261
+ for record in ledger.scan(ScanQuery(action_type="fyi")):
262
+ payload = record.capsule.get("asg_payload") or {}
263
+ if payload.get("event") != EVENT_TURN_REFERENCE:
264
+ continue
265
+ detail = payload.get("detail") or {}
266
+ if capsule_id in (detail.get("referenced_capsule_ids") or []):
267
+ return record
268
+ return None