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,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())
|