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.
- capsule_engine/__init__.py +17 -0
- capsule_engine/bundle_viewer/__init__.py +21 -0
- capsule_engine/bundle_viewer/static/offline_shell.html +1509 -0
- capsule_engine/bundle_viewer/viewer.py +57 -0
- capsule_engine/cli/__init__.py +5 -0
- capsule_engine/cli/bundle_cmd.py +269 -0
- capsule_engine/cli/console_cmd.py +70 -0
- capsule_engine/cli/constraints_cmd.py +74 -0
- capsule_engine/cli/format.py +173 -0
- capsule_engine/cli/guard_cmds.py +361 -0
- capsule_engine/cli/init_cmds.py +169 -0
- capsule_engine/cli/main.py +120 -0
- capsule_engine/cli/packs_cmds.py +96 -0
- capsule_engine/cli/telemetry_cmd.py +80 -0
- capsule_engine/cli/tenant_cmds.py +199 -0
- capsule_engine/cli/verify_cmd.py +110 -0
- capsule_engine/compiler/__init__.py +13 -0
- capsule_engine/compiler/effect_model.py +79 -0
- capsule_engine/compiler/vocabulary.py +253 -0
- capsule_engine/console/__init__.py +10 -0
- capsule_engine/console/api.py +251 -0
- capsule_engine/console/components.css +406 -0
- capsule_engine/console/console.css +130 -0
- capsule_engine/console/console.html +51 -0
- capsule_engine/console/console.js +330 -0
- capsule_engine/console/gallery.html +274 -0
- capsule_engine/console/server.py +137 -0
- capsule_engine/console/tokens.css +68 -0
- capsule_engine/conversation/__init__.py +88 -0
- capsule_engine/conversation/capsules.py +268 -0
- capsule_engine/conversation/exchange.py +279 -0
- capsule_engine/conversation/merkle.py +80 -0
- capsule_engine/conversation/session.py +101 -0
- capsule_engine/envcompat.py +15 -0
- capsule_engine/events/__init__.py +14 -0
- capsule_engine/events/capsule.py +133 -0
- capsule_engine/examples/__init__.py +9 -0
- capsule_engine/examples/data/tau2_airline/LICENSE-tau2-bench +21 -0
- capsule_engine/examples/data/tau2_airline/PROVENANCE.json +70 -0
- capsule_engine/examples/data/tau2_airline/hand_labels.json +893 -0
- capsule_engine/examples/data/tau2_airline/pilot1_live_gemini-2-5-flash_airline_shift1.jsonl +190 -0
- capsule_engine/examples/data/tau2_airline/tau2_committed_claude-3-7-sonnet_airline_trial0.jsonl +556 -0
- capsule_engine/examples/data/tau2_airline/tau2_committed_gpt-4-1-mini_airline_trial0.jsonl +480 -0
- capsule_engine/examples/data/tau2_airline/tau2_committed_gpt-4-1_airline_trial0.jsonl +496 -0
- capsule_engine/examples/data/tau2_airline/tau2_committed_o4-mini_airline_trial0.jsonl +392 -0
- capsule_engine/examples/data/tau2_airline/tau2_conversations_claude-3-7-sonnet_airline_4trials.jsonl +200 -0
- capsule_engine/examples/tau2_airline_reference.py +460 -0
- capsule_engine/examples/tau2_conversation_exchange.py +92 -0
- capsule_engine/folds/__init__.py +104 -0
- capsule_engine/folds/account_core.py +89 -0
- capsule_engine/folds/approval_latency.py +86 -0
- capsule_engine/folds/catalog.py +79 -0
- capsule_engine/folds/catalog_defs/actions.count_by_developer.yaml +10 -0
- capsule_engine/folds/catalog_defs/actions.executed_count.yaml +16 -0
- capsule_engine/folds/catalog_defs/actions.last_decision.yaml +12 -0
- capsule_engine/folds/catalog_defs/counterparty.seen_before.v2.yaml +34 -0
- capsule_engine/folds/catalog_defs/counterparty.seen_before.v3.yaml +38 -0
- capsule_engine/folds/catalog_defs/counterparty.seen_before.yaml +28 -0
- capsule_engine/folds/catalog_defs/disclosure.class_seen.yaml +29 -0
- capsule_engine/folds/catalog_defs/disclosure.statement_seen.yaml +32 -0
- capsule_engine/folds/catalog_defs/hold.active_exposure.yaml +44 -0
- capsule_engine/folds/catalog_defs/spend.weekly.v2.yaml +38 -0
- capsule_engine/folds/catalog_defs/spend.weekly.v3.1.yaml +71 -0
- capsule_engine/folds/catalog_defs/spend.weekly.v3.yaml +76 -0
- capsule_engine/folds/catalog_defs/spend.weekly.yaml +33 -0
- capsule_engine/folds/counterparty_signals.py +179 -0
- capsule_engine/folds/definition.py +421 -0
- capsule_engine/folds/duration.py +16 -0
- capsule_engine/folds/engine.py +436 -0
- capsule_engine/folds/errors.py +44 -0
- capsule_engine/folds/loader.py +24 -0
- capsule_engine/folds/ordering.py +143 -0
- capsule_engine/folds/paths.py +25 -0
- capsule_engine/folds/record_type_coverage.py +58 -0
- capsule_engine/folds/reducers.py +75 -0
- capsule_engine/folds/retention_continuity.py +117 -0
- capsule_engine/folds/set_membership.py +59 -0
- capsule_engine/folds/taxonomy.py +231 -0
- capsule_engine/guards/__init__.py +59 -0
- capsule_engine/guards/action.py +446 -0
- capsule_engine/guards/action_taxonomy.json +27 -0
- capsule_engine/guards/action_taxonomy_v2.json +27 -0
- capsule_engine/guards/action_taxonomy_v3.json +27 -0
- capsule_engine/guards/action_taxonomy_v4.json +27 -0
- capsule_engine/guards/action_taxonomy_v5.json +27 -0
- capsule_engine/guards/capsule.py +377 -0
- capsule_engine/guards/checks/__init__.py +268 -0
- capsule_engine/guards/checks/action_class_gate.py +199 -0
- capsule_engine/guards/checks/base.py +33 -0
- capsule_engine/guards/checks/caps.py +517 -0
- capsule_engine/guards/checks/channel_change.py +64 -0
- capsule_engine/guards/checks/commitment.py +41 -0
- capsule_engine/guards/checks/counterparty_identity_change.py +95 -0
- capsule_engine/guards/checks/counterparty_list.py +176 -0
- capsule_engine/guards/checks/counterparty_seen_before.py +160 -0
- capsule_engine/guards/checks/credential_pattern.py +73 -0
- capsule_engine/guards/checks/dedupe.py +259 -0
- capsule_engine/guards/checks/destination_rail.py +120 -0
- capsule_engine/guards/checks/field_change_count.py +100 -0
- capsule_engine/guards/checks/offer_expiry.py +134 -0
- capsule_engine/guards/checks/plan_containment.py +180 -0
- capsule_engine/guards/checks/price_floor.py +211 -0
- capsule_engine/guards/checks/promise_never.py +236 -0
- capsule_engine/guards/checks/promise_requires_approval.py +214 -0
- capsule_engine/guards/checks/recipient_role.py +117 -0
- capsule_engine/guards/checks/recurring_charge.py +49 -0
- capsule_engine/guards/checks/refundability.py +46 -0
- capsule_engine/guards/checks/release_on_acceptance.py +286 -0
- capsule_engine/guards/checks/required_disclosure.py +233 -0
- capsule_engine/guards/checks/single_commitment.py +221 -0
- capsule_engine/guards/checks/task_authority.py +182 -0
- capsule_engine/guards/checks/upfront_amount.py +71 -0
- capsule_engine/guards/checks/verify_before_dispatch.py +75 -0
- capsule_engine/guards/classes.py +393 -0
- capsule_engine/guards/engine.py +926 -0
- capsule_engine/guards/history_state.py +127 -0
- capsule_engine/guards/plan.py +237 -0
- capsule_engine/guards/revocation.py +120 -0
- capsule_engine/guards/signing.py +60 -0
- capsule_engine/guards/statements.py +62 -0
- capsule_engine/guards/tool_call.py +83 -0
- capsule_engine/guards/wickets/__init__.py +23 -0
- capsule_engine/guards/wickets/catalog.py +75 -0
- capsule_engine/guards/wickets/catalog_defs/action_class_gate.v1.1.yaml +38 -0
- capsule_engine/guards/wickets/catalog_defs/action_class_gate.v1.2.yaml +52 -0
- capsule_engine/guards/wickets/catalog_defs/action_class_gate.yaml +34 -0
- capsule_engine/guards/wickets/catalog_defs/caps.v2.yaml +30 -0
- capsule_engine/guards/wickets/catalog_defs/caps.v3.yaml +49 -0
- capsule_engine/guards/wickets/catalog_defs/caps.v4.yaml +45 -0
- capsule_engine/guards/wickets/catalog_defs/caps.v5.1.yaml +53 -0
- capsule_engine/guards/wickets/catalog_defs/caps.v5.yaml +43 -0
- capsule_engine/guards/wickets/catalog_defs/caps.yaml +17 -0
- capsule_engine/guards/wickets/catalog_defs/caps_holds.yaml +18 -0
- capsule_engine/guards/wickets/catalog_defs/channel_change.v1.1.yaml +18 -0
- capsule_engine/guards/wickets/catalog_defs/channel_change.yaml +10 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_identity_change.yaml +9 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_list.yaml +20 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v2.yaml +15 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v3.yaml +23 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v4.yaml +26 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.yaml +14 -0
- capsule_engine/guards/wickets/catalog_defs/credential_pattern.yaml +14 -0
- capsule_engine/guards/wickets/catalog_defs/dedupe.yaml +19 -0
- capsule_engine/guards/wickets/catalog_defs/destination_rail.seller.v1.1.yaml +32 -0
- capsule_engine/guards/wickets/catalog_defs/destination_rail.seller.yaml +10 -0
- capsule_engine/guards/wickets/catalog_defs/destination_rail.yaml +9 -0
- capsule_engine/guards/wickets/catalog_defs/hold_reconcile.yaml +14 -0
- capsule_engine/guards/wickets/catalog_defs/material_fields_changed.v1.1.yaml +22 -0
- capsule_engine/guards/wickets/catalog_defs/material_fields_changed.yaml +14 -0
- capsule_engine/guards/wickets/catalog_defs/offer_expiry.v1.1.yaml +31 -0
- capsule_engine/guards/wickets/catalog_defs/offer_expiry.yaml +24 -0
- capsule_engine/guards/wickets/catalog_defs/offer_fields_changed.v1.1.yaml +22 -0
- capsule_engine/guards/wickets/catalog_defs/offer_fields_changed.yaml +10 -0
- capsule_engine/guards/wickets/catalog_defs/price_floor.yaml +32 -0
- capsule_engine/guards/wickets/catalog_defs/promise_never.v1.1.yaml +64 -0
- capsule_engine/guards/wickets/catalog_defs/promise_never.yaml +16 -0
- capsule_engine/guards/wickets/catalog_defs/promise_requires_approval.v1.1.yaml +26 -0
- capsule_engine/guards/wickets/catalog_defs/promise_requires_approval.yaml +21 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.v1.1.yaml +23 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.v1.2.yaml +24 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.yaml +10 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_role.v1.1.yaml +26 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_role.yaml +11 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_seen_before.yaml +16 -0
- capsule_engine/guards/wickets/catalog_defs/recurring_charge.yaml +9 -0
- capsule_engine/guards/wickets/catalog_defs/refundability.v1.1.yaml +15 -0
- capsule_engine/guards/wickets/catalog_defs/refundability.yaml +7 -0
- capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.v1.1.yaml +44 -0
- capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.v1.2.yaml +63 -0
- capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.yaml +36 -0
- capsule_engine/guards/wickets/catalog_defs/required_disclosure.v1.1.yaml +36 -0
- capsule_engine/guards/wickets/catalog_defs/required_disclosure.v1.2.yaml +42 -0
- capsule_engine/guards/wickets/catalog_defs/required_disclosure.yaml +25 -0
- capsule_engine/guards/wickets/catalog_defs/single_commitment.seller.yaml +14 -0
- capsule_engine/guards/wickets/catalog_defs/task_authority.v1.1.yaml +20 -0
- capsule_engine/guards/wickets/catalog_defs/task_authority.yaml +8 -0
- capsule_engine/guards/wickets/catalog_defs/upfront_amount.v1.1.yaml +19 -0
- capsule_engine/guards/wickets/catalog_defs/upfront_amount.yaml +11 -0
- capsule_engine/guards/wickets/catalog_defs/verify_before_dispatch.yaml +9 -0
- capsule_engine/guards/wickets/definition.py +164 -0
- capsule_engine/guards/wickets/errors.py +18 -0
- capsule_engine/guards/wickets/loader.py +26 -0
- capsule_engine/guards/wickets/retired.py +62 -0
- capsule_engine/holds/__init__.py +35 -0
- capsule_engine/holds/capsules.py +266 -0
- capsule_engine/holds/engine.py +476 -0
- capsule_engine/holds/errors.py +29 -0
- capsule_engine/holds/policy.py +62 -0
- capsule_engine/holds/scope.py +47 -0
- capsule_engine/mcp/__init__.py +16 -0
- capsule_engine/mcp/config.py +117 -0
- capsule_engine/mcp/server.py +338 -0
- capsule_engine/mcp/tools.py +424 -0
- capsule_engine/packaging.py +60 -0
- capsule_engine/packs/__init__.py +45 -0
- capsule_engine/packs/backfill_coverage.py +238 -0
- capsule_engine/packs/catalog/airline-engagement/AI-BOOTSTRAP.md +53 -0
- capsule_engine/packs/catalog/airline-engagement/folds/session_close.yaml +8 -0
- capsule_engine/packs/catalog/airline-engagement/pack.yaml +146 -0
- capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/good_execution.json +36 -0
- capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/oversight_declared.json +36 -0
- capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/tampered_execution.json +36 -0
- capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/trusted_issuer.jwk.json +5 -0
- capsule_engine/packs/catalog/eu-ai-act/obligations-pack-note.md +68 -0
- capsule_engine/packs/catalog/eu-ai-act/obligations-register.yaml +761 -0
- capsule_engine/packs/catalog/eu-ai-act/pack.yaml +88 -0
- capsule_engine/packs/catalog/eu-ai-act/register.yaml +96 -0
- capsule_engine/packs/catalog/eu-ai-act-deterministic/fixtures/synthetic_internal_assist_sample.jsonl +6 -0
- capsule_engine/packs/catalog/eu-ai-act-deterministic/pack.yaml +580 -0
- capsule_engine/packs/catalog/everyday/AI-BOOTSTRAP.md +194 -0
- capsule_engine/packs/catalog/everyday/fixtures/mini_ledger.jsonl +45 -0
- capsule_engine/packs/catalog/everyday/pack.yaml +480 -0
- capsule_engine/packs/catalog/payments-safety/AI-BOOTSTRAP.md +83 -0
- capsule_engine/packs/catalog/payments-safety/fixtures/mini_ledger.jsonl +8 -0
- capsule_engine/packs/catalog/payments-safety/folds/spend_weekly.yaml +29 -0
- capsule_engine/packs/catalog/payments-safety/pack.yaml +112 -0
- capsule_engine/packs/catalog/seller/fixtures/mini_ledger.jsonl +83 -0
- capsule_engine/packs/catalog/seller/pack.yaml +503 -0
- capsule_engine/packs/catalog/standard-vendor/AI-BOOTSTRAP.md +70 -0
- capsule_engine/packs/catalog/standard-vendor/folds/session_close.yaml +8 -0
- capsule_engine/packs/catalog/standard-vendor/pack.yaml +466 -0
- capsule_engine/packs/contract_diff.py +809 -0
- capsule_engine/packs/contract_validate.py +185 -0
- capsule_engine/packs/corpus_verify.py +126 -0
- capsule_engine/packs/enforce.py +81 -0
- capsule_engine/packs/errors.py +159 -0
- capsule_engine/packs/eu_ai_act_trace_field_map.py +46 -0
- capsule_engine/packs/install.py +266 -0
- capsule_engine/packs/loader.py +1462 -0
- capsule_engine/packs/measurability_report.py +280 -0
- capsule_engine/packs/obligation_results.py +72 -0
- capsule_engine/packs/pins.py +92 -0
- capsule_engine/packs/schema.py +1053 -0
- capsule_engine/packs/thresholds.py +146 -0
- capsule_engine/packs/trace_attainment.py +188 -0
- capsule_engine/packs/trace_reader.py +104 -0
- capsule_engine/policy/__init__.py +37 -0
- capsule_engine/policy/activation.py +87 -0
- capsule_engine/policy/catalog_defs/default.yaml +23 -0
- capsule_engine/policy/catalog_defs/holds.yaml +27 -0
- capsule_engine/policy/errors.py +46 -0
- capsule_engine/policy/limits.py +287 -0
- capsule_engine/policy/loader.py +26 -0
- capsule_engine/policy/manifest.py +289 -0
- capsule_engine/policy/profile.py +281 -0
- capsule_engine/policy/resolve.py +224 -0
- capsule_engine/register/__init__.py +39 -0
- capsule_engine/register/compiler.py +219 -0
- capsule_engine/register/errors.py +40 -0
- capsule_engine/register/loader.py +247 -0
- capsule_engine/register/schema.py +129 -0
- capsule_engine/registry/__init__.py +19 -0
- capsule_engine/registry/conventions.json +162 -0
- capsule_engine/registry/conventions.py +188 -0
- capsule_engine/registry/cpb_registry.json +78 -0
- capsule_engine/report/__init__.py +62 -0
- capsule_engine/report/build.py +267 -0
- capsule_engine/report/coverage.py +777 -0
- capsule_engine/report/errors.py +35 -0
- capsule_engine/report/live_check.py +126 -0
- capsule_engine/report/live_history.py +420 -0
- capsule_engine/report/model.py +95 -0
- capsule_engine/report/render.py +467 -0
- capsule_engine/report/replay.py +1677 -0
- capsule_engine/report/result.py +504 -0
- capsule_engine/report/result_from_folds.py +222 -0
- capsule_engine/report/result_from_layers.py +670 -0
- capsule_engine/report/sale_bundle.py +864 -0
- capsule_engine/report/static/verify.js +451 -0
- capsule_engine/schemas/evidence-contract-v0.json +383 -0
- capsule_engine/schemas/vendor/epistemic-types.json +21 -0
- capsule_engine/schemas/vendor/evidence-result-v0.json +463 -0
- capsule_engine/telemetry/__init__.py +9 -0
- capsule_engine/telemetry/consent.py +39 -0
- capsule_engine/telemetry/events.py +93 -0
- capsule_engine/telemetry/fixtures/synthetic_events.jsonl +45 -0
- capsule_engine/telemetry/funnel.py +147 -0
- capsule_engine/telemetry/record.py +106 -0
- capsule_engine/telemetry/sink.py +61 -0
- capsule_engine/telemetry/state.py +60 -0
- capsule_engine/tenants.py +327 -0
- capsule_engine-0.1.0.dist-info/METADATA +47 -0
- capsule_engine-0.1.0.dist-info/RECORD +288 -0
- capsule_engine-0.1.0.dist-info/WHEEL +5 -0
- capsule_engine-0.1.0.dist-info/entry_points.txt +2 -0
- capsule_engine-0.1.0.dist-info/licenses/LICENSE +202 -0
- capsule_engine-0.1.0.dist-info/licenses/capsule_engine/examples/data/tau2_airline/LICENSE-tau2-bench +21 -0
- 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>.
|