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,393 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Action-class taxonomy (gating decisions doc §1), the classification
3
+ default, and the measurement mapping.
4
+
5
+ The taxonomy is one versioned data table, ``action_taxonomy.json``. Each row
6
+ is a canonical action class and declares, explicitly and with no defaults:
7
+ its trigger class, ``consequential``, ``fail_open_allowed``,
8
+ ``approver_role`` and its legacy aliases. ``load_taxonomy_table`` rejects a
9
+ row missing any of them, so a new action cannot silently inherit a policy.
10
+
11
+ Two axes, two jobs:
12
+
13
+ - **Gating** (``classify``): an action with no declared class -- or one not
14
+ in this taxonomy -- is CONSEQUENTIAL, fail-closed. This is deliberate: it
15
+ is the loophole every other rule in the failure-semantics table would
16
+ otherwise escape through.
17
+ - **Measurement** (``trigger_class``): the canonical action classes collapse
18
+ many-to-one onto a small set of trigger classes, which are the population
19
+ a "should this action have been checked" count is taken over. A trigger
20
+ class is derived from the action class, never authored per record. An
21
+ unknown name raises ``UnmappedActionClass`` here instead of degrading,
22
+ because the failure this guards against is an action counting toward no
23
+ population at all.
24
+
25
+ ``CHANGE`` is a trigger class no row may declare: it is a transition after a
26
+ prior approval, so it is derived from state by the caller
27
+ (``after_prior_approval=True``), not from the action's name.
28
+
29
+ Sealed records are never rewritten. A legacy class name already sealed into
30
+ a record resolves through ``legacy_aliases`` to its canonical row on read.
31
+ A decision capsule carries ``taxonomy_version`` in ``asg_payload`` only when
32
+ its ``Action.taxonomy_version`` was set (``guards/capsule.py``); nothing in
33
+ this package sets it by default yet, so existing records keep their bytes. A
34
+ record without one is read as ``unversioned_records_read_as``.
35
+
36
+ The engine evaluates under the current table only, except that a replay of
37
+ historical records evaluates each under the table it was sealed with
38
+ (``carried_taxonomy``). Earlier tables are carried byte-identical to what
39
+ shipped, as ``action_taxonomy_v<N>.json``, for every version a sealed record
40
+ can name; version 1 never had a table (it is what an unversioned record is
41
+ read as), so it is not carried.
42
+
43
+ Caps config resolves through this table (``checks.caps.resolve_caps_minor``),
44
+ so a cap keyed by a legacy name and one keyed by its canonical name are one
45
+ cap. Tolerances and fail-open opt-ins are still keyed by the raw
46
+ ``action_class`` string a caller sends.
47
+ """
48
+ from __future__ import annotations
49
+
50
+ import json
51
+ from collections.abc import Mapping
52
+ from dataclasses import dataclass
53
+ from importlib import resources
54
+ from types import MappingProxyType
55
+ from typing import TypedDict
56
+
57
+ from agent_action_capsule import json_digest
58
+
59
+ __all__ = [
60
+ "ActionClass",
61
+ "CARRIED_TAXONOMY_VERSIONS",
62
+ "ENGINE_TAXONOMY",
63
+ "TAXONOMY",
64
+ "TAXONOMY_DIGEST",
65
+ "TAXONOMY_VERSION",
66
+ "TRIGGER_CLASSES",
67
+ "UNCLASSIFIED_DEFAULT",
68
+ "UNVERSIONED_TAXONOMY_VERSION",
69
+ "TaxonomyDocument",
70
+ "TaxonomyRow",
71
+ "TaxonomyTable",
72
+ "TaxonomyTableError",
73
+ "UnmappedActionClass",
74
+ "VersionedPayload",
75
+ "carried_taxonomy",
76
+ "check_connector_declaration",
77
+ "classify",
78
+ "is_known_action_class",
79
+ "legacy_aliases",
80
+ "load_taxonomy_table",
81
+ "record_taxonomy_version",
82
+ "resolve",
83
+ "same_classes",
84
+ "trigger_class",
85
+ ]
86
+
87
+ _TABLE_FILE = "action_taxonomy.json"
88
+ # Earlier tables, as shipped: "<version>" -> file. The current one is _TABLE_FILE.
89
+ _EARLIER_TABLE_FILES = {v: f"action_taxonomy_v{v}.json" for v in ("2", "3", "4", "5")}
90
+ _ROW_KEYS = frozenset(
91
+ {"name", "trigger_class", "consequential", "fail_open_allowed", "approver_role", "legacy_aliases"}
92
+ )
93
+ _TOP_KEYS = frozenset(
94
+ {"taxonomy_version", "unversioned_records_read_as", "trigger_classes", "state_derived_trigger_classes", "actions"}
95
+ )
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class ActionClass:
100
+ name: str
101
+ consequential: bool
102
+ # Whether this class MAY be configured to fail open on a stale view or an
103
+ # unreachable engine (gating doc §1: "fail-open permitted for low-risk
104
+ # classes when explicitly configured"). This never makes fail-open a
105
+ # default by itself -- a caller must still opt in per-class on the engine.
106
+ fail_open_allowed: bool = False
107
+ # The role that can resolve a cap-exceeded hold on this class via the
108
+ # HITL bridge (D2, 2026-08-05). None means no approver is configured for
109
+ # this class -- a cap-exceeded action in it hard-denies rather than
110
+ # escalating; this is the "classes explicitly marked deny" half of D2.
111
+ approver_role: str | None = None
112
+ # The measurement population this class counts toward. None only for a
113
+ # non-consequential class (outside every population) and for
114
+ # UNCLASSIFIED_DEFAULT (which ``trigger_class`` refuses to count).
115
+ trigger_class: str | None = None
116
+ legacy_aliases: tuple[str, ...] = ()
117
+
118
+
119
+ class TaxonomyRow(TypedDict):
120
+ """One row of ``action_taxonomy.json``, as authored."""
121
+
122
+ name: str
123
+ trigger_class: str | None
124
+ consequential: bool
125
+ fail_open_allowed: bool
126
+ approver_role: str | None
127
+ legacy_aliases: list[str]
128
+
129
+
130
+ class TaxonomyDocument(TypedDict):
131
+ """``action_taxonomy.json``, as authored."""
132
+
133
+ taxonomy_version: str
134
+ unversioned_records_read_as: str
135
+ trigger_classes: list[str]
136
+ state_derived_trigger_classes: list[str]
137
+ actions: list[TaxonomyRow]
138
+
139
+
140
+ class VersionedPayload(TypedDict, total=False):
141
+ """The part of a decision capsule's ``asg_payload`` this module reads."""
142
+
143
+ action_class: str
144
+ taxonomy_version: str
145
+
146
+
147
+ class TaxonomyTableError(ValueError):
148
+ """The taxonomy table is malformed or under-declared."""
149
+
150
+
151
+ class UnmappedActionClass(LookupError):
152
+ """A name has no row in the taxonomy, so it belongs to no population."""
153
+
154
+
155
+ @dataclass(frozen=True)
156
+ class TaxonomyTable:
157
+ version: str
158
+ unversioned_read_as: str
159
+ trigger_classes: tuple[str, ...]
160
+ classes: Mapping[str, ActionClass]
161
+ aliases: Mapping[str, str]
162
+
163
+ def resolve(self, action_class: str) -> ActionClass | None:
164
+ """The canonical row for a canonical or legacy name, or None."""
165
+ return self.classes.get(self.aliases.get(action_class, action_class))
166
+
167
+ def classify(self, action_class: str | None) -> ActionClass:
168
+ """``classify`` under this table."""
169
+ if action_class is None:
170
+ return UNCLASSIFIED_DEFAULT
171
+ return self.resolve(action_class) or UNCLASSIFIED_DEFAULT
172
+
173
+
174
+ def _require_bool(value: object, where: str) -> bool:
175
+ if not isinstance(value, bool):
176
+ raise TaxonomyTableError(f"{where} must be true or false, got {value!r}")
177
+ return value
178
+
179
+
180
+ def _require_optional_str(value: object, where: str) -> str | None:
181
+ if value is None:
182
+ return None
183
+ return _require_str(value, f"{where} (or null)")
184
+
185
+
186
+ def _require_str(value: object, where: str) -> str:
187
+ if not isinstance(value, str) or not value:
188
+ raise TaxonomyTableError(f"{where} must be a non-empty string, got {value!r}")
189
+ return value
190
+
191
+
192
+ def _require_str_list(value: object, where: str) -> tuple[str, ...]:
193
+ if not isinstance(value, list):
194
+ raise TaxonomyTableError(f"{where} must be a list, got {value!r}")
195
+ return tuple(_require_str(item, f"{where}[{i}]") for i, item in enumerate(value))
196
+
197
+
198
+ def _parse_row(row: object, idx: int, declarable: frozenset[str]) -> ActionClass:
199
+ where = f"actions[{idx}]"
200
+ if not isinstance(row, dict):
201
+ raise TaxonomyTableError(f"{where} must be an object")
202
+ missing = _ROW_KEYS - row.keys()
203
+ if missing:
204
+ raise TaxonomyTableError(f"{where} does not declare {sorted(missing)}; every row declares every key")
205
+ extra = row.keys() - _ROW_KEYS
206
+ if extra:
207
+ raise TaxonomyTableError(f"{where} carries unknown keys {sorted(extra)}")
208
+ name = _require_str(row["name"], f"{where}.name")
209
+ where = f"actions[{name!r}]"
210
+ consequential = _require_bool(row["consequential"], f"{where}.consequential")
211
+ fail_open_allowed = _require_bool(row["fail_open_allowed"], f"{where}.fail_open_allowed")
212
+ approver_role = _require_optional_str(row["approver_role"], f"{where}.approver_role")
213
+ trigger = _require_optional_str(row["trigger_class"], f"{where}.trigger_class")
214
+ if consequential and fail_open_allowed:
215
+ raise TaxonomyTableError(f"{where} is consequential and sets fail_open_allowed; only a non-consequential class may")
216
+ if consequential and trigger is None:
217
+ raise TaxonomyTableError(f"{where} is consequential and has no trigger_class")
218
+ if not consequential and trigger is not None:
219
+ raise TaxonomyTableError(f"{where} is non-consequential but declares trigger_class {trigger!r}")
220
+ if trigger is not None and trigger not in declarable:
221
+ raise TaxonomyTableError(f"{where}.trigger_class={trigger!r} is not a declarable trigger class {sorted(declarable)}")
222
+ return ActionClass(
223
+ name=name,
224
+ consequential=consequential,
225
+ fail_open_allowed=fail_open_allowed,
226
+ approver_role=approver_role,
227
+ trigger_class=trigger,
228
+ legacy_aliases=_require_str_list(row["legacy_aliases"], f"{where}.legacy_aliases"),
229
+ )
230
+
231
+
232
+ def load_taxonomy_table(raw: object) -> TaxonomyTable:
233
+ """Validate a parsed taxonomy table (a ``TaxonomyDocument`` once it passes). Raises ``TaxonomyTableError`` on any
234
+ under-declared row, duplicate name, or alias collision."""
235
+ if not isinstance(raw, dict):
236
+ raise TaxonomyTableError("taxonomy table must be an object")
237
+ missing = _TOP_KEYS - raw.keys()
238
+ if missing:
239
+ raise TaxonomyTableError(f"taxonomy table does not declare {sorted(missing)}")
240
+ version = _require_str(raw["taxonomy_version"], "taxonomy_version")
241
+ unversioned = _require_str(raw["unversioned_records_read_as"], "unversioned_records_read_as")
242
+ triggers = _require_str_list(raw["trigger_classes"], "trigger_classes")
243
+ derived = _require_str_list(raw["state_derived_trigger_classes"], "state_derived_trigger_classes")
244
+ if not set(derived) <= set(triggers):
245
+ raise TaxonomyTableError(f"state_derived_trigger_classes {list(derived)} must be trigger classes")
246
+ declarable = frozenset(triggers) - frozenset(derived)
247
+ rows = raw["actions"]
248
+ if not isinstance(rows, list) or not rows:
249
+ raise TaxonomyTableError("actions must be a non-empty list")
250
+
251
+ classes: dict[str, ActionClass] = {}
252
+ for idx, row in enumerate(rows):
253
+ ac = _parse_row(row, idx, declarable)
254
+ if ac.name in classes:
255
+ raise TaxonomyTableError(f"action class {ac.name!r} declared more than once")
256
+ classes[ac.name] = ac
257
+ aliases: dict[str, str] = {}
258
+ for ac in classes.values():
259
+ for alias in ac.legacy_aliases:
260
+ if alias in classes or alias in aliases:
261
+ raise TaxonomyTableError(f"legacy alias {alias!r} on {ac.name!r} collides with another name")
262
+ aliases[alias] = ac.name
263
+ if not any(not ac.consequential for ac in classes.values()):
264
+ raise TaxonomyTableError("taxonomy has no non-consequential class; the negative set would vanish")
265
+ return TaxonomyTable(
266
+ version=version,
267
+ unversioned_read_as=unversioned,
268
+ trigger_classes=triggers,
269
+ classes=MappingProxyType(classes),
270
+ aliases=MappingProxyType(aliases),
271
+ )
272
+
273
+
274
+ def _load_packaged(name: str = _TABLE_FILE) -> tuple[TaxonomyTable, str]:
275
+ """The packaged table ``name`` and the SHA-256 over its JCS bytes."""
276
+ raw = json.loads(resources.files("capsule_engine.guards").joinpath(name).read_text(encoding="utf-8"))
277
+ return load_taxonomy_table(raw), json_digest(raw)
278
+
279
+
280
+ def _load_earlier() -> dict[str, TaxonomyTable]:
281
+ tables: dict[str, TaxonomyTable] = {}
282
+ for version, name in _EARLIER_TABLE_FILES.items():
283
+ table, _ = _load_packaged(name)
284
+ if table.version != version:
285
+ raise TaxonomyTableError(f"{name} declares taxonomy_version {table.version!r}, not {version!r}")
286
+ tables[version] = table
287
+ return tables
288
+
289
+
290
+ _TABLE, _DIGEST = _load_packaged()
291
+ _CARRIED: Mapping[str, TaxonomyTable] = MappingProxyType({**_load_earlier(), _TABLE.version: _TABLE})
292
+
293
+ # The table this engine evaluates live decisions under.
294
+ ENGINE_TAXONOMY: TaxonomyTable = _TABLE
295
+ # Every taxonomy version a replay can evaluate a sealed record under.
296
+ CARRIED_TAXONOMY_VERSIONS: tuple[str, ...] = tuple(_CARRIED)
297
+
298
+ # A pack may pin this with its version (``packs/schema.py`` ``TaxonomyPin``),
299
+ # so a taxonomy change, even one keeping its version, moves the pack digest.
300
+ TAXONOMY_DIGEST: str = _DIGEST
301
+
302
+ TAXONOMY_VERSION: str = _TABLE.version
303
+ UNVERSIONED_TAXONOMY_VERSION: str = _TABLE.unversioned_read_as
304
+ TRIGGER_CLASSES: tuple[str, ...] = _TABLE.trigger_classes
305
+ # Canonical rows only; legacy names resolve through ``resolve``/``classify``.
306
+ TAXONOMY: Mapping[str, ActionClass] = MappingProxyType(dict(_TABLE.classes))
307
+
308
+ MONEY_TRANSFER = TAXONOMY["money.transfer"]
309
+ DATA_DELETE = TAXONOMY["data.delete"]
310
+ COMMS_EXTERNAL = TAXONOMY["communication.send"]
311
+ # The one low-risk class, so "fail-open only where declared" is a real,
312
+ # exercised path rather than a theoretical one.
313
+ INFO_QUERY = TAXONOMY["info.query"]
314
+
315
+ UNCLASSIFIED_DEFAULT = ActionClass("unclassified", consequential=True)
316
+
317
+
318
+ def carried_taxonomy(version: object) -> TaxonomyTable | None:
319
+ """The table for taxonomy ``version`` as it shipped, or None when this
320
+ engine does not carry it (only a string names a version)."""
321
+ return _CARRIED.get(version) if isinstance(version, str) else None
322
+
323
+
324
+ def same_classes(version: object) -> bool:
325
+ """Whether a record sealed under taxonomy ``version`` names its class
326
+ from a table with exactly the engine's class names, in its order: the
327
+ engine's own version, or a carried one that differs only in approvers."""
328
+ table = ENGINE_TAXONOMY if version == ENGINE_TAXONOMY.version else carried_taxonomy(version)
329
+ return table is not None and tuple(table.classes) == tuple(ENGINE_TAXONOMY.classes)
330
+
331
+
332
+ def resolve(action_class: str) -> ActionClass | None:
333
+ """The canonical row for a canonical or legacy name, or None."""
334
+ return _TABLE.resolve(action_class)
335
+
336
+
337
+ def is_known_action_class(action_class: str) -> bool:
338
+ return resolve(action_class) is not None
339
+
340
+
341
+ def legacy_aliases() -> Mapping[str, str]:
342
+ """``{legacy name: canonical name}`` for every alias in the table."""
343
+ return _TABLE.aliases
344
+
345
+
346
+ def classify(action_class: str | None) -> ActionClass:
347
+ """Resolve a declared class name to its policy.
348
+
349
+ Absent, or not present in the taxonomy, both resolve to the
350
+ consequential/fail-closed default -- the classification default is not
351
+ conditioned on the name being *recognized*, only on it being *declared
352
+ and known*. A legacy name resolves to its canonical row.
353
+ """
354
+ return _TABLE.classify(action_class)
355
+
356
+
357
+ def trigger_class(action_class: str, *, after_prior_approval: bool = False) -> str | None:
358
+ """The measurement population ``action_class`` counts toward.
359
+
360
+ ``None`` means the class is non-consequential and outside every
361
+ population. ``after_prior_approval`` is the caller's state: a
362
+ consequential action that amends something already approved counts as
363
+ ``CHANGE``. Raises ``UnmappedActionClass`` for a name with no row.
364
+ """
365
+ ac = resolve(action_class)
366
+ if ac is None:
367
+ raise UnmappedActionClass(
368
+ f"action class {action_class!r} has no row in taxonomy version {TAXONOMY_VERSION}; "
369
+ "add it to guards/action_taxonomy.json before it can be counted"
370
+ )
371
+ if not ac.consequential:
372
+ return None
373
+ if after_prior_approval:
374
+ return "CHANGE"
375
+ return ac.trigger_class
376
+
377
+
378
+ def check_connector_declaration(exposed: Mapping[str, str]) -> dict[str, ActionClass]:
379
+ """Validate a connector's ``{tool name: canonical action class}``
380
+ declaration. Every target must be a canonical row (not a legacy alias);
381
+ raises ``UnmappedActionClass`` naming every tool that is not."""
382
+ bad = sorted(tool for tool, name in exposed.items() if name not in TAXONOMY)
383
+ if bad:
384
+ raise UnmappedActionClass(f"connector tools {bad} declare no canonical action class in taxonomy version {TAXONOMY_VERSION}")
385
+ return {tool: TAXONOMY[name] for tool, name in exposed.items()}
386
+
387
+
388
+ def record_taxonomy_version(asg_payload: VersionedPayload) -> str:
389
+ """The taxonomy version a sealed decision capsule's ``action_class`` was
390
+ written under; records that predate the table carry none."""
391
+ if "taxonomy_version" not in asg_payload:
392
+ return UNVERSIONED_TAXONOMY_VERSION
393
+ return _require_str(asg_payload["taxonomy_version"], "asg_payload.taxonomy_version")