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,361 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """``capsule guard`` verbs: dry-run replay + self-contained HTML report (moved
3
+ here, matching ``fold_cmds.py``'s per-verb-module convention, so ``main.py``
4
+ stays a thin dispatcher), plus ``guard enforce`` -- a minimal, honest local
5
+ marker command (see ``_cmd_guard_enforce``'s own docstring for exactly what
6
+ it does and doesn't do).
7
+
8
+ Every guard-dry-run invocation here checks the current packaging arm
9
+ (``capsule_ledger.packaging``) and, in "guards-only", both suppresses the
10
+ evidence chrome in the rendered HTML report (``render_report_html``'s own
11
+ ``arm`` param) and rewords/suppresses this command's own stdout so it never
12
+ prints a capsule id or a share/verify link -- see this module's inline
13
+ comments at each print site.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ from .. import packaging
22
+ from ..envcompat import env_get
23
+
24
+ __all__ = ["add_parser"]
25
+
26
+ DEFAULT_CATALOG_DIR = Path(__file__).resolve().parent.parent / "folds" / "catalog_defs"
27
+ DEFAULT_WICKET_DIR = Path(__file__).resolve().parent.parent / "guards" / "wickets" / "catalog_defs"
28
+ DEFAULT_MANIFEST_PATH = Path(__file__).resolve().parent.parent / "policy" / "catalog_defs" / "default.yaml"
29
+
30
+
31
+ def _catalog_dir(args: argparse.Namespace) -> Path:
32
+ if getattr(args, "dir", None):
33
+ return Path(args.dir)
34
+ env = env_get("CAPSULE_FOLD_DIR")
35
+ if env:
36
+ return Path(env)
37
+ return DEFAULT_CATALOG_DIR
38
+
39
+
40
+ def _wicket_dir(args: argparse.Namespace) -> Path:
41
+ if getattr(args, "wicket_dir", None):
42
+ return Path(args.wicket_dir)
43
+ env = env_get("CAPSULE_WICKET_DIR")
44
+ return Path(env) if env else DEFAULT_WICKET_DIR
45
+
46
+
47
+ def _resolve_manifest(args: argparse.Namespace, *, strict: bool):
48
+ """The policy manifest that governs this replay (``--manifest``, default
49
+ the built-in one), resolved against the fold catalog (``_catalog_dir``)
50
+ and the wicket catalog (``--wicket-dir``). ``strict``: one that does not
51
+ resolve is an error (``None``, after saying why) rather than a warning."""
52
+ from ..policy import PolicyManifestError, load_manifest_file, resolve_manifest
53
+
54
+ manifest_path = Path(args.manifest) if getattr(args, "manifest", None) else DEFAULT_MANIFEST_PATH
55
+ try:
56
+ manifest = load_manifest_file(manifest_path)
57
+ return resolve_manifest(manifest, fold_catalog_dir=_catalog_dir(args), wicket_catalog_dir=_wicket_dir(args))
58
+ except PolicyManifestError as exc:
59
+ if strict:
60
+ print(f"capsule guard dry-run: policy manifest {manifest_path} did not resolve ({exc.reason}): {exc}",
61
+ file=sys.stderr)
62
+ else:
63
+ print(f"warning: policy manifest {manifest_path} did not resolve ({exc.reason}: {exc}); "
64
+ "proceeding without a manifest citation", file=sys.stderr)
65
+ return None
66
+
67
+
68
+ def _resolve_manifest_digest(args: argparse.Namespace) -> str | None:
69
+ """Resolve the policy manifest that governs this replay (``--manifest``,
70
+ default the built-in one) against the same fold catalog this command is
71
+ already using (``_catalog_dir``) and the wicket catalog (``--wicket-dir``).
72
+
73
+ Best-effort: a caller pointing ``--dir``/``--wicket-dir`` at a catalog the
74
+ configured manifest doesn't (yet) pin correctly gets a warning, not a
75
+ hard failure -- the replay report itself is still real and useful without
76
+ a resolvable manifest citation. ``--no-manifest`` skips this entirely."""
77
+ if getattr(args, "no_manifest", False):
78
+ return None
79
+ resolved = _resolve_manifest(args, strict=False)
80
+ return resolved.manifest_digest if resolved is not None else None
81
+
82
+
83
+ def _parse_cap_args(items: list[str]) -> dict[str, int] | None:
84
+ caps_minor: dict[str, int] = {}
85
+ for item in items or []:
86
+ cls, sep, value = item.partition("=")
87
+ if not sep:
88
+ print(f"--cap must be CLASS=MINOR_UNITS, got {item!r}", file=sys.stderr)
89
+ return None
90
+ try:
91
+ caps_minor[cls] = int(value)
92
+ except ValueError:
93
+ print(f"--cap value must be an integer (minor units), got {item!r}", file=sys.stderr)
94
+ return None
95
+ return caps_minor
96
+
97
+
98
+ def _cmd_guard_dry_run(args: argparse.Namespace) -> int:
99
+ from agent_action_capsule import compute_capsule_id
100
+
101
+ from ..folds.loader import load_definition_file
102
+ from ..guards.capsule import local_only_refusal
103
+ from ..packs import PackDefinitionError, load_proposals_file
104
+ from ..report import build_dry_run_report, build_dry_run_report_with_proposal, render_report_html
105
+ from ..report.render import TelemetryConfig, decode_fragment, to_fragment_payload
106
+ from ..telemetry.record import record_evidence_touch, record_guard_configured, record_guard_evaluated
107
+
108
+ if bool(args.model_note) != bool(args.model_id):
109
+ print("--model-note and --model-id must be given together (or neither)", file=sys.stderr)
110
+ return 1
111
+
112
+ caps_minor = _parse_cap_args(args.cap)
113
+ if caps_minor is None:
114
+ return 1
115
+
116
+ arm = packaging.current_arm()
117
+ evidence_visible = packaging.evidence_visible(arm)
118
+
119
+ since = None if args.since in (None, "all") else args.since
120
+ fold_path = Path(args.fold_file) if args.fold_file else _catalog_dir(args) / "spend.weekly.yaml"
121
+ caps_fold = load_definition_file(fold_path)
122
+ per_action_minor = None
123
+ per_action_reads = None
124
+ if getattr(args, "caps_from_manifest", False):
125
+ # The resolved manifest's own caps wicket: its window and per-action
126
+ # limits, and the fold it names. A --cap still sets a window limit.
127
+ if getattr(args, "no_manifest", False):
128
+ print("--caps-from-manifest needs a manifest: drop --no-manifest", file=sys.stderr)
129
+ return 1
130
+ resolved = _resolve_manifest(args, strict=True)
131
+ if resolved is None:
132
+ return 1
133
+ if not resolved.caps_minor():
134
+ print("capsule guard dry-run: --caps-from-manifest: the manifest configures no caps limits", file=sys.stderr)
135
+ return 1
136
+ caps_minor = {**resolved.caps_minor(), **caps_minor}
137
+ per_action_minor = resolved.per_action_minor() or None
138
+ per_action_reads = resolved.per_action_reads()
139
+ if not args.fold_file and resolved.caps_fold() is not None:
140
+ caps_fold = resolved.caps_fold()
141
+ manifest_digest = resolved.manifest_digest
142
+ else:
143
+ manifest_digest = _resolve_manifest_digest(args)
144
+
145
+ proposed_caps_minor = None
146
+ proposal_rationale = None
147
+ if args.proposals:
148
+ try:
149
+ _, proposals = load_proposals_file(args.proposals)
150
+ except PackDefinitionError as exc:
151
+ print(f"capsule guard dry-run: --proposals failed to load ({exc.reason}): {exc}", file=sys.stderr)
152
+ return 1
153
+ proposed_caps_minor = {p["action_class"]: p["proposed_cap_minor"] for p in proposals}
154
+ parts = [f"{cls}={cap}" for cls, cap in sorted(proposed_caps_minor.items())]
155
+ proposal_rationale = f"proposed: {', '.join(parts)}"
156
+
157
+ def _build():
158
+ if proposed_caps_minor is not None:
159
+ return build_dry_run_report_with_proposal(
160
+ args.ledger,
161
+ caps_fold=caps_fold,
162
+ proposed_caps_minor=proposed_caps_minor,
163
+ since=since,
164
+ caps_minor=caps_minor,
165
+ proposal_rationale=proposal_rationale,
166
+ operator=args.operator,
167
+ model_note=args.model_note,
168
+ model_id=args.model_id,
169
+ manifest_digest=manifest_digest,
170
+ per_action_minor=per_action_minor,
171
+ per_action_reads=per_action_reads,
172
+ )
173
+ return build_dry_run_report(
174
+ args.ledger,
175
+ caps_fold=caps_fold,
176
+ since=since,
177
+ caps_minor=caps_minor,
178
+ operator=args.operator,
179
+ model_note=args.model_note,
180
+ model_id=args.model_id,
181
+ manifest_digest=manifest_digest,
182
+ per_action_minor=per_action_minor,
183
+ per_action_reads=per_action_reads,
184
+ )
185
+
186
+ report = _build()
187
+
188
+ # The printed link carries every row's decision capsule and the capsule
189
+ # it cites; one holding a local-only field must not leave this machine.
190
+ if args.share and evidence_visible:
191
+ shared = [c for section in report.guards for row in section.rows for c in (row.capsule, row.cited_capsule) if c]
192
+ refusal = local_only_refusal(shared)
193
+ if refusal is not None:
194
+ print(f"capsule guard dry-run --share: refused: {refusal}; rerun without --share", file=sys.stderr)
195
+ return 1
196
+
197
+ telemetry = None
198
+ if args.telemetry_opt_in and args.telemetry_endpoint:
199
+ telemetry = TelemetryConfig(opted_in=True, endpoint=args.telemetry_endpoint)
200
+
201
+ html, fragment = render_report_html(report, arm=arm, telemetry=telemetry)
202
+ out_path = Path(args.out)
203
+ out_path.write_text(html, encoding="utf-8")
204
+ url = f"file://{out_path.resolve()}#{fragment}"
205
+
206
+ print(f"wrote {out_path}")
207
+ if report.manifest_digest is not None:
208
+ print(f"evaluated under manifest {report.manifest_digest}")
209
+ # Arm A ("guards-only"): the share link is a verify-link suggestion --
210
+ # not printed, matching the report itself never surfacing its evidence
211
+ # chrome. The file above still exists and is still openable; this CLI
212
+ # just doesn't advertise the fragment-carried permalink.
213
+ if args.share and evidence_visible:
214
+ print(url)
215
+
216
+ if caps_minor:
217
+ record_guard_configured(arm)
218
+ if report.actions_replayed:
219
+ record_guard_evaluated(arm)
220
+ if evidence_visible and (args.share or args.verify):
221
+ record_evidence_touch(arm)
222
+
223
+ if args.verify:
224
+ written_payload = decode_fragment(fragment)
225
+ rebuilt_payload = to_fragment_payload(_build())
226
+ # exclude generated_at: the one field that legitimately differs
227
+ # between two otherwise-identical replays (it's a wall-clock
228
+ # generation timestamp, not derived from the ledger).
229
+ lhs = {k: v for k, v in written_payload.items() if k != "generated_at"}
230
+ rhs = {k: v for k, v in rebuilt_payload.items() if k != "generated_at"}
231
+ if lhs != rhs:
232
+ print("FAIL: replaying the same ledger did not reproduce the same report", file=sys.stderr)
233
+ return 1
234
+
235
+ mismatches = []
236
+ for section in written_payload["guards"]:
237
+ for row in section["rows"]:
238
+ for capsule in (row.get("capsule"), row.get("cited_capsule")):
239
+ if not capsule:
240
+ continue
241
+ if compute_capsule_id(capsule) != capsule.get("capsule_id"):
242
+ mismatches.append(capsule.get("capsule_id"))
243
+ if mismatches:
244
+ # Arm A: "cited record(s)", never the word "capsule" in output.
245
+ noun = "cited capsule(s)" if evidence_visible else "cited record(s)"
246
+ print(f"FAIL: {len(mismatches)} {noun} do not re-verify", file=sys.stderr)
247
+ return 1
248
+ tail = " and every cited capsule verifies" if evidence_visible else ""
249
+ print(f"OK: report is reproducible{tail}")
250
+
251
+ return 0
252
+
253
+
254
+ def _cmd_guard_enforce(args: argparse.Namespace) -> int:
255
+ """Records, locally, that this install has moved from dry-run to
256
+ enforce -- nothing more. ``GuardEngine.check(..., dry_run=...)`` is
257
+ already a real parameter your own integration code controls (this CLI
258
+ only ever drives it in dry-run mode via ``guard dry-run``); this
259
+ command does not gate anything itself. It exists so there is a real,
260
+ minimal, honest action to record the M2 ("enforcement-on") telemetry
261
+ fact against, matching the report's own "$ capsule guard enforce" callout.
262
+ """
263
+ from ..telemetry.record import record_enforcement_flip
264
+
265
+ arm = packaging.current_arm()
266
+ record_enforcement_flip(arm)
267
+ print(
268
+ "recorded: this install has moved from dry-run to enforce (locally, opt-in telemetry only).\n"
269
+ "This command does not itself gate actions -- wire GuardEngine.check(..., dry_run=False) into "
270
+ "your own integration to actually enforce."
271
+ )
272
+ return 0
273
+
274
+
275
+ def add_parser(sub: argparse._SubParsersAction) -> argparse.ArgumentParser:
276
+ guard = sub.add_parser("guard", help="guard API: dry-run replay + report (full CLI lands in a later task)")
277
+ guard_sub = guard.add_subparsers(dest="guard_command")
278
+ guard.set_defaults(guard_parser=guard)
279
+
280
+ p_dry_run = guard_sub.add_parser(
281
+ "dry-run", help="replay a ledger through the guard checks and emit a self-contained HTML report"
282
+ )
283
+ p_dry_run.add_argument(
284
+ "--ledger", required=True, action="append", help="ledger JSONL file or LedgerStore directory (repeatable)"
285
+ )
286
+ p_dry_run.add_argument(
287
+ "--since", default="7d",
288
+ help="rolling window, e.g. '7d' (anchored to the ledger's own latest record); 'all' for no filter",
289
+ )
290
+ p_dry_run.add_argument(
291
+ "--share", action="store_true",
292
+ help="print the full shareable file://...#<fragment> URL (no-op in the guards-only packaging arm)",
293
+ )
294
+ p_dry_run.add_argument(
295
+ "--verify", action="store_true", help="re-replay and re-verify every cited capsule before exiting"
296
+ )
297
+ p_dry_run.add_argument("--out", default="dry-run-report.html", help="output HTML path (default: dry-run-report.html)")
298
+ p_dry_run.add_argument(
299
+ "--operator", default=None, help="override the displayed operator label (default: the ledger's own)"
300
+ )
301
+ p_dry_run.add_argument(
302
+ "--cap", action="append", default=[], metavar="CLASS=MINOR_UNITS",
303
+ help="per-action-class cap for the caps check, e.g. money.transfer=10000000 (repeatable; omit an action "
304
+ "class to leave it unconfigured -- it will never trigger the caps guard, matching GuardEngine's own default)",
305
+ )
306
+ p_dry_run.add_argument(
307
+ "--model-note", default=None, help="optional pre-written model commentary quote (never generated by this CLI)"
308
+ )
309
+ p_dry_run.add_argument(
310
+ "--model-id", default=None, help="model id the note above was drafted by (required together with --model-note)"
311
+ )
312
+ p_dry_run.add_argument(
313
+ "--fold-file", default=None,
314
+ help="fold definition YAML the caps check evaluates against (default: the built-in "
315
+ "spend.weekly.yaml) -- point this at a pack's own materialized fold, e.g. "
316
+ ".capsule/catalog/folds/payments_safety.yaml, to replay under that pack's caps",
317
+ )
318
+ p_dry_run.add_argument(
319
+ "--proposals", default=None,
320
+ help="proposals YAML file (from `capsule thresholds propose --out`) -- when given, adds a "
321
+ "section showing what would ALSO have been held under the proposed caps, on top of what "
322
+ "--cap already configures (the P2 'conversion moment': what tightening to the proposal "
323
+ "would additionally catch)",
324
+ )
325
+ p_dry_run.add_argument(
326
+ "--dir", help="fold catalog directory the caps check's fold definition lives in (default: built-in catalog, or $CAPSULE_FOLD_DIR)"
327
+ )
328
+ p_dry_run.add_argument(
329
+ "--manifest", help="policy manifest YAML resolved for this replay (default: built-in manifest)"
330
+ )
331
+ p_dry_run.add_argument(
332
+ "--wicket-dir", dest="wicket_dir",
333
+ help="wicket catalog directory the manifest's wicket refs resolve against (default: built-in catalog, or $CAPSULE_WICKET_DIR)",
334
+ )
335
+ p_dry_run.add_argument(
336
+ "--caps-from-manifest", dest="caps_from_manifest", action="store_true",
337
+ help="apply the resolved manifest's caps wicket to the replay: its window and per-action limits "
338
+ "(e.g. caps/3.0.0) and the fold it names; --cap still sets a window limit (default: off, only --cap limits apply)",
339
+ )
340
+ p_dry_run.add_argument(
341
+ "--no-manifest", dest="no_manifest", action="store_true",
342
+ help="skip policy-manifest resolution entirely (report carries no manifest_digest)",
343
+ )
344
+ p_dry_run.add_argument(
345
+ "--telemetry-opt-in", action="store_true",
346
+ help="embed a disclosed, anonymous open-beacon in the report for the M6 (viral unit) metric -- "
347
+ "requires --telemetry-endpoint too, and is off by default; see `capsule telemetry status`",
348
+ )
349
+ p_dry_run.add_argument(
350
+ "--telemetry-endpoint", default=env_get("CAPSULE_LEDGER_TELEMETRY_ENDPOINT"),
351
+ help="where the open-beacon above (if enabled) sends its single anonymous event (default: "
352
+ "$CAPSULE_LEDGER_TELEMETRY_ENDPOINT, or none -- with no endpoint the beacon is never embedded)",
353
+ )
354
+ p_dry_run.set_defaults(func=_cmd_guard_dry_run)
355
+
356
+ p_enforce = guard_sub.add_parser(
357
+ "enforce", help="record locally that this install has moved from dry-run to enforce (telemetry marker only)"
358
+ )
359
+ p_enforce.set_defaults(func=_cmd_guard_enforce)
360
+
361
+ return guard
@@ -0,0 +1,169 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """``capsule init --pack <name>``: install a starter pack in observe mode.
3
+
4
+ Materializes the pack's constraints and folds into ``<project-dir>/.capsule/``,
5
+ writes the policy manifest fragment that cites them plus the pack itself (by
6
+ digest, mode ``observe``) -- so which pack version is in force is provable --
7
+ and, when ``--ledger`` is given, records that installation as a signed
8
+ ``policy_manifest_activated`` event capsule (``packs/install.py``'s
9
+ ``record_pack_activation``).
10
+
11
+ When ``--pins`` is given, every artifact this install would trust (the pack
12
+ itself, plus every fold it ships) is verified against that pins file before
13
+ anything is installed -- fail closed on a missing or mismatched pin,
14
+ nothing materialized, nothing recorded (``packs/pins.py``). This stands in
15
+ for a live ``capsule-registry`` fetch, which does not exist yet
16
+ (registry-architecture-and-namespace-2026-08-10.md §6); the verification
17
+ gate's shape does not change when that swap happens later. Without
18
+ ``--pins``, this command prints each artifact's own digest so a first
19
+ install can seed a pins file for the next one.
20
+
21
+ This command does not run anything against live traffic itself -- it hands
22
+ back the manifest a caller's own integration resolves and builds a
23
+ ``GuardEngine`` from (``packs.build_engine``), calling
24
+ ``GuardEngine.check(..., dry_run=True)`` for as long as the pack stays in
25
+ observe mode. "Observe mode" is a property of the installed manifest
26
+ (``PackRef.mode``), not something this command enforces at runtime.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import secrets
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ from cll.ledger import LedgerStore
36
+
37
+ from ..envcompat import env_get
38
+ from ..guards.signing import LocalSigner
39
+ from ..packs import (
40
+ PackDefinitionError,
41
+ RegistryPinError,
42
+ install_pack,
43
+ load_pack_dir,
44
+ load_pins_file,
45
+ record_pack_activation,
46
+ verify_pins,
47
+ )
48
+
49
+ __all__ = ["add_parser"]
50
+
51
+ BUILTIN_PACK_CATALOG_DIR = Path(__file__).resolve().parent.parent / "packs" / "catalog"
52
+
53
+ _KEY_ID_ENV = "CAPSULE_MCP_SIGNING_KEY_ID"
54
+ _SECRET_ENV = "CAPSULE_MCP_SIGNING_SECRET"
55
+
56
+
57
+ def _available_packs(catalog_dir: Path) -> list[str]:
58
+ if not catalog_dir.is_dir():
59
+ return []
60
+ return sorted(p.name for p in catalog_dir.iterdir() if (p / "pack.yaml").is_file())
61
+
62
+
63
+ def _cmd_init(args: argparse.Namespace) -> int:
64
+ catalog_dir = Path(args.pack_catalog_dir) if args.pack_catalog_dir else BUILTIN_PACK_CATALOG_DIR
65
+ pack_dir = catalog_dir / args.pack
66
+
67
+ if not (pack_dir / "pack.yaml").is_file():
68
+ available = _available_packs(catalog_dir)
69
+ print(f"capsule init: no pack named {args.pack!r} in {catalog_dir}", file=sys.stderr)
70
+ print(f" available packs: {available or '<none>'}", file=sys.stderr)
71
+ return 2
72
+
73
+ try:
74
+ pack = load_pack_dir(pack_dir)
75
+ except PackDefinitionError as exc:
76
+ print(f"capsule init: pack {args.pack!r} failed to load ({exc.reason}): {exc}", file=sys.stderr)
77
+ return 1
78
+
79
+ if args.pins:
80
+ try:
81
+ pins = load_pins_file(args.pins)
82
+ verify_pins(pack, pins)
83
+ except RegistryPinError as exc:
84
+ print(f"capsule init: registry-pin verification failed ({exc.reason}): {exc}", file=sys.stderr)
85
+ print("nothing installed -- fail closed on a missing or mismatched pin.", file=sys.stderr)
86
+ return 1
87
+ print(f"registry-pin verification passed against {args.pins} ({1 + len(pack.folds)} artifact(s))")
88
+
89
+ project_dir = Path(args.project_dir)
90
+ installed = install_pack(pack, project_dir=project_dir, mode="observe")
91
+
92
+ print(f"installed {pack.pack_id} in observe mode")
93
+ print(f" manifest: {installed.manifest_path}")
94
+ print(f" manifest id: {installed.manifest.manifest_id}")
95
+ print(f" manifest digest: {installed.resolved.manifest_digest}")
96
+ print(f" fold catalog: {installed.fold_catalog_dir}")
97
+ print(f" wicket catalog: {installed.wicket_catalog_dir}")
98
+ print(f" obligations: {', '.join(o.id for o in pack.obligations)}")
99
+ if not args.pins:
100
+ print(" artifact digests (seed a pins file with these to verify future installs with --pins):")
101
+ print(f" {pack.pack_id}: {pack.definition_digest()}")
102
+ for fold in pack.folds:
103
+ print(f" {fold.fold_id}: {fold.definition_digest()}")
104
+ print(
105
+ "observe mode: every action this pack governs will be recorded with its would-be verdict "
106
+ "(allow/deny/escalate) -- nothing is enforced until a human runs `capsule enforce --pack "
107
+ f"{args.pack}` (not yet built)."
108
+ )
109
+
110
+ if not args.ledger:
111
+ print()
112
+ print("no --ledger given: manifest written to disk only, no activation capsule recorded")
113
+ return 0
114
+
115
+ key_id = args.key_id or env_get(_KEY_ID_ENV)
116
+ secret_text = args.secret or env_get(_SECRET_ENV)
117
+ generated_secret = secret_text is None
118
+ if key_id is None:
119
+ key_id = f"{args.operator}-pack-install-key"
120
+ if secret_text is None:
121
+ secret_text = secrets.token_hex(32)
122
+ signer = LocalSigner(key_id=key_id, secret=secret_text.encode("utf-8"))
123
+
124
+ ledger = LedgerStore(args.ledger)
125
+ try:
126
+ capsule = record_pack_activation(
127
+ installed, ledger=ledger, operator=args.operator, developer=args.developer, signer=signer
128
+ )
129
+ finally:
130
+ ledger.close()
131
+
132
+ print()
133
+ print(f"activation recorded: {capsule['capsule_id']}")
134
+ if generated_secret:
135
+ print("(signed with a freshly generated key -- not persisted anywhere; pass --key-id/--secret, or set "
136
+ f"${_KEY_ID_ENV}/${_SECRET_ENV}, to reuse a stable key next time)")
137
+ print(f" export {_KEY_ID_ENV}={key_id}")
138
+ print(f" export {_SECRET_ENV}={secret_text}")
139
+ return 0
140
+
141
+
142
+ def add_parser(sub: argparse._SubParsersAction) -> argparse.ArgumentParser:
143
+ p_init = sub.add_parser("init", help="install a starter pack in observe mode (records, enforces nothing)")
144
+ p_init.add_argument("--pack", required=True, help="pack name, e.g. payments-safety (built-in catalog)")
145
+ p_init.add_argument(
146
+ "--project-dir", default=".", help="where to materialize .capsule/ (catalogs + manifest); default: cwd"
147
+ )
148
+ p_init.add_argument(
149
+ "--pack-catalog-dir", default=None, help="override the built-in pack catalog directory (mainly for tests)"
150
+ )
151
+ p_init.add_argument(
152
+ "--pins", default=None,
153
+ help="pins file (artifact id -> 64-hex digest) to verify the pack and its folds against before "
154
+ "installing; fail closed on a missing or mismatched pin (default: none, no verification -- "
155
+ "this command still prints each artifact's digest so you can seed one)",
156
+ )
157
+ p_init.add_argument(
158
+ "--ledger", default=None,
159
+ help="ledger store directory to record this install's activation capsule into (default: none, manifest "
160
+ "is still written to disk)",
161
+ )
162
+ p_init.add_argument("--operator", default="local", help="operator identity for the activation capsule (default: 'local')")
163
+ p_init.add_argument(
164
+ "--developer", default="capsule-init-tool", help="developer identity for the activation capsule"
165
+ )
166
+ p_init.add_argument("--key-id", default=None, help=f"signing key id for the activation capsule (default: ${_KEY_ID_ENV})")
167
+ p_init.add_argument("--secret", default=None, help=f"signing key secret (default: ${_SECRET_ENV}, or freshly generated)")
168
+ p_init.set_defaults(func=_cmd_init)
169
+ return p_init
@@ -0,0 +1,120 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`capsule-engine` CLI entry point: the verbs this repo owns -- `guard`
3
+ (dry-run replay + report, enforce marker), `telemetry` (disclosure/status,
4
+ funnel report), `init` (install a starter pack), `constraints` (list the
5
+ registered guard checks + action-class taxonomy), `tenant`
6
+ (engine-instance-per-tenant provisioning), plus the evidence verbs `verify`
7
+ (verify one ledger record or an offline bundle), `bundle` (produce a
8
+ self-contained verifiable slice of the ledger), and `console` (serve the
9
+ local console UI) -- registered only in the "full" packaging arm, same
10
+ two-arm switch (`capsule_engine.packaging`) the original monorepo used.
11
+
12
+ This is a deliberately minimal dispatcher: the original monorepo's
13
+ `cli/main.py` wired every verb across ledger, folds, guards, packs, policy,
14
+ console, telemetry, etc. Post-split, this repo only owns the verb modules
15
+ that actually live here -- the rest stayed with their owning repo.
16
+ `cli/format.py` is not wired as a subcommand: it has no `add_parser`, it is
17
+ shared output-formatting helpers used by the verbs above.
18
+
19
+ This module is a thin dispatcher; each verb's logic lives in its own
20
+ `cli/*_cmd.py` module.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import sys
26
+
27
+ from .. import packaging
28
+ from . import (
29
+ bundle_cmd,
30
+ console_cmd,
31
+ constraints_cmd,
32
+ guard_cmds,
33
+ init_cmds,
34
+ packs_cmds,
35
+ telemetry_cmd,
36
+ tenant_cmds,
37
+ verify_cmd,
38
+ )
39
+
40
+ __all__ = ["main"]
41
+
42
+
43
+ def _build_parser(arm: str | None = None) -> argparse.ArgumentParser:
44
+ arm = arm or packaging.current_arm()
45
+ parser = argparse.ArgumentParser(prog="capsule-engine", description="capsule-engine control plane")
46
+ parser.add_argument("--version", action="store_true", help="print the version and exit")
47
+ sub = parser.add_subparsers(dest="command")
48
+
49
+ init_cmds.add_parser(sub)
50
+ constraints_cmd.add_parser(sub)
51
+ guard_cmds.add_parser(sub)
52
+ tenant_cmds.add_parser(sub)
53
+ telemetry_cmd.add_parser(sub)
54
+ packs_cmds.add_parser(sub)
55
+
56
+ # The record-query/evidence verbs -- capsule verify, the shareable
57
+ # bundle, and the local console UI -- are the "evidence": registered
58
+ # only in the "full" arm. See ``packaging.py``'s module docstring for
59
+ # why an env var, not a fork, drives this.
60
+ if packaging.evidence_visible(arm):
61
+ verify_cmd.add_parser(sub)
62
+ bundle_cmd.add_parser(sub)
63
+ console_cmd.add_parser(sub)
64
+
65
+ return parser
66
+
67
+
68
+ def main(argv: list[str] | None = None) -> int:
69
+ parser = _build_parser()
70
+ args = parser.parse_args(argv)
71
+
72
+ if args.version:
73
+ from .. import __version__
74
+
75
+ print(__version__)
76
+ return 0
77
+
78
+ from ..telemetry.record import record_install_seen
79
+
80
+ record_install_seen(packaging.current_arm())
81
+
82
+ if args.command is None:
83
+ parser.print_help()
84
+ return 0
85
+
86
+ if args.command == "constraints":
87
+ if getattr(args, "constraints_command", None) is None:
88
+ args.constraints_parser.print_help()
89
+ return 0
90
+ return args.func(args)
91
+
92
+ if args.command == "guard":
93
+ if getattr(args, "guard_command", None) is None:
94
+ args.guard_parser.print_help()
95
+ return 0
96
+ return args.func(args)
97
+
98
+ if args.command == "tenant":
99
+ if getattr(args, "tenant_command", None) is None:
100
+ args.tenant_parser.print_help()
101
+ return 0
102
+ return args.func(args)
103
+
104
+ if args.command == "telemetry":
105
+ if getattr(args, "telemetry_command", None) is None:
106
+ args.telemetry_parser.print_help()
107
+ return 0
108
+ return args.func(args)
109
+
110
+ if args.command == "packs":
111
+ if getattr(args, "packs_command", None) is None:
112
+ args.packs_parser.print_help()
113
+ return 0
114
+ return args.func(args)
115
+
116
+ return args.func(args)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ sys.exit(main())