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,424 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The MCP tool implementations: plain functions returning plain, JSON-serializable
3
+ dicts, each one a thin wrapper over an existing module -- ``LedgerAPI``, the fold
4
+ engine, or the guard engine/checks. No business logic is reimplemented here.
5
+
6
+ Kept independent of ``mcp.server.fastmcp`` (no import of it anywhere in this
7
+ module) so every tool is directly unit-testable with a plain ``LedgerStore`` and
8
+ no MCP session/transport in the loop -- ``server.py`` is the only module that
9
+ knows FastMCP exists.
10
+
11
+ Every read tool returns an ``envelope`` (or, for ``record.verify``, a
12
+ verification result) alongside its answer -- never a bare number or a bare
13
+ yes/no with nothing behind it. This is deliberate: an agent calling these tools
14
+ must never need to fall back to raw ledger rows to check a tool's own claim.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from datetime import datetime, timedelta, timezone
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from cll.ledger.api import LedgerAPI, ScanQuery
23
+
24
+ from ..cli.constraints_cmd import CHECKS, DEFAULT_CAPS_FOLD_ID
25
+ from ..cli.format import summarize_action
26
+ from ..folds.catalog import Catalog
27
+ from ..folds.engine import evaluate_one
28
+ from ..folds.errors import FoldDefinitionError, FoldDeterminismError
29
+ from ..folds.loader import load_definition_file
30
+ from ..guards import Action, GuardEngine
31
+ from ..guards.checks.caps import cap_for, resolve_caps_minor
32
+ from ..guards.checks.dedupe import check_dedupe
33
+ from ..guards.classes import TAXONOMY, UNCLASSIFIED_DEFAULT
34
+
35
+ __all__ = [
36
+ "ledger_query",
37
+ "fold_list",
38
+ "fold_get",
39
+ "budget_remaining",
40
+ "action_been_done",
41
+ "constraints_list",
42
+ "decision_explain",
43
+ "record_get",
44
+ "record_verify",
45
+ "intent_declare",
46
+ ]
47
+
48
+ _DEDUPE_WINDOW_DAYS = 30 # matches GuardEngine's own default (guards/engine.py)
49
+
50
+
51
+ def _error(reason: str, message: str) -> dict[str, Any]:
52
+ return {"error": {"reason": reason, "message": message}}
53
+
54
+
55
+ def _record_dict(record) -> dict[str, Any]:
56
+ return {
57
+ "seq": record.seq,
58
+ "capsule_id": record.capsule_id,
59
+ "capsule": record.capsule,
60
+ "consequential": record.consequential,
61
+ }
62
+
63
+
64
+ def _shift(ts: str, *, days: int) -> str:
65
+ text = ts[:-1] + "+00:00" if ts.endswith("Z") else ts
66
+ dt = datetime.fromisoformat(text) - timedelta(days=days)
67
+ return dt.isoformat().replace("+00:00", "Z")
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # ledger.query
72
+ # ---------------------------------------------------------------------------
73
+
74
+
75
+ def ledger_query(
76
+ ledger: LedgerAPI,
77
+ *,
78
+ agent: str | None = None,
79
+ since: str | None = None,
80
+ until: str | None = None,
81
+ counterparty: str | None = None,
82
+ verdict: str | None = None,
83
+ action_type: str | None = None,
84
+ limit: int | None = None,
85
+ ) -> dict[str, Any]:
86
+ """The same filtered scan `capsule log` runs: every matching record, plus the
87
+ honest total (a filtered view is never mistaken for the whole ledger) and
88
+ the chain-gap count `capsule log`'s own footer reports."""
89
+ total = sum(1 for _ in ledger.scan(ScanQuery()))
90
+ query = ScanQuery(
91
+ agent=agent,
92
+ since=since,
93
+ until=until,
94
+ counterparty=counterparty,
95
+ verdict=verdict,
96
+ action_type=action_type,
97
+ limit=limit,
98
+ )
99
+ records = list(ledger.scan(query))
100
+ gaps = ledger.find_gaps()
101
+ return {
102
+ "records": [_record_dict(r) for r in records],
103
+ "matched": len(records),
104
+ "total": total,
105
+ "range": [records[0].seq, records[-1].seq] if records else [0, -1],
106
+ "checkpoint": {"tree_size": total},
107
+ "staleness": {"checkpoint_age_ms": 0},
108
+ "chain_gaps": len(gaps),
109
+ }
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # fold.list / fold.get
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ def fold_list(catalog_dir: str | Path) -> dict[str, Any]:
118
+ """The fold catalog: every declared fold's id, definition digest, and
119
+ source path, matching `capsule fold list` exactly (same `Catalog`)."""
120
+ catalog = Catalog(catalog_dir)
121
+ entries = catalog.list_entries()
122
+ errors = catalog.list_errors()
123
+ return {
124
+ "folds": [
125
+ {"fold_id": e.definition.fold_id, "digest": e.digest, "source_path": str(e.source_path)}
126
+ for e in entries
127
+ ],
128
+ "errors": [
129
+ {"source_path": str(e.source_path), "reason": e.reason, "message": e.message} for e in errors
130
+ ],
131
+ }
132
+
133
+
134
+ def fold_get(
135
+ ledger: LedgerAPI,
136
+ catalog_dir: str | Path,
137
+ *,
138
+ fold: str,
139
+ key: str | None = None,
140
+ as_of: str | None = None,
141
+ ) -> dict[str, Any]:
142
+ """Evaluate one fold over the current ledger and return its full result
143
+ envelope -- the same replay `capsule fold test` runs, against the live
144
+ ledger instead of a one-off `--ledger` file.
145
+
146
+ ``fold`` is a fold_id, a definition digest, or a path to a definition
147
+ YAML file (checked in that order). Rolling-window folds require
148
+ ``as_of``: this tool, like the fold engine underneath it, refuses to
149
+ invent a time anchor from the wall clock -- supply one derived from real
150
+ data (e.g. the timestamp of the record you care about).
151
+ """
152
+ path = Path(fold)
153
+ try:
154
+ if path.exists():
155
+ definition = load_definition_file(path)
156
+ else:
157
+ entry = Catalog(catalog_dir).get(fold)
158
+ if entry is None:
159
+ return _error("not_found", f"no such fold {fold!r} in catalog {catalog_dir}")
160
+ definition = entry.definition
161
+ except FoldDefinitionError as exc:
162
+ return _error(exc.reason, str(exc))
163
+
164
+ records = [r.capsule for r in ledger.scan(ScanQuery())]
165
+ try:
166
+ trace = evaluate_one(definition, records, key_value=key, as_of=as_of)
167
+ except FoldDeterminismError as exc:
168
+ return _error(exc.reason, str(exc))
169
+
170
+ return {"fold_id": definition.fold_id, "key": key, **trace.to_envelope()}
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # budget.remaining
175
+ # ---------------------------------------------------------------------------
176
+
177
+
178
+ def budget_remaining(
179
+ ledger: LedgerAPI,
180
+ catalog_dir: str | Path,
181
+ caps_minor: dict[str, int],
182
+ *,
183
+ agent: str,
184
+ action_class: str = "money.transfer",
185
+ as_of: str | None = None,
186
+ ) -> dict[str, Any]:
187
+ """How much of ``action_class``'s configured weekly cap ``agent`` has left,
188
+ evaluated the same way the guard's own `caps` check does (T1's
189
+ `spend.weekly` fold, replayed for real -- never a number this tool
190
+ computes by summing raw records itself).
191
+
192
+ If no cap is configured for ``action_class`` (this deployment's own
193
+ `--cap CLASS=MINOR_UNITS` / `$CAPSULE_MCP_CAPS_MINOR`), returns
194
+ ``cap_configured: false`` rather than a fabricated remaining amount --
195
+ matching the caps check's own `n/a` result for an unconfigured class.
196
+ """
197
+ cap_minor = cap_for(resolve_caps_minor(caps_minor), action_class)
198
+ if cap_minor is None:
199
+ return {
200
+ "agent": agent,
201
+ "action_class": action_class,
202
+ "cap_configured": False,
203
+ "reason": "no cap configured for this action class",
204
+ }
205
+
206
+ entry = Catalog(catalog_dir).get(DEFAULT_CAPS_FOLD_ID)
207
+ if entry is None:
208
+ return _error("not_found", f"caps fold {DEFAULT_CAPS_FOLD_ID!r} not found in catalog {catalog_dir}")
209
+
210
+ records = [r.capsule for r in ledger.scan(ScanQuery(agent=agent))]
211
+ resolved_as_of = as_of or datetime.now(timezone.utc).isoformat()
212
+ trace = evaluate_one(entry.definition, records, key_value=agent, as_of=resolved_as_of)
213
+ spent_minor = trace.result or 0
214
+ remaining_minor = cap_minor - spent_minor
215
+
216
+ return {
217
+ "agent": agent,
218
+ "action_class": action_class,
219
+ "cap_configured": True,
220
+ "cap_minor": cap_minor,
221
+ "spent_minor": spent_minor,
222
+ "remaining_minor": remaining_minor,
223
+ "over_cap": remaining_minor < 0,
224
+ "envelope": trace.to_envelope(),
225
+ }
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # action.been_done
230
+ # ---------------------------------------------------------------------------
231
+
232
+
233
+ def action_been_done(
234
+ ledger: LedgerAPI,
235
+ *,
236
+ verb: str,
237
+ operator: str,
238
+ developer: str,
239
+ action_type: str = "decide",
240
+ target: str | None = None,
241
+ equivalence_key: str | None = None,
242
+ since_days: int = _DEDUPE_WINDOW_DAYS,
243
+ timestamp: str | None = None,
244
+ ) -> dict[str, Any]:
245
+ """Has an equivalent action already been recorded? Wraps the guard's own
246
+ `dedupe` check (exact-match equivalence index, see `guards/checks/dedupe.py`)
247
+ -- the same check `GuardEngine.check` runs before dispatch, run here as a
248
+ standalone query rather than as a gate.
249
+ """
250
+ action = Action(
251
+ verb=verb,
252
+ operator=operator,
253
+ developer=developer,
254
+ action_type=action_type,
255
+ target=target,
256
+ equivalence_key=equivalence_key,
257
+ timestamp=timestamp,
258
+ )
259
+ since = _shift(action.resolved_timestamp(), days=since_days)
260
+ outcome = check_dedupe(action, ledger, since=since)
261
+ c = outcome.constraint
262
+ return {
263
+ "verb": verb,
264
+ "operator": operator,
265
+ "developer": developer,
266
+ "action_type": action_type,
267
+ "target": target,
268
+ "been_done": c.result == "fail",
269
+ "reason": c.reason,
270
+ "evidence": c.evidence,
271
+ "checked_window_days": since_days,
272
+ }
273
+
274
+
275
+ # ---------------------------------------------------------------------------
276
+ # constraints.list
277
+ # ---------------------------------------------------------------------------
278
+
279
+
280
+ def constraints_list(catalog_dir: str | Path) -> dict[str, Any]:
281
+ """The registered guard checks and the action-class taxonomy -- the same
282
+ static catalog `capsule constraints list` prints, structured instead of
283
+ formatted for a terminal."""
284
+ caps_entry = Catalog(catalog_dir).get(DEFAULT_CAPS_FOLD_ID)
285
+ caps_method = caps_entry.definition.fold_id if caps_entry is not None else None
286
+
287
+ checks = [
288
+ {
289
+ "id": check_id,
290
+ "type": check_type,
291
+ "method": caps_method if check_id == "caps" else method,
292
+ "description": description,
293
+ }
294
+ for check_id, check_type, method, description in CHECKS
295
+ ]
296
+ action_classes = [
297
+ {"name": ac.name, "consequential": ac.consequential, "fail_open_allowed": ac.fail_open_allowed}
298
+ for ac in (*TAXONOMY.values(), UNCLASSIFIED_DEFAULT)
299
+ ]
300
+ return {"checks": checks, "action_classes": action_classes}
301
+
302
+
303
+ # ---------------------------------------------------------------------------
304
+ # decision.explain / record.get / record.verify
305
+ # ---------------------------------------------------------------------------
306
+
307
+
308
+ def decision_explain(ledger: LedgerAPI, *, capsule_id: str) -> dict[str, Any]:
309
+ """Why a decision came out the way it did: the capsule's own constraints
310
+ breakdown (`id`/`result`/`method` per check that ran) plus its verdict --
311
+ the same information `capsule show` prints, never a separately-generated
312
+ explanation. A capsule's constraints ARE the explanation; there is no
313
+ other reasoning to fetch."""
314
+ record = ledger.fetch(capsule_id)
315
+ if record is None:
316
+ return _error("not_found", f"no such capsule {capsule_id!r}")
317
+
318
+ capsule = record.capsule
319
+ disposition = capsule.get("disposition") or {}
320
+ return {
321
+ "capsule_id": record.capsule_id,
322
+ "agent": capsule.get("developer"),
323
+ "operator": capsule.get("operator"),
324
+ "action": summarize_action(capsule),
325
+ "action_type": capsule.get("action_type"),
326
+ "timestamp": capsule.get("timestamp"),
327
+ "decision": disposition.get("decision"),
328
+ "verdict_class": disposition.get("verdict_class"),
329
+ "constraints": [
330
+ {
331
+ "id": c.get("id"),
332
+ "result": c.get("result"),
333
+ "check_type": c.get("check_type"),
334
+ "method": c.get("method"),
335
+ }
336
+ for c in (capsule.get("constraints") or [])
337
+ ],
338
+ "chain": capsule.get("chain"),
339
+ }
340
+
341
+
342
+ def record_get(ledger: LedgerAPI, *, capsule_id: str) -> dict[str, Any]:
343
+ """The full raw capsule by id or unambiguous prefix -- `capsule show --json`'s
344
+ own output, unchanged."""
345
+ record = ledger.fetch(capsule_id)
346
+ if record is None:
347
+ return _error("not_found", f"no such capsule {capsule_id!r}")
348
+ return _record_dict(record)
349
+
350
+
351
+ def record_verify(ledger: LedgerAPI, *, capsule_id: str) -> dict[str, Any]:
352
+ """Independently re-verify one capsule's digest and chain -- `capsule verify`'s
353
+ own check, run against the live ledger. Returns `ok` plus every finding
354
+ (never just a bare boolean) so a `False` always comes with a reason."""
355
+ record = ledger.fetch(capsule_id)
356
+ if record is None:
357
+ return _error("not_found", f"no such capsule {capsule_id!r}")
358
+ result = ledger.verify(capsule_id)
359
+ if result is None:
360
+ return _error("not_found", f"no such capsule {capsule_id!r}")
361
+ return {
362
+ "capsule_id": record.capsule_id,
363
+ "ok": result.ok,
364
+ "findings": [
365
+ {"code": f.code, "detail": f.detail, "severity": f.severity} for f in result.findings
366
+ ],
367
+ }
368
+
369
+
370
+ # ---------------------------------------------------------------------------
371
+ # intent.declare -- the only write tool
372
+ # ---------------------------------------------------------------------------
373
+
374
+
375
+ def intent_declare(
376
+ ledger: LedgerAPI,
377
+ guard: GuardEngine,
378
+ *,
379
+ verb: str,
380
+ operator: str,
381
+ developer: str,
382
+ action_class: str | None = None,
383
+ amount_minor: int | None = None,
384
+ currency: str | None = None,
385
+ target: str | None = None,
386
+ cited_mandate_capsule_id: str | None = None,
387
+ equivalence_key: str | None = None,
388
+ ) -> dict[str, Any]:
389
+ """Declare an intended action and run it through the guard for a real
390
+ decision -- allow, deny, or escalate. THE ONLY WRITE TOOL on this server:
391
+ every other tool only reads. On anything but an infra-level failure, the
392
+ decision is appended to the ledger as a signed capsule (`GuardEngine.check`,
393
+ same as a live `capsule guard` call would do) -- `capsule_id` in the response
394
+ is directly queryable afterward via `record.get` or `ledger.query`.
395
+
396
+ ``action_class`` is looked up in the starter taxonomy
397
+ (`capsule constraints list` shows it); absent or unrecognized both resolve to
398
+ the consequential, fail-closed default -- there is no silent bypass for
399
+ an unclassified action.
400
+ """
401
+ action = Action(
402
+ verb=verb,
403
+ operator=operator,
404
+ developer=developer,
405
+ action_class=action_class,
406
+ amount_minor=amount_minor,
407
+ currency=currency,
408
+ target=target,
409
+ cited_mandate_capsule_id=cited_mandate_capsule_id,
410
+ equivalence_key=equivalence_key,
411
+ )
412
+ decision = guard.check(action)
413
+ return {
414
+ "outcome": decision.outcome,
415
+ "capsule_id": (decision.capsule or {}).get("capsule_id"),
416
+ "dry_run": decision.dry_run,
417
+ "degraded": decision.degraded,
418
+ "degradation_kind": decision.degradation_kind,
419
+ "reason": decision.reason,
420
+ "checkpoint": decision.checkpoint,
421
+ "constraints": [
422
+ {"id": c.id, "result": c.result, "reason": c.reason} for c in decision.constraints
423
+ ],
424
+ }
@@ -0,0 +1,60 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Packaging
3
+
4
+ One wheel, one codebase. ``guards-only`` keeps the
5
+ caps/dedupe/verify-before-dispatch checks and ``dry_run`` fully functional;
6
+ the evidence machinery underneath (capsules, permalinks, verify surfaces)
7
+ stays present in the code but is not surfaced: no capsule vocabulary in CLI
8
+ output, no share/verify links printed or rendered, and the record-query verbs
9
+ (``log``/``show``/``verify``/``bundle``) are not registered at all. ``full``
10
+ is today's existing behavior, unchanged.
11
+
12
+ ``guards-only`` changes what is shown, never what is recorded:
13
+ every check still seals its record to the ledger;
14
+ the CLI simply doesn't register the record-query verbs or print capsule
15
+ vocabulary and links. Switching to full at any time surfaces the complete
16
+ history recorded all along.
17
+
18
+ Mechanism, and why: an environment variable rather than a pip extra, a
19
+ persistent config file, or a per-invocation CLI flag.
20
+
21
+ * A pip extra (``capsule-ledger[evidence]``) implies a *dependency* difference --
22
+ something extra gets installed. Nothing does: both arms import the exact
23
+ same modules, the only difference is what's registered/rendered. An extra
24
+ would misdescribe what actually changed.
25
+ * A per-invocation flag (``--minimal``/``--full``) has to be remembered on
26
+ every command, which is exactly the kind of friction that would confound
27
+ the very activation/enforcement metrics this packaging split exists to
28
+ measure honestly.
29
+ * A persistent config file needs an init step before first use, which adds
30
+ friction to the "clean install" acceptance test this task is measured
31
+ against.
32
+ * An env var can be set once (in the environment an operator already
33
+ controls for their agent runtime) and then every ``capsule`` invocation in
34
+ that environment picks it up consistently, with zero extra state and zero
35
+ per-command overhead.
36
+ """
37
+ from __future__ import annotations
38
+
39
+ from .envcompat import env_get
40
+
41
+ __all__ = ["GUARDS_ONLY", "FULL", "current_arm", "evidence_visible"]
42
+
43
+ GUARDS_ONLY = "guards-only"
44
+ FULL = "full"
45
+ _VALID = (GUARDS_ONLY, FULL)
46
+ _ENV_VAR = "CAPSULE_LEDGER_ARM"
47
+
48
+
49
+ def current_arm() -> str:
50
+ """The active packaging arm for this process: ``$CAPSULE_LEDGER_ARM``
51
+ if it names a valid arm, else ``full``
52
+ (today's existing, unchanged behavior)."""
53
+ value = env_get(_ENV_VAR, FULL).strip().lower()
54
+ return value if value in _VALID else FULL
55
+
56
+
57
+ def evidence_visible(arm: str | None = None) -> bool:
58
+ """Whether evidence (capsule vocabulary, permalinks, verify surfaces)
59
+ should be surfaced to the user for the given (or current) arm."""
60
+ return (arm or current_arm()) == FULL
@@ -0,0 +1,45 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Starter packs: installable obligation -> deterministic-check -> capsule
3
+ recipes, layered on top of the existing guard/fold/manifest primitives.
4
+
5
+ See the module docstrings in ``schema.py`` (the parsed pack shape),
6
+ ``loader.py`` (``pack.yaml`` -> ``PackDefinition``), ``install.py``
7
+ (``PackDefinition`` -> an installed, manifest-governed ``GuardEngine``), and
8
+ ``pins.py`` (fail-closed registry-pin verification before install) for the
9
+ pieces. ``catalog/`` holds the packs this repo ships (``payments-safety``
10
+ today).
11
+ """
12
+ from .enforce import accept_thresholds, enforce_pack, profile_for_accepted
13
+ from .errors import PackDefinitionError, RegistryPinError
14
+ from .install import InstalledPack, build_engine, install_pack, record_pack_activation
15
+ from .loader import load_pack_dir
16
+ from .obligation_results import ObligationResult, obligation_results
17
+ from .pins import load_pins_file, verify_pins
18
+ from .schema import ActionSemantic, Obligation, PackDefinition, PackFixtures, ProposerStub
19
+ from .thresholds import ThresholdProposal, load_proposals_file, propose_thresholds, write_proposals_file
20
+
21
+ __all__ = [
22
+ "PackDefinition",
23
+ "Obligation",
24
+ "ActionSemantic",
25
+ "ProposerStub",
26
+ "PackFixtures",
27
+ "load_pack_dir",
28
+ "ObligationResult",
29
+ "obligation_results",
30
+ "PackDefinitionError",
31
+ "InstalledPack",
32
+ "install_pack",
33
+ "build_engine",
34
+ "record_pack_activation",
35
+ "load_pins_file",
36
+ "verify_pins",
37
+ "RegistryPinError",
38
+ "ThresholdProposal",
39
+ "propose_thresholds",
40
+ "write_proposals_file",
41
+ "load_proposals_file",
42
+ "accept_thresholds",
43
+ "enforce_pack",
44
+ "profile_for_accepted",
45
+ ]