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,96 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""``capsule-engine packs`` verbs -- the packs-runtime's own CLI surface.
|
|
3
|
+
|
|
4
|
+
propose -- the GENERIC, READ-ONLY "would this pack
|
|
5
|
+
work" measurability report for ANY pack: resolves/MISSING-
|
|
6
|
+
INSTRUMENT per outcome, from the pack's own tier/mode/
|
|
7
|
+
evidence_instrument fields, over a JSONL corpus. Ported from
|
|
8
|
+
capsule-ledger's ``capsule setup propose --pack`` (moved here
|
|
9
|
+
-- the packs runtime's home) --
|
|
10
|
+
this mode never touches ``.capsule-setup/`` and persists
|
|
11
|
+
nothing, so it carries no dependency on the setup/Candidate
|
|
12
|
+
machinery that stayed in capsule-compiler.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from ..packs import PackDefinitionError, load_pack_dir
|
|
22
|
+
from ..packs import measurability_report as pack_measurability_report
|
|
23
|
+
|
|
24
|
+
__all__ = ["add_parser"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_units_jsonl(path: str) -> list[dict]:
|
|
28
|
+
units = []
|
|
29
|
+
with open(path, encoding="utf-8") as fh:
|
|
30
|
+
for line in fh:
|
|
31
|
+
line = line.strip()
|
|
32
|
+
if line:
|
|
33
|
+
units.append(json.loads(line))
|
|
34
|
+
return units
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _cmd_packs_propose(args: argparse.Namespace) -> int:
|
|
38
|
+
"""READ-ONLY: prints resolves/MISSING-INSTRUMENT per outcome, persists
|
|
39
|
+
nothing -- there are no T1 declarations to confirm afterward. Confirmable
|
|
40
|
+
persistence for a generic pack is a separate design decision (a
|
|
41
|
+
MeasurabilityRow is not a Candidate/ProposedOutcome), not built here."""
|
|
42
|
+
if args.corpus is None:
|
|
43
|
+
print("capsule-engine packs propose: --corpus is required", file=sys.stderr)
|
|
44
|
+
return 2
|
|
45
|
+
if args.entity_key is None:
|
|
46
|
+
print(
|
|
47
|
+
"capsule-engine packs propose: --entity-key is required (no default -- names the unit field "
|
|
48
|
+
"identifying a repeat entity for fold_counterparty/fold_cohort rows, e.g. task_id, session_id)",
|
|
49
|
+
file=sys.stderr,
|
|
50
|
+
)
|
|
51
|
+
return 2
|
|
52
|
+
try:
|
|
53
|
+
pack = load_pack_dir(Path(args.pack))
|
|
54
|
+
except PackDefinitionError as exc:
|
|
55
|
+
print(f"capsule-engine packs propose: pack failed to load ({exc.reason}): {exc}", file=sys.stderr)
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
units = _load_units_jsonl(args.corpus)
|
|
59
|
+
key = args.entity_key
|
|
60
|
+
try:
|
|
61
|
+
report = pack_measurability_report.build_measurability_report(
|
|
62
|
+
pack, units, entity_key=pack_measurability_report.entity_key_field(key)
|
|
63
|
+
)
|
|
64
|
+
except pack_measurability_report.MissingEntityKeyField as exc:
|
|
65
|
+
print(f"capsule-engine packs propose: {exc}", file=sys.stderr)
|
|
66
|
+
return 2
|
|
67
|
+
|
|
68
|
+
print(f"pack: {pack.pack_id}")
|
|
69
|
+
print(f"corpus: {args.corpus} ({len(units)} unit(s))")
|
|
70
|
+
print(f"entity_key: {key}")
|
|
71
|
+
print("READ-ONLY report -- no declarations persisted")
|
|
72
|
+
print()
|
|
73
|
+
print(pack_measurability_report.render_terminal(report), end="")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def add_parser(sub: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
78
|
+
packs = sub.add_parser("packs", help="packs-runtime verbs: propose (generic measurability report)")
|
|
79
|
+
packs_sub = packs.add_subparsers(dest="packs_command")
|
|
80
|
+
packs.set_defaults(packs_parser=packs)
|
|
81
|
+
|
|
82
|
+
p_propose = packs_sub.add_parser(
|
|
83
|
+
"propose",
|
|
84
|
+
help="GENERIC READ-ONLY measurability report for any pack over a JSONL corpus",
|
|
85
|
+
)
|
|
86
|
+
p_propose.add_argument("--pack", required=True, help="path to a pack directory (pack.yaml)")
|
|
87
|
+
p_propose.add_argument("--corpus", default=None, help="path to a JSONL file of units shaped {'messages': [...]}")
|
|
88
|
+
p_propose.add_argument(
|
|
89
|
+
"--entity-key",
|
|
90
|
+
default=None,
|
|
91
|
+
help="REQUIRED, no default: the unit field identifying a repeat entity for fold_counterparty/"
|
|
92
|
+
"fold_cohort rows (e.g. task_id, session_id)",
|
|
93
|
+
)
|
|
94
|
+
p_propose.set_defaults(func=_cmd_packs_propose)
|
|
95
|
+
|
|
96
|
+
return packs
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`capsule telemetry`: disclosure/status, and the 6-metric funnel report
|
|
3
|
+
generator -- a reporting tool over already-collected events, not the
|
|
4
|
+
collection itself. See ``capsule_ledger/telemetry/`` for the instrumentation."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from importlib import resources
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
__all__ = ["add_parser"]
|
|
14
|
+
|
|
15
|
+
_SYNTHETIC_FIXTURE = "synthetic_events.jsonl"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _cmd_status(_args: argparse.Namespace) -> int:
|
|
19
|
+
from ..telemetry.consent import DISCLOSURE_TEXT, is_opted_in
|
|
20
|
+
from ..telemetry.state import state_path
|
|
21
|
+
|
|
22
|
+
print(DISCLOSURE_TEXT)
|
|
23
|
+
print(f"currently: {'ON' if is_opted_in() else 'OFF (default)'}")
|
|
24
|
+
if is_opted_in():
|
|
25
|
+
print(f"local state file: {state_path()}")
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_events(path: str | None, *, use_fixture: bool) -> list[dict]:
|
|
30
|
+
if use_fixture:
|
|
31
|
+
text = resources.files("capsule_engine.telemetry").joinpath("fixtures", _SYNTHETIC_FIXTURE).read_text(
|
|
32
|
+
encoding="utf-8"
|
|
33
|
+
)
|
|
34
|
+
else:
|
|
35
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
36
|
+
return [json.loads(line) for line in text.splitlines() if line.strip()]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _cmd_funnel(args: argparse.Namespace) -> int:
|
|
40
|
+
from ..telemetry.funnel import compute_funnel, render_funnel_report
|
|
41
|
+
|
|
42
|
+
if not args.dry_run and not args.events:
|
|
43
|
+
print("capsule telemetry funnel: --events PATH is required unless --dry-run is given", file=sys.stderr)
|
|
44
|
+
return 2
|
|
45
|
+
|
|
46
|
+
raw_events = _load_events(args.events, use_fixture=args.dry_run)
|
|
47
|
+
report = compute_funnel(raw_events)
|
|
48
|
+
text = render_funnel_report(report)
|
|
49
|
+
|
|
50
|
+
if args.dry_run:
|
|
51
|
+
text = "(dry run -- rendered against bundled synthetic fixture data, no real telemetry)\n\n" + text
|
|
52
|
+
|
|
53
|
+
if args.out:
|
|
54
|
+
Path(args.out).write_text(text, encoding="utf-8")
|
|
55
|
+
print(f"wrote {args.out}")
|
|
56
|
+
else:
|
|
57
|
+
print(text)
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def add_parser(sub: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
62
|
+
telemetry = sub.add_parser("telemetry", help="telemetry disclosure/status, and the 6-metric funnel report")
|
|
63
|
+
telemetry_sub = telemetry.add_subparsers(dest="telemetry_command")
|
|
64
|
+
telemetry.set_defaults(telemetry_parser=telemetry)
|
|
65
|
+
|
|
66
|
+
p_status = telemetry_sub.add_parser("status", help="show the disclosure text and current opt-in state")
|
|
67
|
+
p_status.set_defaults(func=_cmd_status)
|
|
68
|
+
|
|
69
|
+
p_funnel = telemetry_sub.add_parser(
|
|
70
|
+
"funnel", help="render the 6-metric funnel report over collected telemetry (or synthetic fixture data)"
|
|
71
|
+
)
|
|
72
|
+
p_funnel.add_argument("--events", help="path to a JSONL file of collected telemetry events")
|
|
73
|
+
p_funnel.add_argument(
|
|
74
|
+
"--dry-run", action="store_true",
|
|
75
|
+
help="render against bundled synthetic/fixture data instead of --events (no real telemetry required)",
|
|
76
|
+
)
|
|
77
|
+
p_funnel.add_argument("--out", help="write the report to this path instead of stdout")
|
|
78
|
+
p_funnel.set_defaults(func=_cmd_funnel)
|
|
79
|
+
|
|
80
|
+
return telemetry
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""``capsule tenant`` verbs: provision/upgrade/list engine-instance-per-tenant
|
|
3
|
+
deployments (``capsule_ledger.tenants``) -- the "run one whole engine
|
|
4
|
+
instance per tenant" pattern for a caller embedding this package for many
|
|
5
|
+
customers. See ``docs/tenant-provisioning.md`` for the layout and the
|
|
6
|
+
isolation rationale.
|
|
7
|
+
|
|
8
|
+
init -- provision a brand-new tenant: fresh ledger dir, this tenant's
|
|
9
|
+
own pinned manifest, first activation capsule. Refuses if the
|
|
10
|
+
tenant already has anything on disk.
|
|
11
|
+
upgrade -- re-materialize an existing tenant's manifest (e.g. after a
|
|
12
|
+
pack version bump) and record a new activation epoch, only if
|
|
13
|
+
the manifest actually changed. Refuses if the tenant was never
|
|
14
|
+
initialized.
|
|
15
|
+
list -- print every provisioned tenant's id/manifest/key under a
|
|
16
|
+
tenants-root, from their own tenant.json files.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from ..envcompat import env_get
|
|
25
|
+
from ..packs import PackDefinitionError, load_pack_dir
|
|
26
|
+
from ..tenants import TenantProvisionError, init_tenant, list_tenants, upgrade_tenant
|
|
27
|
+
|
|
28
|
+
__all__ = ["add_parser"]
|
|
29
|
+
|
|
30
|
+
BUILTIN_PACK_CATALOG_DIR = Path(__file__).resolve().parent.parent / "packs" / "catalog"
|
|
31
|
+
|
|
32
|
+
_KEY_ID_ENV = "CAPSULE_MCP_SIGNING_KEY_ID"
|
|
33
|
+
_SECRET_ENV = "CAPSULE_MCP_SIGNING_SECRET"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _available_packs(catalog_dir: Path) -> list[str]:
|
|
37
|
+
if not catalog_dir.is_dir():
|
|
38
|
+
return []
|
|
39
|
+
return sorted(p.name for p in catalog_dir.iterdir() if (p / "pack.yaml").is_file())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _load_pack(args: argparse.Namespace, *, verb: str):
|
|
43
|
+
"""Shared ``--pack <name>`` resolution, same lookup ``capsule init``
|
|
44
|
+
uses -- returns ``None`` (default manifest) when ``--pack`` is omitted,
|
|
45
|
+
or a ``(pack, None)``/``(None, error_code)`` pair on failure."""
|
|
46
|
+
if not args.pack:
|
|
47
|
+
return None, None
|
|
48
|
+
catalog_dir = Path(args.pack_catalog_dir) if args.pack_catalog_dir else BUILTIN_PACK_CATALOG_DIR
|
|
49
|
+
pack_dir = catalog_dir / args.pack
|
|
50
|
+
if not (pack_dir / "pack.yaml").is_file():
|
|
51
|
+
available = _available_packs(catalog_dir)
|
|
52
|
+
print(f"capsule tenant {verb}: no pack named {args.pack!r} in {catalog_dir}", file=sys.stderr)
|
|
53
|
+
print(f" available packs: {available or '<none>'}", file=sys.stderr)
|
|
54
|
+
return None, 2
|
|
55
|
+
try:
|
|
56
|
+
return load_pack_dir(pack_dir), None
|
|
57
|
+
except PackDefinitionError as exc:
|
|
58
|
+
print(f"capsule tenant {verb}: pack {args.pack!r} failed to load ({exc.reason}): {exc}", file=sys.stderr)
|
|
59
|
+
return None, 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _cmd_tenant_init(args: argparse.Namespace) -> int:
|
|
63
|
+
pack, error_rc = _load_pack(args, verb="init")
|
|
64
|
+
if error_rc is not None:
|
|
65
|
+
return error_rc
|
|
66
|
+
|
|
67
|
+
key_id = args.key_id or env_get(_KEY_ID_ENV)
|
|
68
|
+
secret_text = args.secret or env_get(_SECRET_ENV)
|
|
69
|
+
secret = secret_text.encode("utf-8") if secret_text is not None else None
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
result = init_tenant(
|
|
73
|
+
args.tenants_root,
|
|
74
|
+
args.tenant_id,
|
|
75
|
+
pack=pack,
|
|
76
|
+
key_id=key_id,
|
|
77
|
+
secret=secret,
|
|
78
|
+
operator=args.operator,
|
|
79
|
+
developer=args.developer,
|
|
80
|
+
)
|
|
81
|
+
except TenantProvisionError as exc:
|
|
82
|
+
print(f"capsule tenant init: {exc.reason}: {exc}", file=sys.stderr)
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
print(f"provisioned tenant {args.tenant_id!r} at {result.layout.root}")
|
|
86
|
+
print(f" ledger: {result.layout.ledger_dir}")
|
|
87
|
+
print(f" manifest: {result.layout.manifest_path}")
|
|
88
|
+
print(f" manifest id: {result.manifest_id}")
|
|
89
|
+
print(f" manifest digest: {result.manifest_digest}")
|
|
90
|
+
print(f" activation: {result.activation_capsule_id}")
|
|
91
|
+
print(f" key id: {result.key_id}")
|
|
92
|
+
if result.generated_secret:
|
|
93
|
+
print()
|
|
94
|
+
print(
|
|
95
|
+
"signing secret (shown once -- this command does not persist it anywhere; "
|
|
96
|
+
"store it in this tenant's own secret manager, not this repo or terminal history):"
|
|
97
|
+
)
|
|
98
|
+
print(f" {result.secret.decode('utf-8')}")
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _cmd_tenant_upgrade(args: argparse.Namespace) -> int:
|
|
103
|
+
pack, error_rc = _load_pack(args, verb="upgrade")
|
|
104
|
+
if error_rc is not None:
|
|
105
|
+
return error_rc
|
|
106
|
+
|
|
107
|
+
key_id = args.key_id or env_get(_KEY_ID_ENV)
|
|
108
|
+
secret_text = args.secret or env_get(_SECRET_ENV)
|
|
109
|
+
if not key_id or not secret_text:
|
|
110
|
+
print(
|
|
111
|
+
f"capsule tenant upgrade: --key-id/--secret are required (or set ${_KEY_ID_ENV}/${_SECRET_ENV}) -- "
|
|
112
|
+
"this must be the tenant's current live signing key, not a fresh one",
|
|
113
|
+
file=sys.stderr,
|
|
114
|
+
)
|
|
115
|
+
return 2
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
result = upgrade_tenant(
|
|
119
|
+
args.tenants_root,
|
|
120
|
+
args.tenant_id,
|
|
121
|
+
pack=pack,
|
|
122
|
+
key_id=key_id,
|
|
123
|
+
secret=secret_text.encode("utf-8"),
|
|
124
|
+
operator=args.operator,
|
|
125
|
+
developer=args.developer,
|
|
126
|
+
)
|
|
127
|
+
except TenantProvisionError as exc:
|
|
128
|
+
print(f"capsule tenant upgrade: {exc.reason}: {exc}", file=sys.stderr)
|
|
129
|
+
return 1
|
|
130
|
+
|
|
131
|
+
if not result.changed:
|
|
132
|
+
print(f"tenant {args.tenant_id!r}: manifest unchanged ({result.manifest_digest}), nothing to activate")
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
print(f"upgraded tenant {args.tenant_id!r} at {result.layout.root}")
|
|
136
|
+
print(f" manifest id: {result.manifest_id}")
|
|
137
|
+
print(f" manifest digest: {result.manifest_digest}")
|
|
138
|
+
print(f" activation: {result.activation_capsule_id}")
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _cmd_tenant_list(args: argparse.Namespace) -> int:
|
|
143
|
+
tenants = list_tenants(args.tenants_root)
|
|
144
|
+
if not tenants:
|
|
145
|
+
print(f"no provisioned tenants under {args.tenants_root}")
|
|
146
|
+
return 0
|
|
147
|
+
for tenant in tenants:
|
|
148
|
+
print(
|
|
149
|
+
f"{tenant['tenant_id']}\t{tenant['manifest_id']}\t{tenant['manifest_digest'][:16]}…\t"
|
|
150
|
+
f"key={tenant['key_id']}"
|
|
151
|
+
)
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _add_pack_args(p: argparse.ArgumentParser) -> None:
|
|
156
|
+
p.add_argument(
|
|
157
|
+
"--pack", default=None, help="starter pack name to install for this tenant (default: none, built-in default manifest)"
|
|
158
|
+
)
|
|
159
|
+
p.add_argument(
|
|
160
|
+
"--pack-catalog-dir", default=None, help="override the built-in pack catalog directory (mainly for tests)"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def add_parser(sub: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
165
|
+
tenant = sub.add_parser(
|
|
166
|
+
"tenant", help="engine-instance-per-tenant provisioning: init/upgrade/list (see docs/tenant-provisioning.md)"
|
|
167
|
+
)
|
|
168
|
+
tenant_sub = tenant.add_subparsers(dest="tenant_command")
|
|
169
|
+
tenant.set_defaults(tenant_parser=tenant)
|
|
170
|
+
|
|
171
|
+
p_init = tenant_sub.add_parser(
|
|
172
|
+
"init", help="provision a brand-new tenant instance (ledger dir + manifest + first activation)"
|
|
173
|
+
)
|
|
174
|
+
p_init.add_argument("--tenants-root", required=True, help="directory holding one subdirectory per tenant")
|
|
175
|
+
p_init.add_argument("--tenant-id", required=True, help="this tenant's id (becomes a directory name -- lowercase, digits, hyphens)")
|
|
176
|
+
_add_pack_args(p_init)
|
|
177
|
+
p_init.add_argument("--operator", default="local", help="operator identity for the first activation capsule (default: 'local')")
|
|
178
|
+
p_init.add_argument("--developer", default="capsule-tenant-init", help="developer identity for the first activation capsule")
|
|
179
|
+
p_init.add_argument("--key-id", default=None, help=f"this tenant's signing key id (default: '<tenant-id>-signing-key', or ${_KEY_ID_ENV})")
|
|
180
|
+
p_init.add_argument("--secret", default=None, help=f"this tenant's signing key secret (default: ${_SECRET_ENV}, or freshly generated)")
|
|
181
|
+
p_init.set_defaults(func=_cmd_tenant_init)
|
|
182
|
+
|
|
183
|
+
p_upgrade = tenant_sub.add_parser(
|
|
184
|
+
"upgrade", help="re-materialize an existing tenant's manifest and record a new activation epoch if it changed"
|
|
185
|
+
)
|
|
186
|
+
p_upgrade.add_argument("--tenants-root", required=True, help="directory holding one subdirectory per tenant")
|
|
187
|
+
p_upgrade.add_argument("--tenant-id", required=True, help="this tenant's id")
|
|
188
|
+
_add_pack_args(p_upgrade)
|
|
189
|
+
p_upgrade.add_argument("--operator", default="local", help="operator identity for the new activation capsule (default: 'local')")
|
|
190
|
+
p_upgrade.add_argument("--developer", default="capsule-tenant-upgrade", help="developer identity for the new activation capsule")
|
|
191
|
+
p_upgrade.add_argument("--key-id", default=None, help=f"this tenant's CURRENT live signing key id (default: ${_KEY_ID_ENV}) -- required")
|
|
192
|
+
p_upgrade.add_argument("--secret", default=None, help=f"this tenant's CURRENT live signing key secret (default: ${_SECRET_ENV}) -- required")
|
|
193
|
+
p_upgrade.set_defaults(func=_cmd_tenant_upgrade)
|
|
194
|
+
|
|
195
|
+
p_list = tenant_sub.add_parser("list", help="list every provisioned tenant under a tenants-root")
|
|
196
|
+
p_list.add_argument("--tenants-root", required=True, help="directory holding one subdirectory per tenant")
|
|
197
|
+
p_list.set_defaults(func=_cmd_tenant_list)
|
|
198
|
+
|
|
199
|
+
return tenant
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`capsule verify`: verify one ledger record, or an entire bundle file offline.
|
|
3
|
+
|
|
4
|
+
Exit codes (CI-friendly, and distinguished because "not found" and "found
|
|
5
|
+
but tampered" are different failure modes a caller may want to branch on):
|
|
6
|
+
0 verified clean
|
|
7
|
+
1 verification failed (digest mismatch, chain gap, etc. -- ``result.ok`` is False)
|
|
8
|
+
2 usage error (no capsule_id/--bundle given, or the capsule/bundle wasn't found)
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from agent_action_capsule import verify as verify_capsule
|
|
17
|
+
from capsule_emit.ledger_io import open_ledger, require_ledger_path
|
|
18
|
+
|
|
19
|
+
from .format import build_echo
|
|
20
|
+
|
|
21
|
+
__all__ = ["add_parser", "run"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def add_parser(sub: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
25
|
+
p = sub.add_parser("verify", help="verify one ledger record, or an entire bundle file")
|
|
26
|
+
p.add_argument("capsule_id", nargs="?", help="a capsule_id or an unambiguous prefix (omit with --bundle)")
|
|
27
|
+
p.add_argument("--ledger", help="ledger store directory or a JSONL fixture file (default: $CAPSULE_LEDGER)")
|
|
28
|
+
p.add_argument(
|
|
29
|
+
"--bundle", help="verify every record in a bundle file produced by `capsule bundle`, offline and self-contained"
|
|
30
|
+
)
|
|
31
|
+
p.add_argument("--json", action="store_true", help="print the raw verification result(s) as JSON")
|
|
32
|
+
p.set_defaults(func=run)
|
|
33
|
+
return p
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _print_result(capsule_id: str, result) -> bool:
|
|
37
|
+
if result.ok:
|
|
38
|
+
print(f"✓ verifies · {capsule_id}")
|
|
39
|
+
return True
|
|
40
|
+
print(f"✗ verification failed · {capsule_id}")
|
|
41
|
+
for finding in result.findings:
|
|
42
|
+
print(f" - {finding.code}: {finding.detail}")
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run(args: argparse.Namespace) -> int:
|
|
47
|
+
# ``verify`` only exists at all in the "full" packaging arm -- see
|
|
48
|
+
# ``cli/main.py`` -- so any use of it is M5's "verify run" fact.
|
|
49
|
+
from ..telemetry.record import record_evidence_touch
|
|
50
|
+
|
|
51
|
+
record_evidence_touch("full")
|
|
52
|
+
|
|
53
|
+
if args.bundle:
|
|
54
|
+
return _run_bundle(args)
|
|
55
|
+
if not args.capsule_id:
|
|
56
|
+
print("capsule verify: capsule_id is required unless --bundle is given", file=sys.stderr)
|
|
57
|
+
return 2
|
|
58
|
+
|
|
59
|
+
ledger_path = require_ledger_path("verify", args)
|
|
60
|
+
if ledger_path is None:
|
|
61
|
+
return 2
|
|
62
|
+
|
|
63
|
+
with open_ledger(ledger_path) as store:
|
|
64
|
+
record = store.fetch(args.capsule_id)
|
|
65
|
+
if record is None:
|
|
66
|
+
print(f"no such capsule {args.capsule_id!r} in ledger {ledger_path}", file=sys.stderr)
|
|
67
|
+
return 2
|
|
68
|
+
result = store.verify(record.capsule_id)
|
|
69
|
+
capsule_id = record.capsule_id
|
|
70
|
+
|
|
71
|
+
if args.json:
|
|
72
|
+
print(
|
|
73
|
+
json.dumps(
|
|
74
|
+
{
|
|
75
|
+
"capsule_id": capsule_id,
|
|
76
|
+
"ok": result.ok,
|
|
77
|
+
"findings": [{"code": f.code, "detail": f.detail, "severity": f.severity} for f in result.findings],
|
|
78
|
+
},
|
|
79
|
+
indent=2,
|
|
80
|
+
sort_keys=True,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return 0 if result.ok else 1
|
|
84
|
+
|
|
85
|
+
ok = _print_result(capsule_id, result)
|
|
86
|
+
print()
|
|
87
|
+
print(build_echo("verify", positional=capsule_id))
|
|
88
|
+
return 0 if ok else 1
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _run_bundle(args: argparse.Namespace) -> int:
|
|
92
|
+
try:
|
|
93
|
+
with open(args.bundle, encoding="utf-8") as fh:
|
|
94
|
+
bundle = json.load(fh)
|
|
95
|
+
except OSError as exc:
|
|
96
|
+
print(f"capsule verify: cannot read bundle {args.bundle!r}: {exc}", file=sys.stderr)
|
|
97
|
+
return 2
|
|
98
|
+
|
|
99
|
+
records = bundle.get("records", [])
|
|
100
|
+
ids = [r.get("capsule_id") for r in records]
|
|
101
|
+
all_ok = True
|
|
102
|
+
for capsule in records:
|
|
103
|
+
result = verify_capsule(capsule, store=ids)
|
|
104
|
+
ok = _print_result(capsule.get("capsule_id", "?"), result)
|
|
105
|
+
all_ok = all_ok and ok
|
|
106
|
+
|
|
107
|
+
print()
|
|
108
|
+
status = "verifies clean" if all_ok else "verification FAILED"
|
|
109
|
+
print(f"bundle {args.bundle}: {len(records)} record(s), {status}")
|
|
110
|
+
return 0 if all_ok else 1
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Shared foundation for the outcome compiler: closed vocabulary and the
|
|
3
|
+
advisory effect model.
|
|
4
|
+
|
|
5
|
+
This subpackage holds ONLY the pure, dependency-free pieces of the outcome
|
|
6
|
+
compiler that ``capsule_engine.packs.loader`` needs at pack-load time to
|
|
7
|
+
validate ``outcomes[]`` entries. Neither module here touches
|
|
8
|
+
``guards.capsule``/``guards.signing`` -- the sealed-capsule builders
|
|
9
|
+
(compilation record, offer/response, refusal, scope census) stay in the
|
|
10
|
+
separate ``capsule-compiler`` repo, which imports these two modules as
|
|
11
|
+
``capsule_engine.compiler.vocabulary`` / ``capsule_engine.compiler.effect_model``.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The advisory effect model (design §4b gap 1): every effect model this
|
|
3
|
+
codebase had before this wave assumed the agent caused the state change it
|
|
4
|
+
observed. A read-only advisory deployment's agent cannot act at all -- a
|
|
5
|
+
person acts on its recommendations manually -- so any field that quietly
|
|
6
|
+
encodes agent-causation breaks visibly there instead of invisibly at a
|
|
7
|
+
deployment that never falsifies it.
|
|
8
|
+
|
|
9
|
+
Three claims, closed set. Two are legitimate near-misses; the third MUST be
|
|
10
|
+
refused at compile time, and this module is where that refusal is
|
|
11
|
+
mechanical rather than a reviewer's judgment call: ``compile_effect_claim``
|
|
12
|
+
returns a verdict pair plus (for the refused claim) the reason code, and
|
|
13
|
+
raises nothing -- refusing IS the successful, expected return for that
|
|
14
|
+
claim, not an exceptional path. ``packs/loader.py`` is what turns an
|
|
15
|
+
``EvidenceContract`` that gets this wrong into a load-time error (an outcome
|
|
16
|
+
cannot declare ``effect_claim="agent.caused_resolution"`` with anything
|
|
17
|
+
other than the verdict pair this module computes for it).
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
|
|
23
|
+
from .vocabulary import VerdictPair
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ADMISSIBLE_EFFECT_CLAIMS",
|
|
27
|
+
"REFUSED_EFFECT_CLAIMS",
|
|
28
|
+
"EFFECT_CLAIMS",
|
|
29
|
+
"UnknownEffectClaim",
|
|
30
|
+
"EffectClaimCompilation",
|
|
31
|
+
"compile_effect_claim",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# The two legitimate near-misses (design §4b gap 1). Both are recordable
|
|
35
|
+
# without asserting causation: a human's action can be cited by its own
|
|
36
|
+
# capsule (``recommendation.acted_on``) or the ordering of a resolution
|
|
37
|
+
# after an action can be cited without asserting the action produced it
|
|
38
|
+
# (``resolution.followed_action``).
|
|
39
|
+
ADMISSIBLE_EFFECT_CLAIMS = frozenset({"recommendation.acted_on", "resolution.followed_action"})
|
|
40
|
+
|
|
41
|
+
# The undecomposable claim. A resolved ticket following a recommendation is
|
|
42
|
+
# correlation, not causation -- the record can honestly attest the
|
|
43
|
+
# recommendation was made, a human acted, the ticket resolved, and the
|
|
44
|
+
# elapsed order of those, but never that the agent caused the resolution.
|
|
45
|
+
REFUSED_EFFECT_CLAIMS = frozenset({"agent.caused_resolution"})
|
|
46
|
+
|
|
47
|
+
EFFECT_CLAIMS = ADMISSIBLE_EFFECT_CLAIMS | REFUSED_EFFECT_CLAIMS
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class UnknownEffectClaim(ValueError):
|
|
51
|
+
"""``effect_claim`` is not one of the closed ``EFFECT_CLAIMS`` set --
|
|
52
|
+
same "unregistered is a typo" doctrine as every other closed vocabulary
|
|
53
|
+
in this repo; this is not an open, registry-resolved vocabulary."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class EffectClaimCompilation:
|
|
58
|
+
effect_claim: str
|
|
59
|
+
verdict: VerdictPair
|
|
60
|
+
refusal_reason_code: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def compile_effect_claim(effect_claim: str) -> EffectClaimCompilation:
|
|
64
|
+
"""Compile one effect claim into its verdict pair. The refused claim
|
|
65
|
+
compiles successfully -- to a REFUSED/REFUSED pair with its reason code
|
|
66
|
+
-- rather than raising, because the refusal itself is the correct,
|
|
67
|
+
recordable outcome (design §4: "a report that visibly refuses one claim
|
|
68
|
+
is a report whose other claims get believed")."""
|
|
69
|
+
if effect_claim in ADMISSIBLE_EFFECT_CLAIMS:
|
|
70
|
+
return EffectClaimCompilation(
|
|
71
|
+
effect_claim=effect_claim, verdict=VerdictPair(forward="DETERMINISTIC", backward="DETERMINISTIC")
|
|
72
|
+
)
|
|
73
|
+
if effect_claim in REFUSED_EFFECT_CLAIMS:
|
|
74
|
+
return EffectClaimCompilation(
|
|
75
|
+
effect_claim=effect_claim,
|
|
76
|
+
verdict=VerdictPair(forward="REFUSED", backward="REFUSED"),
|
|
77
|
+
refusal_reason_code="agent_caused_resolution_undecomposable",
|
|
78
|
+
)
|
|
79
|
+
raise UnknownEffectClaim(f"effect_claim must be one of {sorted(EFFECT_CLAIMS)}; got {effect_claim!r}")
|