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,327 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Engine-instance-per-tenant provisioning: one physically-separate ledger
3
+ dir + policy manifest + signing-key identity per tenant, under a shared
4
+ ``<tenants-root>/<tenant-id>/`` layout.
5
+
6
+ This is deliberately NOT a multi-tenant ledger process. The OSS engine
7
+ (``LedgerStore``/``GuardEngine``) is single-org by scope; the strongest
8
+ isolation story for a caller embedding it for many customers is to run one
9
+ whole engine instance per tenant -- separate files, separate keys, so a
10
+ compromise or a bug in one tenant's instance can never cross into another's
11
+ (see ``docs/tenant-provisioning.md``). This module is the shared logic
12
+ behind ``capsule tenant init``/``upgrade``/``list`` (``cli/tenant_cmds.py``):
13
+ templating that instantiation, not a new kind of engine.
14
+
15
+ Layout, under ``<tenants-root>/<tenant_id>/``::
16
+
17
+ ledger/ a LedgerStore root (segments/, index.sqlite3)
18
+ .capsule/policy/manifest.yaml this tenant's own pinned policy manifest
19
+ .capsule/catalog/... materialized fold/wicket defs, pack installs only
20
+ tenant.json provisioning metadata -- never the signing secret
21
+
22
+ Signing secrets are never written to disk anywhere in this module, matching
23
+ every other write path in this package (``guards/signing.py``): a freshly
24
+ generated secret is handed back to the caller once, for the caller to place
25
+ in whatever secret store the tenant's own process/environment uses.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import re
31
+ import secrets as secrets_mod
32
+ from dataclasses import dataclass
33
+ from pathlib import Path
34
+
35
+ import yaml
36
+ from cll.ledger import LedgerStore
37
+
38
+ from .guards.signing import LocalSigner, key_fingerprint
39
+ from .packs import PackDefinition, install_pack
40
+ from .packs.install import CATALOG_DIRNAME
41
+ from .policy import (
42
+ Manifest,
43
+ build_manifest_activation_capsule,
44
+ find_latest_activation,
45
+ load_manifest_file,
46
+ resolve_manifest,
47
+ )
48
+
49
+ __all__ = [
50
+ "TenantProvisionError",
51
+ "TenantLayout",
52
+ "InitResult",
53
+ "UpgradeResult",
54
+ "TENANT_ID_RE",
55
+ "tenant_layout",
56
+ "init_tenant",
57
+ "upgrade_tenant",
58
+ "list_tenants",
59
+ ]
60
+
61
+ _PACKAGE_ROOT = Path(__file__).resolve().parent
62
+ DEFAULT_MANIFEST_PATH = _PACKAGE_ROOT / "policy" / "catalog_defs" / "default.yaml"
63
+ DEFAULT_FOLD_DIR = _PACKAGE_ROOT / "folds" / "catalog_defs"
64
+ DEFAULT_WICKET_DIR = _PACKAGE_ROOT / "guards" / "wickets" / "catalog_defs"
65
+
66
+ LEDGER_DIRNAME = "ledger"
67
+ TENANT_METADATA_FILENAME = "tenant.json"
68
+
69
+ # Deliberately conservative -- a tenant_id feeds directly into a filesystem
70
+ # path (``<tenants-root>/<tenant_id>/``). No ``/``, no ``.``, no leading
71
+ # dash: rules out path traversal (``../..``) and hidden-dir surprises.
72
+ TENANT_ID_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,62}$")
73
+
74
+
75
+ class TenantProvisionError(Exception):
76
+ """Fail-closed provisioning error: nothing partial is left behind."""
77
+
78
+ def __init__(self, reason: str, message: str):
79
+ super().__init__(message)
80
+ self.reason = reason
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class TenantLayout:
85
+ tenant_id: str
86
+ root: Path
87
+
88
+ @property
89
+ def ledger_dir(self) -> Path:
90
+ return self.root / LEDGER_DIRNAME
91
+
92
+ @property
93
+ def catalog_root(self) -> Path:
94
+ return self.root / CATALOG_DIRNAME
95
+
96
+ @property
97
+ def manifest_path(self) -> Path:
98
+ return self.catalog_root / "policy" / "manifest.yaml"
99
+
100
+ @property
101
+ def metadata_path(self) -> Path:
102
+ return self.root / TENANT_METADATA_FILENAME
103
+
104
+
105
+ def tenant_layout(tenants_root: str | Path, tenant_id: str) -> TenantLayout:
106
+ if not TENANT_ID_RE.match(tenant_id):
107
+ raise TenantProvisionError(
108
+ "invalid_tenant_id",
109
+ f"tenant_id {tenant_id!r} must match {TENANT_ID_RE.pattern} (lowercase "
110
+ "letters/digits/hyphens, cannot start with a hyphen) -- it becomes a "
111
+ "directory name",
112
+ )
113
+ return TenantLayout(tenant_id=tenant_id, root=Path(tenants_root) / tenant_id)
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class InitResult:
118
+ layout: TenantLayout
119
+ manifest_id: str
120
+ manifest_digest: str
121
+ key_id: str
122
+ secret: bytes
123
+ generated_secret: bool
124
+ activation_capsule_id: str
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class UpgradeResult:
129
+ layout: TenantLayout
130
+ manifest_id: str
131
+ manifest_digest: str
132
+ changed: bool
133
+ activation_capsule_id: str | None
134
+
135
+
136
+ def _materialize_default_manifest(
137
+ layout: TenantLayout, *, manifest_path: Path, fold_catalog_dir: Path, wicket_catalog_dir: Path
138
+ ):
139
+ manifest: Manifest = load_manifest_file(manifest_path)
140
+ resolved = resolve_manifest(manifest, fold_catalog_dir=fold_catalog_dir, wicket_catalog_dir=wicket_catalog_dir)
141
+ layout.manifest_path.parent.mkdir(parents=True, exist_ok=True)
142
+ layout.manifest_path.write_text(yaml.safe_dump(manifest.canonical_dict(), sort_keys=False))
143
+ return resolved
144
+
145
+
146
+ def _materialize_manifest(
147
+ layout: TenantLayout,
148
+ *,
149
+ pack: PackDefinition | None,
150
+ manifest_path: Path | None,
151
+ fold_catalog_dir: Path | None,
152
+ wicket_catalog_dir: Path | None,
153
+ ):
154
+ """Write this tenant's own pinned manifest to ``layout.manifest_path``
155
+ and return its ``ResolvedManifest``. Two mutually exclusive modes: a
156
+ starter pack (delegates to ``packs.install_pack``, which already
157
+ materializes the pack's fold/wicket catalog alongside the manifest), or
158
+ the built-in default manifest (no pack -- folds/wickets stay referenced
159
+ from the installed package's own catalog, same as ``capsule manifest
160
+ show`` does today)."""
161
+ if pack is not None:
162
+ installed = install_pack(pack, project_dir=layout.root, mode="observe")
163
+ return installed.resolved
164
+ return _materialize_default_manifest(
165
+ layout,
166
+ manifest_path=manifest_path or DEFAULT_MANIFEST_PATH,
167
+ fold_catalog_dir=fold_catalog_dir or DEFAULT_FOLD_DIR,
168
+ wicket_catalog_dir=wicket_catalog_dir or DEFAULT_WICKET_DIR,
169
+ )
170
+
171
+
172
+ def init_tenant(
173
+ tenants_root: str | Path,
174
+ tenant_id: str,
175
+ *,
176
+ pack: PackDefinition | None = None,
177
+ manifest_path: Path | None = None,
178
+ fold_catalog_dir: Path | None = None,
179
+ wicket_catalog_dir: Path | None = None,
180
+ key_id: str | None = None,
181
+ secret: bytes | None = None,
182
+ operator: str = "local",
183
+ developer: str = "capsule-tenant-init",
184
+ ) -> InitResult:
185
+ """Provision a brand-new tenant instance: a fresh ledger dir, this
186
+ tenant's own pinned manifest, and a first ``policy_manifest_activated``
187
+ activation capsule opening its epoch history -- signed by a fresh (or
188
+ caller-supplied) key, so the tenant's ledger carries a real record of
189
+ what policy governed it from record one. Refuses if anything already
190
+ exists at this tenant's path (fail closed -- use ``upgrade_tenant`` for
191
+ an existing tenant)."""
192
+ layout = tenant_layout(tenants_root, tenant_id)
193
+ if layout.root.exists() and any(layout.root.iterdir()):
194
+ raise TenantProvisionError(
195
+ "already_initialized",
196
+ f"{layout.root} already exists and is not empty -- run `capsule tenant upgrade` "
197
+ "instead, or provision a different --tenant-id",
198
+ )
199
+
200
+ resolved = _materialize_manifest(
201
+ layout, pack=pack, manifest_path=manifest_path, fold_catalog_dir=fold_catalog_dir, wicket_catalog_dir=wicket_catalog_dir
202
+ )
203
+
204
+ resolved_key_id = key_id or f"{tenant_id}-signing-key"
205
+ generated_secret = secret is None
206
+ resolved_secret = secret if secret is not None else secrets_mod.token_hex(32).encode("utf-8")
207
+ signer = LocalSigner(key_id=resolved_key_id, secret=resolved_secret)
208
+
209
+ with LedgerStore(layout.ledger_dir) as store:
210
+ capsule = build_manifest_activation_capsule(
211
+ resolved=resolved,
212
+ operator=operator,
213
+ developer=developer,
214
+ signer=signer,
215
+ previous_activation_capsule_id=None,
216
+ )
217
+ store.append(capsule, consequential=False)
218
+
219
+ metadata = {
220
+ "tenant_id": tenant_id,
221
+ "manifest_id": resolved.manifest.manifest_id,
222
+ "manifest_digest": resolved.manifest_digest,
223
+ "pack_id": pack.pack_id if pack is not None else None,
224
+ "key_id": resolved_key_id,
225
+ "key_fingerprint": key_fingerprint(resolved_key_id, resolved_secret),
226
+ "activation_capsule_id": capsule["capsule_id"],
227
+ }
228
+ layout.metadata_path.write_text(json.dumps(metadata, indent=2, sort_keys=True) + "\n")
229
+
230
+ return InitResult(
231
+ layout=layout,
232
+ manifest_id=resolved.manifest.manifest_id,
233
+ manifest_digest=resolved.manifest_digest,
234
+ key_id=resolved_key_id,
235
+ secret=resolved_secret,
236
+ generated_secret=generated_secret,
237
+ activation_capsule_id=capsule["capsule_id"],
238
+ )
239
+
240
+
241
+ def upgrade_tenant(
242
+ tenants_root: str | Path,
243
+ tenant_id: str,
244
+ *,
245
+ key_id: str,
246
+ secret: bytes,
247
+ pack: PackDefinition | None = None,
248
+ manifest_path: Path | None = None,
249
+ fold_catalog_dir: Path | None = None,
250
+ wicket_catalog_dir: Path | None = None,
251
+ operator: str = "local",
252
+ developer: str = "capsule-tenant-upgrade",
253
+ ) -> UpgradeResult:
254
+ """Re-materialize an already-provisioned tenant's manifest against the
255
+ currently-installed catalog (or a new pack version) and, only if the
256
+ resulting manifest digest actually changed, record a new activation
257
+ capsule chained to the tenant's previous one -- a new policy epoch, not
258
+ a silent rewrite. Idempotent: running this twice with nothing changed
259
+ upstream is a no-op on the ledger (``changed=False``), matching
260
+ ``install_pack``'s own idempotency guarantee for the files it writes.
261
+ Requires the tenant's *current* live signing key -- upgrading is a
262
+ normal write to that tenant's own epoch history, not a key rotation
263
+ (use ``capsule key rotate`` against this tenant's ledger for that)."""
264
+ layout = tenant_layout(tenants_root, tenant_id)
265
+ if not layout.metadata_path.is_file():
266
+ raise TenantProvisionError(
267
+ "not_initialized",
268
+ f"no {TENANT_METADATA_FILENAME} under {layout.root} -- run `capsule tenant init` first",
269
+ )
270
+ previous_metadata = json.loads(layout.metadata_path.read_text())
271
+
272
+ resolved = _materialize_manifest(
273
+ layout, pack=pack, manifest_path=manifest_path, fold_catalog_dir=fold_catalog_dir, wicket_catalog_dir=wicket_catalog_dir
274
+ )
275
+
276
+ if resolved.manifest_digest == previous_metadata.get("manifest_digest"):
277
+ return UpgradeResult(
278
+ layout=layout,
279
+ manifest_id=resolved.manifest.manifest_id,
280
+ manifest_digest=resolved.manifest_digest,
281
+ changed=False,
282
+ activation_capsule_id=previous_metadata.get("activation_capsule_id"),
283
+ )
284
+
285
+ signer = LocalSigner(key_id=key_id, secret=secret)
286
+ with LedgerStore(layout.ledger_dir) as store:
287
+ previous_activation = find_latest_activation(store)
288
+ capsule = build_manifest_activation_capsule(
289
+ resolved=resolved,
290
+ operator=operator,
291
+ developer=developer,
292
+ signer=signer,
293
+ previous_activation_capsule_id=previous_activation.capsule_id if previous_activation is not None else None,
294
+ )
295
+ store.append(capsule, consequential=False)
296
+
297
+ metadata = {
298
+ **previous_metadata,
299
+ "manifest_id": resolved.manifest.manifest_id,
300
+ "manifest_digest": resolved.manifest_digest,
301
+ "pack_id": pack.pack_id if pack is not None else previous_metadata.get("pack_id"),
302
+ "activation_capsule_id": capsule["capsule_id"],
303
+ }
304
+ layout.metadata_path.write_text(json.dumps(metadata, indent=2, sort_keys=True) + "\n")
305
+
306
+ return UpgradeResult(
307
+ layout=layout,
308
+ manifest_id=resolved.manifest.manifest_id,
309
+ manifest_digest=resolved.manifest_digest,
310
+ changed=True,
311
+ activation_capsule_id=capsule["capsule_id"],
312
+ )
313
+
314
+
315
+ def list_tenants(tenants_root: str | Path) -> list[dict]:
316
+ """Every provisioned tenant's ``tenant.json`` under ``tenants_root``,
317
+ sorted by tenant_id. A tenant directory with no metadata file (init
318
+ interrupted before completion) is skipped, not guessed at."""
319
+ root = Path(tenants_root)
320
+ if not root.is_dir():
321
+ return []
322
+ out = []
323
+ for child in sorted(root.iterdir()):
324
+ metadata_path = child / TENANT_METADATA_FILENAME
325
+ if metadata_path.is_file():
326
+ out.append(json.loads(metadata_path.read_text()))
327
+ return out
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: capsule-engine
3
+ Version: 0.1.0
4
+ Summary: The thin single-node engine: guard, append-only fold compute, packs, policy activation, and the local console/report/bundle-viewer runtime for AI agent actions.
5
+ Author: Action State Group
6
+ License-Expression: Apache-2.0 AND MIT
7
+ Project-URL: Homepage, https://github.com/action-state-group/capsule-engine
8
+ Project-URL: Source, https://github.com/action-state-group/capsule-engine
9
+ Keywords: agent,guard,fold,capsule,engine
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ License-File: capsule_engine/examples/data/tau2_airline/LICENSE-tau2-bench
16
+ Requires-Dist: agent-action-capsule>=0.11.0
17
+ Requires-Dist: checkpointed-local-log>=0.6.0
18
+ Requires-Dist: jsonschema>=4.18
19
+ Requires-Dist: capsule-emit>=0.12.0
20
+ Requires-Dist: pyyaml>=6.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7.0; extra == "dev"
23
+ Requires-Dist: ruff>=0.4; extra == "dev"
24
+ Requires-Dist: mcp<2.0,>=1.0; extra == "dev"
25
+ Requires-Dist: agentrust-trace>=0.10.0; extra == "dev"
26
+ Provides-Extra: mcp
27
+ Requires-Dist: mcp<2.0,>=1.0; extra == "mcp"
28
+ Provides-Extra: trace
29
+ Requires-Dist: agentrust-trace>=0.10.0; extra == "trace"
30
+ Dynamic: license-file
31
+
32
+ # capsule-engine
33
+
34
+ The thin single-node enforcement layer for AI agent actions: the guard, append-only fold
35
+ compute, the Terms Packs runtime, policy activation, and the local
36
+ console/report/bundle-viewer runtime.
37
+
38
+ ```
39
+ pip install capsule-engine
40
+ ```
41
+
42
+ It installs the `capsule_engine` package and the `capsule-engine` command. Its dependencies,
43
+ `agent-action-capsule`, `checkpointed-local-log`, `capsule-emit`, `jsonschema` and `pyyaml`, come
44
+ from PyPI.
45
+
46
+ The documentation, the Go module and the shared conformance vectors are in the repository:
47
+ <https://github.com/action-state-group/capsule-engine>.