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,214 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """promise_requires_approval check: a class of statement the agent may make
3
+ only inside its task authority or with a one-shot approval.
4
+
5
+ Reads ``Action.representation_class``, one member of the closed set
6
+ ``representation_classes``. A class outside ``requires_approval`` passes. A
7
+ class inside it passes on either of two bases, named in the evidence's
8
+ ``authority_basis``:
9
+
10
+ - ``task_authority_ref``: the task authority already allows it. The sealed
11
+ task-authority record is read only when the engine's own digest of it
12
+ equals ``Action.task_authority_ref``
13
+ (``task_authority.bind_task_authority_record``), and its
14
+ ``body.authorized_representation_classes`` (``AUTHORIZED_PATH``) lists the
15
+ class. No approval event is needed. From 1.1.0 the wicket config names
16
+ the path (``authorized_path``): capsulectl's task authority seals the
17
+ classes the user ordered at ``body.ordered_representations``, and the
18
+ list counts only when every member is a class in
19
+ ``representation_classes``; a malformed list allows nothing. 1.0.0 names
20
+ none and reads ``AUTHORIZED_PATH`` as it always did.
21
+ - ``authorized_by``: a bound approval for a statement outside the task
22
+ authority. ``Action.authorized_by`` is the SHA-256 digest of the WHOLE
23
+ sealed approval record, that record is supplied beside the action for one
24
+ decision (``GuardEngine.check(..., authorization_record=...)``), the
25
+ engine recomputes its digest itself (``authorization_record_digest``) and
26
+ reads it ONLY when that equals the reference, and the record's
27
+ ``body.representation_class`` (``CLASS_PATH``) names this action's class.
28
+
29
+ Anything less fails: no reference, no record, a record the reference does
30
+ not bind, a record with no digest, or an approval for another class. Never
31
+ ``n/a``: an unreadable approval is no approval. Under 1.1.0 a failure on an
32
+ action that cites no task authority (an x-deal-v0 deal has none) reads
33
+ "no order or approval of yours covers this promise"; 1.0.0 keeps its
34
+ own reasons for that failure. Both name a class after its article ("an
35
+ authenticity statement"). A class no approval may
36
+ clear belongs to ``promise_never``, not here.
37
+
38
+ A class outside the closed set fails closed. Applies only to the configured
39
+ ``action_classes``.
40
+ """
41
+ from __future__ import annotations
42
+
43
+ from typing import TypedDict
44
+
45
+ from agent_action_capsule import json_digest
46
+ from agent_action_capsule.canonical import FloatInDigestError, UnsafeIntegerError
47
+
48
+ from ..action import Action
49
+ from ..capsule import ConstraintOutcome, NotApplicableEvidence, not_applicable_evidence
50
+ from .base import CheckOutcome
51
+ from .promise_never import with_article
52
+ from .required_disclosure import require_known_classes
53
+ from .task_authority import TaskAuthorityRecord, UnboundRecord, bind_task_authority_record
54
+
55
+ __all__ = [
56
+ "AUTHORIZED_PATH",
57
+ "CLASS_PATH",
58
+ "AuthorizationBody",
59
+ "AuthorizationRecord",
60
+ "authorization_record_digest",
61
+ "check_promise_requires_approval",
62
+ ]
63
+
64
+ _CHECK_ID = "promise_requires_approval"
65
+ _METHOD = "authority_or_bound_approval_v0"
66
+
67
+ # Where the approved class sits inside the sealed approval record, one member
68
+ # name per level.
69
+ CLASS_PATH: tuple[str, ...] = ("body", "representation_class")
70
+ # Where the classes the task authority allows sit inside the sealed
71
+ # task-authority record.
72
+ AUTHORIZED_PATH: tuple[str, ...] = ("body", "authorized_representation_classes")
73
+
74
+
75
+ class AuthorizationBody(TypedDict):
76
+ representation_class: str
77
+
78
+
79
+ class AuthorizationRecord(TypedDict):
80
+ """The whole sealed approval record as decoded JSON, digested
81
+ byte-for-byte as supplied. Only the approved class is named here; any
82
+ other member the producer seals is covered by the digest and otherwise
83
+ unread."""
84
+
85
+ body: AuthorizationBody
86
+
87
+
88
+ class PromiseEvidence(TypedDict):
89
+ representation_class: str
90
+ recognised: bool
91
+ requires_approval: bool
92
+ authority_basis: str | None
93
+ task_authority_ref: str | None
94
+ authorized_by: str | None
95
+
96
+
97
+ def authorization_record_digest(record: AuthorizationRecord) -> str:
98
+ """The digest ``authorized_by`` carries for ``record``: SHA-256 over the
99
+ record's JCS bytes."""
100
+ return json_digest(record)
101
+
102
+
103
+ class _Unapproved(Exception):
104
+ """The supplied approval does not approve this statement; the message says why."""
105
+
106
+
107
+ def _approved_class(action: Action, record: AuthorizationRecord | None) -> str:
108
+ """The class the bound approval names. Raises ``_Unapproved`` when the
109
+ record is missing, unbound or names no class."""
110
+ ref = action.authorized_by
111
+ if ref is None:
112
+ raise _Unapproved("the action cites no approval (authorized_by)")
113
+ if record is None:
114
+ raise _Unapproved("no approval record was supplied for authorized_by")
115
+ try:
116
+ bound = authorization_record_digest(record) == ref
117
+ except (FloatInDigestError, UnsafeIntegerError) as exc:
118
+ raise _Unapproved(f"the supplied approval record has no digest ({exc})") from exc
119
+ if not bound:
120
+ raise _Unapproved("ref mismatch: the supplied approval record is not the record authorized_by names")
121
+ node: object = record
122
+ for name in CLASS_PATH:
123
+ # Decoded JSON: each level is checked before it is read.
124
+ if not isinstance(node, dict) or name not in node:
125
+ raise _Unapproved(f"the approval record names no class at {'.'.join(CLASS_PATH)}")
126
+ node = node[name]
127
+ if not isinstance(node, str):
128
+ raise _Unapproved(f"the approval record's {'.'.join(CLASS_PATH)} is not one class")
129
+ return node
130
+
131
+
132
+ def _authority_allows(action: Action, record: TaskAuthorityRecord | None, cls: str,
133
+ path: tuple[str, ...], known: list[str] | None = None) -> bool:
134
+ """Whether the bound task authority lists ``cls`` at ``path``. An unbound
135
+ or silent record allows nothing; the approval is then the only basis
136
+ left. With ``known`` (1.1.0), the list counts only when every member is
137
+ one of those class names: a list holding anything else is malformed and
138
+ allows nothing."""
139
+ try:
140
+ _, bound = bind_task_authority_record(action, record)
141
+ except UnboundRecord:
142
+ # No bound authority is not an error here: the approval is then the
143
+ # only basis, and a failure's reason names what the approval lacked.
144
+ return False
145
+ node: object = bound
146
+ for name in path:
147
+ # Decoded JSON: each level is checked before it is read.
148
+ if not isinstance(node, dict) or name not in node:
149
+ return False
150
+ node = node[name]
151
+ if not isinstance(node, list):
152
+ return False
153
+ if known is not None and not all(isinstance(c, str) and c in known for c in node):
154
+ return False
155
+ return cls in node
156
+
157
+
158
+ def _outcome(result: str, reason: str, evidence: PromiseEvidence | NotApplicableEvidence) -> CheckOutcome:
159
+ return CheckOutcome(
160
+ constraint=ConstraintOutcome(
161
+ id=_CHECK_ID, result=result, reason=reason, evidence=evidence, check_type="policy", method=_METHOD
162
+ )
163
+ )
164
+
165
+
166
+ def check_promise_requires_approval(
167
+ action: Action,
168
+ task_authority_record: TaskAuthorityRecord | None,
169
+ authorization_record: AuthorizationRecord | None,
170
+ *,
171
+ representation_classes: list[str],
172
+ requires_approval: list[str],
173
+ action_classes: list[str],
174
+ authorized_path: list[str] | None = None,
175
+ ) -> CheckOutcome:
176
+ require_known_classes(requires_approval, representation_classes, what="requires_approval")
177
+ if action.action_class not in action_classes:
178
+ return _outcome("n/a", "the rule is not configured for this action class",
179
+ not_applicable_evidence(_CHECK_ID, in_scope=False))
180
+ cls = action.representation_class
181
+ if cls is None:
182
+ return _outcome("n/a", "the action carries no representation_class; the statement could not be classed",
183
+ not_applicable_evidence(_CHECK_ID, in_scope=True, missing_field="representation_class"))
184
+ recognised = cls in representation_classes
185
+ needs = cls in requires_approval
186
+
187
+ def evidence(basis: str | None) -> PromiseEvidence:
188
+ return PromiseEvidence(representation_class=cls, recognised=recognised, requires_approval=needs,
189
+ authority_basis=basis, task_authority_ref=action.task_authority_ref,
190
+ authorized_by=action.authorized_by)
191
+
192
+ if not recognised:
193
+ return _outcome("fail", f"representation_class {cls!r} is not in {representation_classes}", evidence(None))
194
+ if not needs:
195
+ return _outcome("pass", f"{with_article(cls)} statement needs no approval", evidence(None))
196
+ path = AUTHORIZED_PATH if authorized_path is None else tuple(authorized_path)
197
+ known = representation_classes if authorized_path is not None else None
198
+ if _authority_allows(action, task_authority_record, cls, path, known):
199
+ return _outcome("pass", f"the task authority allows {with_article(cls)} statement", evidence("task_authority_ref"))
200
+ try:
201
+ approved = _approved_class(action, authorization_record)
202
+ except _Unapproved as exc:
203
+ why = str(exc)
204
+ else:
205
+ if approved == cls:
206
+ return _outcome("pass", f"the {cls} statement cites a bound approval for {cls}",
207
+ evidence("authorized_by"))
208
+ why = f"the approval is for {approved}, not {cls}"
209
+ reason = f"{with_article(cls)} statement is outside the task authority and needs a bound approval: {why}"
210
+ if authorized_path is not None and action.task_authority_ref is None:
211
+ # What the user's card says (design note capsule-engine#194 §2): the
212
+ # evidence still records the class and what was cited.
213
+ reason = "no order or approval of yours covers this promise"
214
+ return _outcome("fail", reason, evidence(None))
@@ -0,0 +1,117 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """recipient_role check: membership of the disclosure recipient's role.
3
+
4
+ Reads ``Action.recipient_role``, one member of the closed set ``roles``
5
+ the wicket pins (``fulfilling_merchant``, ``third_party``, ``self``; a
6
+ seller's set is ``buyer``, ``third_party``, ``self``), and passes when it is
7
+ one of ``allowed_roles``. A role outside ``roles`` fails closed and the
8
+ evidence says it was not recognised. It records the role, never who the
9
+ recipient is. Applies only to the configured ``action_classes``.
10
+
11
+ ``role_classes`` (optional) names roles that pass only for some classes of
12
+ data: ``{platform: [email]}`` passes a disclosure to a platform when every
13
+ class in ``Action.disclosed_classes`` is ``email``. One that gives any other
14
+ class fails, and so does one that names no class, since it cannot be shown
15
+ to give only those. A wicket that sets it records the classes it read and the
16
+ roles' classes in the evidence; one that does not keeps the evidence it had.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from typing import TypedDict
21
+
22
+ from ..action import Action
23
+ from ..capsule import ConstraintOutcome, NotApplicableEvidence, not_applicable_evidence
24
+ from .base import CheckOutcome
25
+
26
+ __all__ = ["check_recipient_role"]
27
+
28
+ _CHECK_ID = "recipient_role"
29
+ _METHOD = "set_membership_v0"
30
+
31
+
32
+ class RoleEvidence(TypedDict):
33
+ recipient_role: str
34
+ recognised: bool
35
+ allowed_roles: list[str]
36
+
37
+
38
+ class RoleClassesEvidence(RoleEvidence):
39
+ role_classes: dict[str, list[str]]
40
+ disclosed_classes: list[str] | None
41
+
42
+
43
+ def _outcome(
44
+ result: str, reason: str, evidence: RoleEvidence | RoleClassesEvidence | NotApplicableEvidence
45
+ ) -> CheckOutcome:
46
+ return CheckOutcome(
47
+ constraint=ConstraintOutcome(
48
+ id=_CHECK_ID, result=result, reason=reason, evidence=evidence, check_type="policy", method=_METHOD
49
+ )
50
+ )
51
+
52
+
53
+ def _either(words: list[str]) -> str:
54
+ """``a``, ``a or b``, ``a, b or c``; ``no role`` for none."""
55
+ if not words:
56
+ return "no role"
57
+ return words[0] if len(words) == 1 else f"{', '.join(words[:-1])} or {words[-1]}"
58
+
59
+
60
+ def _passing(allowed: list[str], role_classes: dict[str, list[str]]) -> str:
61
+ """The roles that pass, once: ``fulfilling_merchant or self, or a
62
+ platform given only email``."""
63
+ limited = [f"a {role} given only {_either(classes)}" for role, classes in sorted(role_classes.items())]
64
+ if not allowed:
65
+ return _either(limited)
66
+ return _either(allowed) + "".join(f", or {phrase}" for phrase in limited)
67
+
68
+
69
+ def check_recipient_role(
70
+ action: Action,
71
+ *,
72
+ roles: list[str],
73
+ allowed_roles: list[str],
74
+ action_classes: list[str],
75
+ role_classes: dict[str, list[str]] | None = None,
76
+ ) -> CheckOutcome:
77
+ limited = {role: sorted(classes) for role, classes in (role_classes or {}).items()}
78
+ misplaced = sorted(set(limited) - set(roles)) + sorted(set(limited) & set(allowed_roles))
79
+ if misplaced or any(not classes for classes in limited.values()):
80
+ raise ValueError(
81
+ "role_classes must name roles in roles and not in allowed_roles, each with at least one class; "
82
+ f"got {sorted(limited)}"
83
+ )
84
+ if action.action_class not in action_classes:
85
+ return _outcome("n/a", "the rule is not configured for this action class",
86
+ not_applicable_evidence(_CHECK_ID, in_scope=False))
87
+ role = action.recipient_role
88
+ if role is None:
89
+ return _outcome("n/a", "the action carries no recipient_role; the recipient's role could not be checked",
90
+ not_applicable_evidence(_CHECK_ID, in_scope=True, missing_field="recipient_role"))
91
+ recognised = role in roles
92
+ allowed = sorted(allowed_roles)
93
+ evidence: RoleEvidence | RoleClassesEvidence = RoleEvidence(
94
+ recipient_role=role, recognised=recognised, allowed_roles=allowed
95
+ )
96
+ if role_classes is not None:
97
+ disclosed = list(action.disclosed_classes) if action.disclosed_classes is not None else None
98
+ evidence = RoleClassesEvidence(**evidence, role_classes=limited, disclosed_classes=disclosed)
99
+ passing = _passing(allowed, limited)
100
+ # A role outside the closed set fails even if ``allowed_roles`` names it.
101
+ if not recognised:
102
+ return _outcome("fail", f"recipient_role {role!r} is not a role this rule knows; personal data "
103
+ f"passes only to {passing}", evidence)
104
+ if role in allowed:
105
+ return _outcome("pass", f"the recipient is the {role}", evidence)
106
+ if role in limited:
107
+ classes = limited[role]
108
+ if action.disclosed_classes is None:
109
+ return _outcome("fail", f"the recipient is a {role}, which may be given only {_either(classes)}; "
110
+ "the disclosure names no class of data", evidence)
111
+ others = [c for c in action.disclosed_classes if c not in classes]
112
+ if others:
113
+ return _outcome("fail", f"the recipient is a {role}, which may be given only {_either(classes)}; "
114
+ f"this disclosure also gives {_either(others)}", evidence)
115
+ return _outcome("pass", f"the recipient is a {role}, given only {_either(list(action.disclosed_classes))}",
116
+ evidence)
117
+ return _outcome("fail", f"the recipient is a {role}; personal data passes only to {passing}", evidence)
@@ -0,0 +1,49 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """recurring_charge check: a read of the action's declared recurrence.
3
+
4
+ Fails when ``Action.recurrence`` is present and is not one of the configured
5
+ ``one_time_values`` -- that is, when the payment sets up a charge that
6
+ repeats. Applies only to the configured ``action_classes``.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import TypedDict
11
+
12
+ from ..action import Action
13
+ from ..capsule import ConstraintOutcome, NotApplicableEvidence, not_applicable_evidence
14
+ from .base import CheckOutcome
15
+
16
+ __all__ = ["check_recurring_charge"]
17
+
18
+ _CHECK_ID = "recurring_charge"
19
+ _METHOD = "structured_field_v0"
20
+
21
+
22
+ class RecurrenceEvidence(TypedDict):
23
+ recurrence: str
24
+ one_time_values: list[str]
25
+
26
+
27
+ def _outcome(result: str, reason: str, evidence: NotApplicableEvidence | RecurrenceEvidence) -> CheckOutcome:
28
+ return CheckOutcome(
29
+ constraint=ConstraintOutcome(
30
+ id=_CHECK_ID, result=result, reason=reason, evidence=evidence, check_type="policy", method=_METHOD
31
+ )
32
+ )
33
+
34
+
35
+ def check_recurring_charge(action: Action, *, one_time_values: list[str], action_classes: list[str]) -> CheckOutcome:
36
+ if action.action_class not in action_classes:
37
+ return _outcome(
38
+ "n/a", "the rule is not configured for this action class", not_applicable_evidence(_CHECK_ID, in_scope=False)
39
+ )
40
+ if action.recurrence is None:
41
+ return _outcome(
42
+ "n/a",
43
+ "the action carries no recurrence; whether it repeats could not be read",
44
+ not_applicable_evidence(_CHECK_ID, in_scope=True, missing_field="recurrence"),
45
+ )
46
+ evidence = RecurrenceEvidence(recurrence=action.recurrence, one_time_values=sorted(one_time_values))
47
+ if action.recurrence in one_time_values:
48
+ return _outcome("pass", f"recurrence {action.recurrence!r} is a one-time payment", evidence)
49
+ return _outcome("fail", f"recurrence {action.recurrence!r} sets up a repeating charge", evidence)
@@ -0,0 +1,46 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """refundability check: can the payment be refunded?
3
+
4
+ Reads ``Action.refundable`` and fails when it is ``False``. It records the
5
+ value it read and the rail beside it (``None`` when the action names none);
6
+ it does not read the refund terms themselves. Applies only to the configured
7
+ ``action_classes``.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import TypedDict
12
+
13
+ from ..action import Action
14
+ from ..capsule import ConstraintOutcome, NotApplicableEvidence, not_applicable_evidence
15
+ from .base import CheckOutcome
16
+
17
+ __all__ = ["check_refundability"]
18
+
19
+ _CHECK_ID = "refundability"
20
+ _METHOD = "declared_boolean_v0"
21
+
22
+
23
+ class RefundabilityEvidence(TypedDict):
24
+ refundable: bool
25
+ rail: str | None
26
+
27
+
28
+ def _outcome(result: str, reason: str, evidence: RefundabilityEvidence | NotApplicableEvidence) -> CheckOutcome:
29
+ return CheckOutcome(
30
+ constraint=ConstraintOutcome(
31
+ id=_CHECK_ID, result=result, reason=reason, evidence=evidence, check_type="policy", method=_METHOD
32
+ )
33
+ )
34
+
35
+
36
+ def check_refundability(action: Action, *, action_classes: list[str]) -> CheckOutcome:
37
+ if action.action_class not in action_classes:
38
+ return _outcome("n/a", "the rule is not configured for this action class",
39
+ not_applicable_evidence(_CHECK_ID, in_scope=False))
40
+ if action.refundable is None:
41
+ return _outcome("n/a", "the action carries no refundable; whether it can be undone could not be checked",
42
+ not_applicable_evidence(_CHECK_ID, in_scope=True, missing_field="refundable"))
43
+ evidence = RefundabilityEvidence(refundable=action.refundable, rail=action.rail)
44
+ if action.refundable:
45
+ return _outcome("pass", "the payment is refundable", evidence)
46
+ return _outcome("fail", "the payment is not refundable", evidence)