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,266 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Builders for the four hold-lifecycle record types: ``hold.reserve``,
3
+ ``hold.release``, ``hold.expire``, ``hold.reconcile``.
4
+
5
+ A reservation is a capsule, not side-band state (design premise shared by
6
+ capsule-emit #51/#52/#53): reserve/release/expire/reconcile are signed,
7
+ appended records built the same way ``guards/capsule.py``'s
8
+ ``build_decision_capsule`` and ``events/capsule.py``'s ``build_event_capsule``
9
+ build every other capsule this codebase produces -- same ``Capsule``/``Disposition``/``Chain``/
10
+ ``AssuranceBlock`` primitives, same digest-then-sign-then-reseal sequence, so
11
+ these records are ordinary, independently verifiable ledger capsules with no
12
+ special-cased verification path.
13
+
14
+ Record-type identity lives in the ``action_id`` verb (``hold.reserve/<uuid>``
15
+ etc.), the same place every other capsule in this codebase carries its verb
16
+ (``guards/checks/dedupe.py``'s ``_capsule_verb``) -- not a new top-level
17
+ field, and not the spec's closed ``action_type`` enum (-02 §5.1 restricts
18
+ ``action_type`` to ``fyi``/``decide``; these are ``decide``, since a
19
+ reservation is a consequential decision about exposure).
20
+
21
+ Every numeric field here is integer minor units, MUST-FAIL on float --
22
+ mirrors ``folds/reducers.py``'s own discipline for the same reason: these
23
+ values flow into a fold's ``sum`` reducer (``hold.active_exposure``,
24
+ ``folds/catalog_defs/hold.active_exposure.yaml``), which cannot reproduce a
25
+ float byte-exactly across implementations.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from typing import Any
30
+
31
+ from agent_action_capsule import (
32
+ DEFAULT_FORMAT_VERSION,
33
+ AssuranceBlock,
34
+ Capsule,
35
+ Chain,
36
+ Disposition,
37
+ compute_capsule_id,
38
+ json_digest,
39
+ )
40
+ from agent_action_capsule.canonical import CANONICALIZATION_JCS
41
+
42
+ from ..guards.action import Action
43
+ from ..guards.signing import Signer
44
+ from .errors import FLOAT_IN_HOLD_AMOUNT, NON_INTEGER_HOLD_AMOUNT, HoldError
45
+
46
+ __all__ = [
47
+ "build_hold_reserve_capsule",
48
+ "build_hold_release_capsule",
49
+ "build_hold_expire_capsule",
50
+ "build_hold_reconcile_capsule",
51
+ ]
52
+
53
+ _SPEC_VERSION = "draft-mih-scitt-agent-action-capsule-02"
54
+ _FORMAT_VERSION = DEFAULT_FORMAT_VERSION
55
+
56
+ # The only registered chain.relation this task uses: "supersedes" for a
57
+ # terminal transition that closes/replaces the reserve's open state
58
+ # (release, expiry, a successful reconcile), matching the existing
59
+ # EUR150k-bridge precedent (`guards/capsule.py`'s own docstring; the
60
+ # registry's own definition: "resolution, expiry, escalation close/replace
61
+ # the parent's open state").
62
+ SUPERSEDES = "supersedes"
63
+
64
+
65
+ def check_integer_amount(value: Any, field: str) -> int:
66
+ """Amounts are integer minor units -- floats MUST-FAIL (same discipline
67
+ as ``folds/reducers.py._check_integer``, applied at record-build time so
68
+ a bad amount fails loudly here with a named reason, not deep inside JCS
69
+ canonicalization when the capsule is later digested)."""
70
+ if isinstance(value, bool):
71
+ raise HoldError(NON_INTEGER_HOLD_AMOUNT, f"{field!r} is a bool, not an integer amount")
72
+ if isinstance(value, float):
73
+ raise HoldError(FLOAT_IN_HOLD_AMOUNT, f"{field!r} carries a float ({value!r}); amounts MUST be integer minor units")
74
+ if not isinstance(value, int):
75
+ raise HoldError(NON_INTEGER_HOLD_AMOUNT, f"{field!r} is {type(value).__name__}, not an integer")
76
+ return value
77
+
78
+
79
+ def _hold_action(action: Action, verb: str) -> Action:
80
+ """A fresh ``Action`` carrying this hold record's own verb/action_id,
81
+ otherwise identical to the action this hold is evaluated for."""
82
+ return Action(
83
+ verb=verb,
84
+ operator=action.operator,
85
+ developer=action.developer,
86
+ action_class=action.action_class,
87
+ action_type="decide",
88
+ timestamp=action.resolved_timestamp(),
89
+ amount_minor=action.amount_minor,
90
+ currency=action.currency,
91
+ target=action.target,
92
+ )
93
+
94
+
95
+ def _seal(body: dict, signer: Signer) -> dict:
96
+ """Sign the pre-signature canonical body, commit the signature into the
97
+ body, then compute capsule_id over everything (identical sequence to
98
+ ``guards/capsule.py``'s two builders -- see that module's own comment on
99
+ why the signature is committed pre-digest)."""
100
+ presig_digest = json_digest(body)
101
+ body["asg_signature"] = {
102
+ "key_id": signer.key_id,
103
+ "alg": signer.algorithm,
104
+ "sig": signer.sign(presig_digest),
105
+ }
106
+ capsule_id = compute_capsule_id(body)
107
+ sealed = {"spec_version": body["spec_version"], "format_version": body["format_version"], "capsule_id": capsule_id}
108
+ for k, v in body.items():
109
+ if k not in sealed:
110
+ sealed[k] = v
111
+ return sealed
112
+
113
+
114
+ def _build(*, hold_action: Action, chain: Chain | None, asg_payload: dict, signer: Signer) -> dict:
115
+ disposition = Disposition(decision="accept", approver="policy", human_disposed=False, verdict_class=None)
116
+ capsule_obj = Capsule(
117
+ spec_version=_SPEC_VERSION,
118
+ format_version=_FORMAT_VERSION,
119
+ canonicalization_id=CANONICALIZATION_JCS,
120
+ action_id=hold_action.resolved_action_id(),
121
+ action_type="decide",
122
+ operator=hold_action.operator,
123
+ developer=hold_action.developer,
124
+ timestamp=hold_action.resolved_timestamp(),
125
+ assurance=AssuranceBlock(
126
+ attestation_mode="self_attested",
127
+ effect_mode="not_applicable",
128
+ ledger_mode="chained" if chain is not None else "standalone",
129
+ ),
130
+ disposition=disposition,
131
+ chain=chain,
132
+ )
133
+ body = capsule_obj.to_dict()
134
+ body["asg_payload"] = asg_payload
135
+ return _seal(body, signer)
136
+
137
+
138
+ def build_hold_reserve_capsule(
139
+ *,
140
+ action: Action,
141
+ reserved_amount_minor: int,
142
+ fold_id: str,
143
+ fold_digest: str,
144
+ fold_envelope: dict,
145
+ checkpoint: dict,
146
+ signer: Signer,
147
+ manifest_digest: str | None = None,
148
+ ) -> dict:
149
+ """Reserve-at-seal (#51.1): cites the evaluation's own fold envelope
150
+ (fold digest, record range, checkpoint) and the reserved amount. No
151
+ ``chain`` -- a fresh reservation opens a new hold, standalone; release/
152
+ expiry/reconcile are what chain back to *this* capsule's id."""
153
+ check_integer_amount(reserved_amount_minor, "reserved_amount_minor")
154
+ hold_action = _hold_action(action, "hold.reserve")
155
+ asg_payload: dict[str, Any] = {
156
+ "amount_minor": reserved_amount_minor,
157
+ "reserved_amount_minor": reserved_amount_minor,
158
+ "hold_scope": {"fold_id": fold_id, "fold_digest": fold_digest, "subject": action.developer},
159
+ "fold_envelope": fold_envelope,
160
+ "checkpoint": checkpoint,
161
+ }
162
+ if action.currency is not None:
163
+ asg_payload["currency"] = action.currency
164
+ if action.target is not None:
165
+ asg_payload["target"] = action.target
166
+ if manifest_digest is not None:
167
+ asg_payload["manifest_digest"] = manifest_digest
168
+ return _build(hold_action=hold_action, chain=None, asg_payload=asg_payload, signer=signer)
169
+
170
+
171
+ def build_hold_release_capsule(
172
+ *,
173
+ action: Action,
174
+ reserve_capsule_id: str,
175
+ reserved_amount_minor: int,
176
+ signer: Signer,
177
+ reason: str | None = None,
178
+ ) -> dict:
179
+ """Voluntary cancellation of a still-active hold. Terminal: the reserve's
180
+ exposure fully unwinds (``amount_minor`` is the negative of the reserved
181
+ amount, so the ``hold.active_exposure`` fold nets back to zero for this
182
+ hold)."""
183
+ check_integer_amount(reserved_amount_minor, "reserved_amount_minor")
184
+ hold_action = _hold_action(action, "hold.release")
185
+ chain = Chain(parent_capsule_id=reserve_capsule_id, relation=SUPERSEDES)
186
+ asg_payload: dict[str, Any] = {
187
+ "amount_minor": -reserved_amount_minor,
188
+ "released_amount_minor": reserved_amount_minor,
189
+ }
190
+ if reason is not None:
191
+ asg_payload["reason"] = reason
192
+ return _build(hold_action=hold_action, chain=chain, asg_payload=asg_payload, signer=signer)
193
+
194
+
195
+ def build_hold_expire_capsule(
196
+ *,
197
+ action: Action,
198
+ reserve_capsule_id: str,
199
+ reserved_amount_minor: int,
200
+ signer: Signer,
201
+ reason: str | None = None,
202
+ ) -> dict:
203
+ """Expiry (#52.1): TERMINAL for this hold -- after this capsule exists,
204
+ nothing may dispatch citing the original reservation. Same net-to-zero
205
+ exposure unwind as release; the two are distinguished by verb (a caller
206
+ choosing to cancel vs. a TTL/policy elapsing), not by any different
207
+ fold-visible effect."""
208
+ check_integer_amount(reserved_amount_minor, "reserved_amount_minor")
209
+ hold_action = _hold_action(action, "hold.expire")
210
+ chain = Chain(parent_capsule_id=reserve_capsule_id, relation=SUPERSEDES)
211
+ asg_payload: dict[str, Any] = {
212
+ "amount_minor": -reserved_amount_minor,
213
+ "expired_amount_minor": reserved_amount_minor,
214
+ }
215
+ if reason is not None:
216
+ asg_payload["reason"] = reason
217
+ return _build(hold_action=hold_action, chain=chain, asg_payload=asg_payload, signer=signer)
218
+
219
+
220
+ def build_hold_reconcile_capsule(
221
+ *,
222
+ action: Action,
223
+ reserve_capsule_id: str,
224
+ execution_capsule_id: str | None,
225
+ reserved_amount_minor: int,
226
+ executed_amount_minor: int,
227
+ tolerance_minor: int,
228
+ signer: Signer,
229
+ manifest_digest: str | None = None,
230
+ ) -> dict:
231
+ """Planned vs. executed (#53.1): reserve at planned amount, convert at
232
+ executed amount, the delta sealed as this record -- chained to the
233
+ reserve capsule via ``chain`` (the schema's single-parent link) and to
234
+ the execution capsule via the ``asg_payload.execution_capsule_id``
235
+ citation (a plain payload reference, not a registry relation -- the
236
+ schema has only one ``chain`` slot per capsule).
237
+
238
+ ``amount_minor`` is the *delta* (``executed - reserved``): summed against
239
+ the reserve's own ``+reserved_amount_minor`` contribution, the
240
+ ``hold.active_exposure`` fold nets to exactly ``executed_amount_minor``
241
+ once this record lands -- "executed once reconciled" (#53.4) falls out
242
+ of the fold algebra, not a special case in the fold definition.
243
+
244
+ Only called for an in-tolerance conversion; an over-tolerance conversion
245
+ routes through the existing cap-exceeded ALLOW/DENY/ESCALATE vocabulary
246
+ instead (``holds/engine.py``) and this capsule is never built for it --
247
+ "never silently adjusts the aggregate" (#53.3).
248
+ """
249
+ check_integer_amount(reserved_amount_minor, "reserved_amount_minor")
250
+ check_integer_amount(executed_amount_minor, "executed_amount_minor")
251
+ check_integer_amount(tolerance_minor, "tolerance_minor")
252
+ delta_minor = executed_amount_minor - reserved_amount_minor
253
+ hold_action = _hold_action(action, "hold.reconcile")
254
+ chain = Chain(parent_capsule_id=reserve_capsule_id, relation=SUPERSEDES)
255
+ asg_payload: dict[str, Any] = {
256
+ "amount_minor": delta_minor,
257
+ "reserved_amount_minor": reserved_amount_minor,
258
+ "executed_amount_minor": executed_amount_minor,
259
+ "delta_minor": delta_minor,
260
+ "tolerance_minor": tolerance_minor,
261
+ }
262
+ if execution_capsule_id is not None:
263
+ asg_payload["execution_capsule_id"] = execution_capsule_id
264
+ if manifest_digest is not None:
265
+ asg_payload["manifest_digest"] = manifest_digest
266
+ return _build(hold_action=hold_action, chain=chain, asg_payload=asg_payload, signer=signer)