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,57 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Render ``capsule bundle --with-viewer``'s self-contained HTML artifact.
|
|
3
|
+
|
|
4
|
+
The template (``static/offline_shell.html``) is a vendored, byte-for-byte
|
|
5
|
+
copy of scitt-cose's ``hosted_profiles.hosted.render_bundle_page(offline=True)``
|
|
6
|
+
output -- see ``scripts/vendor_bundle_viewer.py`` for how it's re-synced.
|
|
7
|
+
scitt-cose's own ``hosted_profiles/`` package is deliberately excluded from
|
|
8
|
+
its published wheel, so there is no pip dependency that could pull this in
|
|
9
|
+
at install time; vendoring one generated artifact (both repos are
|
|
10
|
+
Apache-2.0) is the mechanism, not a live fetch from a running verify
|
|
11
|
+
service -- ``capsule bundle`` is otherwise a fully local, network-free
|
|
12
|
+
command reading only the local ledger file, and ``--with-viewer`` keeps
|
|
13
|
+
that property rather than making bundle *production* depend on a service
|
|
14
|
+
being reachable. The template embeds MMR_JS and BUNDLE_JS inline (no
|
|
15
|
+
``<script src>``) so the artifact this writes needs no network to open or
|
|
16
|
+
verify, on either end.
|
|
17
|
+
|
|
18
|
+
The template carries one real embed slot,
|
|
19
|
+
``window.__BUNDLE_FRAGMENT_B64U__="@@BUNDLE_FRAGMENT@@"`` -- substituted
|
|
20
|
+
here with the same base64url fragment ``capsule bundle`` already computes
|
|
21
|
+
for its permalink. Other appearances of that placeholder text elsewhere in
|
|
22
|
+
the template are BUNDLE_JS's own placeholder-detection / re-splice logic
|
|
23
|
+
(its in-browser "download self-contained copy" button) and must not be
|
|
24
|
+
touched, so this replaces only the one assignment, not every occurrence of
|
|
25
|
+
the bare token.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
from importlib import resources
|
|
31
|
+
|
|
32
|
+
__all__ = ["render_offline_viewer_html"]
|
|
33
|
+
|
|
34
|
+
_EMBED_SLOT_PLACEHOLDER = '<script>window.__BUNDLE_FRAGMENT_B64U__="@@BUNDLE_FRAGMENT@@";</script>'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_template() -> str:
|
|
38
|
+
return (
|
|
39
|
+
resources.files("capsule_engine.bundle_viewer")
|
|
40
|
+
.joinpath("static", "offline_shell.html")
|
|
41
|
+
.read_text(encoding="utf-8")
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def render_offline_viewer_html(fragment: str) -> str:
|
|
46
|
+
"""Return the self-contained HTML for *fragment* (the same base64url
|
|
47
|
+
bundle fragment used in the bundle's permalink). The result carries no
|
|
48
|
+
external references at all -- open it directly, no server required."""
|
|
49
|
+
template = _load_template()
|
|
50
|
+
if template.count(_EMBED_SLOT_PLACEHOLDER) != 1:
|
|
51
|
+
raise RuntimeError(
|
|
52
|
+
"vendored offline_shell.html's embed slot is missing or no longer "
|
|
53
|
+
"unique -- re-check it against a fresh vendor pass "
|
|
54
|
+
"(scripts/vendor_bundle_viewer.py) before shipping --with-viewer"
|
|
55
|
+
)
|
|
56
|
+
embed_slot = f"<script>window.__BUNDLE_FRAGMENT_B64U__={json.dumps(fragment)};</script>"
|
|
57
|
+
return template.replace(_EMBED_SLOT_PLACEHOLDER, embed_slot, 1)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`capsule bundle`: a self-contained, independently verifiable slice of the
|
|
3
|
+
ledger, plus a verify-surface permalink.
|
|
4
|
+
|
|
5
|
+
"Self-contained" is load-bearing: the bundle transitively pulls in any
|
|
6
|
+
``chain.parent_capsule_id`` a selected record cites (walking the real
|
|
7
|
+
ledger) so the slice verifies on its own, without needing the rest of the
|
|
8
|
+
ledger present -- verification below runs with ``store=`` restricted to
|
|
9
|
+
exactly the bundle's own capsule ids, never the full ledger, to prove that
|
|
10
|
+
property rather than assume it.
|
|
11
|
+
|
|
12
|
+
The permalink is fragment-carried (``#...``, after the URL's ``#``): per
|
|
13
|
+
the workspace's other verify surfaces (`agentactioncapsule-site` /
|
|
14
|
+
`scitt-cose`'s hosted verifier), capsule data that goes after ``#`` is never
|
|
15
|
+
sent to a server -- only the browser-side JS reads it. ``verify.
|
|
16
|
+
agentactioncapsule.org`` is this workspace's existing public verify domain
|
|
17
|
+
(see CLAUDE.md / STATUS.md T7 notes); the base URL is still overridable
|
|
18
|
+
(``--verify-base-url`` / ``$CAPSULE_VERIFY_BASE_URL``) since no bundle-specific
|
|
19
|
+
route exists there yet -- this is this package's own convention until one
|
|
20
|
+
does.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import base64
|
|
26
|
+
import dataclasses
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
from datetime import datetime, timezone
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from agent_action_capsule import verify as verify_capsule
|
|
34
|
+
from capsule_emit.checkpoint import MmrLedger
|
|
35
|
+
from capsule_emit.checkpoint import core as mmr_core
|
|
36
|
+
from capsule_emit.ledger_io import (
|
|
37
|
+
add_scan_query_args,
|
|
38
|
+
build_scan_query,
|
|
39
|
+
echo_parts,
|
|
40
|
+
open_ledger,
|
|
41
|
+
require_ledger_path,
|
|
42
|
+
)
|
|
43
|
+
from capsule_emit.period import add_period_arg, apply_period
|
|
44
|
+
from cll.ledger.api import ScanQuery
|
|
45
|
+
|
|
46
|
+
from ..envcompat import env_get
|
|
47
|
+
from ..guards.capsule import local_only_refusal
|
|
48
|
+
from .format import build_echo, format_staleness
|
|
49
|
+
|
|
50
|
+
__all__ = ["add_parser", "run"]
|
|
51
|
+
|
|
52
|
+
DEFAULT_VERIFY_BASE_URL = "https://verify.agentactioncapsule.org/bundle"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def add_parser(sub: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
56
|
+
p = sub.add_parser("bundle", help="produce a self-contained verifiable slice of the ledger")
|
|
57
|
+
add_scan_query_args(p)
|
|
58
|
+
add_period_arg(p)
|
|
59
|
+
p.add_argument("--out", default="bundle.json", help="output path for the bundle file (default: %(default)s)")
|
|
60
|
+
p.add_argument(
|
|
61
|
+
"--verify-base-url",
|
|
62
|
+
dest="verify_base_url",
|
|
63
|
+
default=env_get("CAPSULE_VERIFY_BASE_URL", DEFAULT_VERIFY_BASE_URL),
|
|
64
|
+
help="base URL the permalink's fragment is appended to (default: %(default)s)",
|
|
65
|
+
)
|
|
66
|
+
p.add_argument(
|
|
67
|
+
"--with-viewer",
|
|
68
|
+
dest="with_viewer",
|
|
69
|
+
action="store_true",
|
|
70
|
+
help=(
|
|
71
|
+
"also write a self-contained offline HTML viewer next to --out, so the "
|
|
72
|
+
"bundle verifies on a machine with no network (default viewer path: "
|
|
73
|
+
"--out with a .html extension)"
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
p.add_argument(
|
|
77
|
+
"--viewer-out",
|
|
78
|
+
dest="viewer_out",
|
|
79
|
+
default=None,
|
|
80
|
+
help="output path for the offline viewer HTML (implies --with-viewer; default: derived from --out)",
|
|
81
|
+
)
|
|
82
|
+
p.set_defaults(func=run)
|
|
83
|
+
return p
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _echo_safe_out(out: str) -> str:
|
|
87
|
+
"""The ``--out`` value ``cli_echo`` renders on the public bundle
|
|
88
|
+
permalink and offline viewer -- it must show where the file went
|
|
89
|
+
relative to the invocation, never the operator's absolute filesystem
|
|
90
|
+
layout. An absolute path outside the cwd tree (no relative route to it)
|
|
91
|
+
falls back to just the filename rather than leaking the full path."""
|
|
92
|
+
p = Path(out)
|
|
93
|
+
if not p.is_absolute():
|
|
94
|
+
return out
|
|
95
|
+
rel = os.path.relpath(p, Path.cwd())
|
|
96
|
+
return rel if not rel.startswith("..") else p.name
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _default_viewer_out(out: str) -> str:
|
|
100
|
+
"""``bundle.json`` -> ``bundle.html`` alongside it; falls back to a
|
|
101
|
+
``-viewer.html`` suffix on the rare path where ``--out`` already ends in
|
|
102
|
+
``.html`` (so the two outputs never collide)."""
|
|
103
|
+
p = Path(out)
|
|
104
|
+
candidate = p.with_suffix(".html")
|
|
105
|
+
if str(candidate) == out:
|
|
106
|
+
candidate = p.with_name(p.stem + "-viewer.html")
|
|
107
|
+
return str(candidate)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _collect_with_parents(store, matched):
|
|
111
|
+
"""Pull in any cited chain parent not already selected, so the bundle
|
|
112
|
+
verifies standalone without needing the rest of the ledger. A parent
|
|
113
|
+
that genuinely isn't in the ledger (a real gap) is left out -- it
|
|
114
|
+
surfaces honestly as a finding on the citing record, never hidden."""
|
|
115
|
+
by_id = {r.capsule_id: r for r in matched}
|
|
116
|
+
frontier = list(matched)
|
|
117
|
+
missing: set[str] = set()
|
|
118
|
+
while frontier:
|
|
119
|
+
rec = frontier.pop()
|
|
120
|
+
parent_id = (rec.capsule.get("chain") or {}).get("parent_capsule_id")
|
|
121
|
+
if not parent_id or parent_id in by_id or parent_id in missing:
|
|
122
|
+
continue
|
|
123
|
+
parent = store.fetch(parent_id)
|
|
124
|
+
if parent is None:
|
|
125
|
+
missing.add(parent_id)
|
|
126
|
+
continue
|
|
127
|
+
by_id[parent.capsule_id] = parent
|
|
128
|
+
frontier.append(parent)
|
|
129
|
+
return sorted(by_id.values(), key=lambda r: r.seq)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _build_completeness_certificate(store, records, tree_size: int) -> dict | None:
|
|
133
|
+
"""MMR range proof (plus a consistency proof bridging to the full
|
|
134
|
+
ledger's checkpoint, when the bundle's range doesn't already reach it)
|
|
135
|
+
over the bundle's own record range.
|
|
136
|
+
|
|
137
|
+
Shape matches scitt-cose's viewer (``MMR_JS`` / ``checkCompleteness`` in
|
|
138
|
+
``hosted_profiles/hosted.py``) field for field -- that module ports this
|
|
139
|
+
package's MMR core to JS and already checks a certificate in this shape;
|
|
140
|
+
this is the CLI side of that contract, not a new one.
|
|
141
|
+
"""
|
|
142
|
+
if not records:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
mmr = MmrLedger(store)
|
|
146
|
+
mmr.sync()
|
|
147
|
+
|
|
148
|
+
from_seq, to_seq = records[0].seq, records[-1].seq
|
|
149
|
+
proof = mmr.range_proof(from_seq, to_seq)
|
|
150
|
+
range_root = mmr.root_at(proof.size).hex()
|
|
151
|
+
|
|
152
|
+
checkpoint_size = mmr_core.node_count(tree_size)
|
|
153
|
+
if checkpoint_size == proof.size:
|
|
154
|
+
checkpoint_root = range_root
|
|
155
|
+
consistency = None
|
|
156
|
+
else:
|
|
157
|
+
checkpoint_root = mmr.root_at(checkpoint_size).hex()
|
|
158
|
+
consistency = mmr.consistency_proof(proof.size, checkpoint_size)
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
"v": 1,
|
|
162
|
+
"range_proof": {
|
|
163
|
+
"from_seq": proof.from_seq,
|
|
164
|
+
"to_seq": proof.to_seq,
|
|
165
|
+
"size": proof.size,
|
|
166
|
+
"from_index": proof.from_index,
|
|
167
|
+
"to_index": proof.to_index,
|
|
168
|
+
"witness": list(proof.witness),
|
|
169
|
+
},
|
|
170
|
+
"range_root": range_root,
|
|
171
|
+
"checkpoint_size": checkpoint_size,
|
|
172
|
+
"checkpoint_root": checkpoint_root,
|
|
173
|
+
"consistency_proof": dataclasses.asdict(consistency) if consistency is not None else None,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def run(args: argparse.Namespace) -> int:
|
|
178
|
+
# ``bundle`` only exists at all in the "full" packaging arm -- see
|
|
179
|
+
# ``cli/main.py`` -- so any use of it is M5's "bundle/share created" fact.
|
|
180
|
+
from ..telemetry.record import record_evidence_touch
|
|
181
|
+
|
|
182
|
+
record_evidence_touch("full")
|
|
183
|
+
|
|
184
|
+
ledger_path = require_ledger_path("bundle", args)
|
|
185
|
+
if ledger_path is None:
|
|
186
|
+
return 2
|
|
187
|
+
|
|
188
|
+
apply_period(args) # --period fills in args.since/args.until in place when unset
|
|
189
|
+
query = build_scan_query(args)
|
|
190
|
+
with open_ledger(ledger_path) as store:
|
|
191
|
+
matched = list(store.scan(query))
|
|
192
|
+
records = _collect_with_parents(store, matched)
|
|
193
|
+
capsules = [r.capsule for r in records]
|
|
194
|
+
refusal = local_only_refusal(capsules)
|
|
195
|
+
if refusal is not None:
|
|
196
|
+
print(f"capsule bundle: refused: {refusal}; narrow the selection to records without them", file=sys.stderr)
|
|
197
|
+
return 2
|
|
198
|
+
ids = [c["capsule_id"] for c in capsules]
|
|
199
|
+
|
|
200
|
+
verification: dict[str, dict] = {}
|
|
201
|
+
all_ok = True
|
|
202
|
+
for capsule in capsules:
|
|
203
|
+
result = verify_capsule(capsule, store=ids)
|
|
204
|
+
verification[capsule["capsule_id"]] = {
|
|
205
|
+
"ok": result.ok,
|
|
206
|
+
"findings": [{"code": f.code, "detail": f.detail, "severity": f.severity} for f in result.findings],
|
|
207
|
+
}
|
|
208
|
+
all_ok = all_ok and result.ok
|
|
209
|
+
|
|
210
|
+
tree_size = sum(1 for _ in store.scan(ScanQuery()))
|
|
211
|
+
completeness_certificate = _build_completeness_certificate(store, records, tree_size)
|
|
212
|
+
|
|
213
|
+
echo = build_echo(
|
|
214
|
+
"bundle", flags=[*echo_parts(args), ("--period", args.period), ("--out", _echo_safe_out(args.out))]
|
|
215
|
+
)
|
|
216
|
+
bundle = {
|
|
217
|
+
"bundle_version": "1",
|
|
218
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
219
|
+
"query": {
|
|
220
|
+
k: v
|
|
221
|
+
for k, v in {
|
|
222
|
+
"agent": args.agent,
|
|
223
|
+
# args.since/args.until already reflect --period's resolved
|
|
224
|
+
# bounds (apply_period mutates them in place above) -- a
|
|
225
|
+
# bundle's recorded query must show what was actually
|
|
226
|
+
# scanned, not the pre-resolution flag.
|
|
227
|
+
"since": args.since,
|
|
228
|
+
"until": args.until,
|
|
229
|
+
"period": args.period,
|
|
230
|
+
"counterparty": args.counterparty,
|
|
231
|
+
"verdict": args.verdict,
|
|
232
|
+
"action_type": args.action_type,
|
|
233
|
+
"limit": args.limit,
|
|
234
|
+
}.items()
|
|
235
|
+
if v is not None
|
|
236
|
+
},
|
|
237
|
+
"cli_echo": echo,
|
|
238
|
+
"records": capsules,
|
|
239
|
+
"range": [records[0].seq, records[-1].seq] if records else [0, -1],
|
|
240
|
+
"checkpoint": {"tree_size": tree_size},
|
|
241
|
+
"verification": verification,
|
|
242
|
+
"completeness_certificate": completeness_certificate,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
with open(args.out, "w", encoding="utf-8") as fh:
|
|
246
|
+
json.dump(bundle, fh, indent=2, sort_keys=True)
|
|
247
|
+
|
|
248
|
+
payload = json.dumps(bundle, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
249
|
+
fragment = base64.urlsafe_b64encode(payload).decode("ascii").rstrip("=")
|
|
250
|
+
permalink = f"{args.verify_base_url}#{fragment}"
|
|
251
|
+
|
|
252
|
+
r0, r1 = bundle["range"]
|
|
253
|
+
status = "all verify" if all_ok else "VERIFICATION FAILURE in this slice"
|
|
254
|
+
print(f"wrote {args.out} ({len(records)} record(s), records {r0}–{r1}, {status})")
|
|
255
|
+
print(f"checkpoint #{tree_size} · as of {format_staleness(0)}")
|
|
256
|
+
print(f"verify: {permalink}")
|
|
257
|
+
|
|
258
|
+
if args.with_viewer or args.viewer_out is not None:
|
|
259
|
+
from ..bundle_viewer import render_offline_viewer_html
|
|
260
|
+
|
|
261
|
+
viewer_out = args.viewer_out or _default_viewer_out(args.out)
|
|
262
|
+
viewer_html = render_offline_viewer_html(fragment)
|
|
263
|
+
with open(viewer_out, "w", encoding="utf-8") as fh:
|
|
264
|
+
fh.write(viewer_html)
|
|
265
|
+
print(f"wrote {viewer_out} (self-contained, opens with no network)")
|
|
266
|
+
|
|
267
|
+
print()
|
|
268
|
+
print(echo)
|
|
269
|
+
return 0 if all_ok else 1
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`capsule console`: serve the local console UI (record stream, filters,
|
|
3
|
+
per-record inspector, fold strip) over the real ledger.
|
|
4
|
+
|
|
5
|
+
LOCAL ONLY -- binds to localhost by default, no hosted anything, no account
|
|
6
|
+
system. See `capsule_ledger/console/server.py` for the HTTP wiring and
|
|
7
|
+
`capsule_ledger/console/{tokens,components}.css` for the shared component
|
|
8
|
+
library this UI is built from (never re-invented here).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
|
|
14
|
+
from capsule_emit.ledger_io import require_ledger_path
|
|
15
|
+
|
|
16
|
+
from .format import build_echo
|
|
17
|
+
|
|
18
|
+
__all__ = ["add_parser", "run"]
|
|
19
|
+
|
|
20
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
21
|
+
DEFAULT_PORT = 8420
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def add_parser(sub: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
25
|
+
p = sub.add_parser(
|
|
26
|
+
"console", help="serve the local console UI (record stream + inspector) over the real ledger"
|
|
27
|
+
)
|
|
28
|
+
p.add_argument("--ledger", help="ledger store directory or a JSONL fixture file (default: $CAPSULE_LEDGER)")
|
|
29
|
+
p.add_argument(
|
|
30
|
+
"--host", default=DEFAULT_HOST, help="bind host -- local only by default (default: %(default)s)"
|
|
31
|
+
)
|
|
32
|
+
p.add_argument("--port", type=int, default=DEFAULT_PORT, help="bind port (default: %(default)s)")
|
|
33
|
+
p.set_defaults(func=run)
|
|
34
|
+
return p
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run(args: argparse.Namespace) -> int:
|
|
38
|
+
# The console only exists at all in the "full" packaging arm (see
|
|
39
|
+
# ``cli/main.py``) -- it renders capsule vocabulary and verification
|
|
40
|
+
# state throughout, matching log/show/verify/bundle's own M5 marker.
|
|
41
|
+
from ..telemetry.record import record_evidence_touch
|
|
42
|
+
|
|
43
|
+
record_evidence_touch("full")
|
|
44
|
+
|
|
45
|
+
ledger_path = require_ledger_path("console", args)
|
|
46
|
+
if ledger_path is None:
|
|
47
|
+
return 2
|
|
48
|
+
|
|
49
|
+
from ..console.server import build_server
|
|
50
|
+
|
|
51
|
+
server = build_server(ledger_path, host=args.host, port=args.port)
|
|
52
|
+
url = f"http://{args.host}:{args.port}/"
|
|
53
|
+
print(f"capsule console: serving {ledger_path} at {url} (local only — no hosted anything, no account system)")
|
|
54
|
+
print(
|
|
55
|
+
build_echo(
|
|
56
|
+
"console",
|
|
57
|
+
flags=[
|
|
58
|
+
("--ledger", args.ledger),
|
|
59
|
+
("--host", args.host if args.host != DEFAULT_HOST else None),
|
|
60
|
+
("--port", args.port if args.port != DEFAULT_PORT else None),
|
|
61
|
+
],
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
try:
|
|
65
|
+
server.serve_forever()
|
|
66
|
+
except KeyboardInterrupt:
|
|
67
|
+
pass
|
|
68
|
+
finally:
|
|
69
|
+
server.server_close()
|
|
70
|
+
return 0
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`capsule constraints list`: the registered guard checks + the starter
|
|
3
|
+
action-class taxonomy (T3's ``guards/classes.py``) -- a static catalog, not
|
|
4
|
+
a ledger query, so it needs no ``--ledger``."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..folds.catalog import Catalog
|
|
11
|
+
from ..guards.classes import TAXONOMY, UNCLASSIFIED_DEFAULT
|
|
12
|
+
|
|
13
|
+
__all__ = ["add_parser", "run_list", "CHECKS", "DEFAULT_CAPS_FOLD_ID", "DEFAULT_CATALOG_DIR"]
|
|
14
|
+
|
|
15
|
+
DEFAULT_CATALOG_DIR = Path(__file__).resolve().parent.parent / "folds" / "catalog_defs"
|
|
16
|
+
# The caps check's fold is a per-deployment GuardEngine configuration
|
|
17
|
+
# (`GuardEngine(caps_fold=...)`); this listing resolves the one T3 wires up
|
|
18
|
+
# in its own tests/fixtures (`conftest.py`'s `caps_fold` fixture) as the
|
|
19
|
+
# representative default, rather than invent one of its own.
|
|
20
|
+
DEFAULT_CAPS_FOLD_ID = "spend.weekly/1.0.0"
|
|
21
|
+
|
|
22
|
+
# (check_id, check_type, method, description) -- method is None for `caps`,
|
|
23
|
+
# whose method is the configured fold_id, resolved at print time below.
|
|
24
|
+
# Public (not `_`-prefixed): the MCP server's `constraints.list` tool reuses
|
|
25
|
+
# this exact catalog rather than keeping its own copy in sync by hand.
|
|
26
|
+
CHECKS = [
|
|
27
|
+
(
|
|
28
|
+
"dedupe",
|
|
29
|
+
"policy",
|
|
30
|
+
"exact_match_index_v0",
|
|
31
|
+
"equivalence lookup over a rolling window — exact-match only, no fuzzy/semantic matching",
|
|
32
|
+
),
|
|
33
|
+
(
|
|
34
|
+
"caps",
|
|
35
|
+
"policy",
|
|
36
|
+
None,
|
|
37
|
+
"fold predicate: weekly_spend + amount <= cap (integer minor units only)",
|
|
38
|
+
),
|
|
39
|
+
(
|
|
40
|
+
"verify_before_dispatch",
|
|
41
|
+
"policy",
|
|
42
|
+
"agent_action_capsule.verify",
|
|
43
|
+
"the cited mandate capsule must exist in the ledger and independently re-verify",
|
|
44
|
+
),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def add_parser(sub: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
49
|
+
constraints = sub.add_parser("constraints", help="the registered guard checks and the action-class taxonomy")
|
|
50
|
+
csub = constraints.add_subparsers(dest="constraints_command")
|
|
51
|
+
constraints.set_defaults(constraints_parser=constraints)
|
|
52
|
+
|
|
53
|
+
p_list = csub.add_parser("list", help="list the registered guard checks and the action-class taxonomy")
|
|
54
|
+
p_list.add_argument("--dir", help="fold catalog directory the caps check reads from (default: built-in catalog)")
|
|
55
|
+
p_list.set_defaults(func=run_list)
|
|
56
|
+
|
|
57
|
+
return constraints
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_list(args: argparse.Namespace) -> int:
|
|
61
|
+
catalog_dir = Path(args.dir) if getattr(args, "dir", None) else DEFAULT_CATALOG_DIR
|
|
62
|
+
caps_entry = Catalog(catalog_dir).get(DEFAULT_CAPS_FOLD_ID)
|
|
63
|
+
caps_method = caps_entry.definition.fold_id if caps_entry is not None else "(no caps fold configured)"
|
|
64
|
+
|
|
65
|
+
print("checks:")
|
|
66
|
+
for check_id, check_type, method, description in CHECKS:
|
|
67
|
+
resolved_method = caps_method if check_id == "caps" else method
|
|
68
|
+
print(f" {check_id:<24} {check_type:<8} {resolved_method:<28} {description}")
|
|
69
|
+
|
|
70
|
+
print()
|
|
71
|
+
print("action classes (absent or unrecognized -> unclassified, fail-closed):")
|
|
72
|
+
for ac in (*TAXONOMY.values(), UNCLASSIFIED_DEFAULT):
|
|
73
|
+
print(f" {ac.name:<26} consequential={str(ac.consequential):<6} fail_open_allowed={ac.fail_open_allowed}")
|
|
74
|
+
return 0
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Shared CLI output discipline: the DM Mono fold-envelope line, staleness
|
|
3
|
+
copy, the CLI-echo canonical form, and one capsule-summary helper.
|
|
4
|
+
|
|
5
|
+
The envelope-line format is copied verbatim from the design handoff
|
|
6
|
+
README's "Product laws" section (`design_handoff_capsule_ledger_ux/README.md`):
|
|
7
|
+
``fold <id> · records N–M · checkpoint #X · as of <staleness>`` -- literal
|
|
8
|
+
text output, not a font choice. ``<id>`` is the fold's own ``fold`` envelope
|
|
9
|
+
field (the definition digest already computed by the fold engine), never a
|
|
10
|
+
name this module invents.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import shlex
|
|
16
|
+
|
|
17
|
+
from ..registry import describe_action_class
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"format_staleness",
|
|
21
|
+
"format_envelope_line",
|
|
22
|
+
"format_coverage_footnote",
|
|
23
|
+
"build_echo",
|
|
24
|
+
"cli_echo_leaks_absolute_path",
|
|
25
|
+
"summarize_action",
|
|
26
|
+
"format_action_class",
|
|
27
|
+
"assurance_grade_parts",
|
|
28
|
+
"format_assurance_grade",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
_WINDOWS_DRIVE_RE = re.compile(r"[A-Za-z]:\\")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def format_staleness(age_ms: int) -> str:
|
|
35
|
+
"""Render a checkpoint age in the design's "as of 14s ago" style.
|
|
36
|
+
|
|
37
|
+
``age_ms <= 0`` (a value freshly computed by this same CLI invocation,
|
|
38
|
+
never backdated) renders as "just now" rather than "0s ago".
|
|
39
|
+
"""
|
|
40
|
+
if age_ms <= 0:
|
|
41
|
+
return "just now"
|
|
42
|
+
seconds = age_ms / 1000
|
|
43
|
+
if seconds < 60:
|
|
44
|
+
return f"{round(seconds)}s ago"
|
|
45
|
+
minutes = seconds / 60
|
|
46
|
+
if minutes < 60:
|
|
47
|
+
return f"{round(minutes)}m ago"
|
|
48
|
+
hours = minutes / 60
|
|
49
|
+
if hours < 24:
|
|
50
|
+
return f"{round(hours)}h ago"
|
|
51
|
+
days = hours / 24
|
|
52
|
+
return f"{round(days)}d ago"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def format_envelope_line(envelope: dict) -> str:
|
|
56
|
+
"""Render a fold result envelope (spec §4 shape) as the literal DM Mono line."""
|
|
57
|
+
r0, r1 = envelope["range"]
|
|
58
|
+
checkpoint_num = envelope["checkpoint"].get("tree_size")
|
|
59
|
+
staleness_ms = envelope["staleness"].get("checkpoint_age_ms", 0)
|
|
60
|
+
return (
|
|
61
|
+
f"fold {envelope['fold']} · records {r0}–{r1} · "
|
|
62
|
+
f"checkpoint #{checkpoint_num} · as of {format_staleness(staleness_ms)}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def format_coverage_footnote(coverage: dict) -> str:
|
|
67
|
+
"""Render a fold's coverage statement (T2R rev9(a),
|
|
68
|
+
``EvaluationTrace.coverage_statement()``) as the design's literal
|
|
69
|
+
per-number footnote: "range-complete through C · captured under boundary
|
|
70
|
+
B, rule R · reconciled against O over interval T". The rule this
|
|
71
|
+
enforces (rev9(a)): *a report cannot render a number without its
|
|
72
|
+
coverage statement* -- every caller that shows a fold's ``result`` shows
|
|
73
|
+
this alongside it, never bare.
|
|
74
|
+
|
|
75
|
+
``capture: "unknown"`` (no boundary/rule declared for this evaluation)
|
|
76
|
+
renders visibly rather than being silently dropped from the footnote;
|
|
77
|
+
``reconciled: None`` (no external comparison set) renders as
|
|
78
|
+
"reconciled: none" for the same reason.
|
|
79
|
+
"""
|
|
80
|
+
_, range_end = coverage["range"]
|
|
81
|
+
capture = coverage["capture"]
|
|
82
|
+
if capture == "unknown":
|
|
83
|
+
capture_part = "captured under boundary: unknown"
|
|
84
|
+
else:
|
|
85
|
+
capture_part = f"captured under boundary {capture['boundary']}, rule {capture['rule']}"
|
|
86
|
+
reconciled = coverage["reconciled"]
|
|
87
|
+
reconciled_part = (
|
|
88
|
+
"reconciled: none"
|
|
89
|
+
if reconciled is None
|
|
90
|
+
else f"reconciled against {reconciled['source']} over interval {reconciled['interval']}"
|
|
91
|
+
)
|
|
92
|
+
return f"range-complete through {range_end} · {capture_part} · {reconciled_part}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_echo(verb: str, *, positional: str | None = None, flags: list[tuple[str, object]] = ()) -> str:
|
|
96
|
+
"""The CLI-echo canonical form: the exact, copy-pasteable invocation that
|
|
97
|
+
produced this output (design handoff README, "CLI echo standing rule").
|
|
98
|
+
Flag order is fixed (whatever the caller passes), not the order the user
|
|
99
|
+
typed them in, so the same query always echoes identically.
|
|
100
|
+
"""
|
|
101
|
+
tokens = ["capsule", verb]
|
|
102
|
+
if positional is not None:
|
|
103
|
+
tokens.append(shlex.quote(str(positional)))
|
|
104
|
+
for flag, value in flags:
|
|
105
|
+
if value is None or value is False:
|
|
106
|
+
continue
|
|
107
|
+
tokens.append(flag)
|
|
108
|
+
if value is not True:
|
|
109
|
+
tokens.append(shlex.quote(str(value)))
|
|
110
|
+
return "≡ " + " ".join(tokens)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cli_echo_leaks_absolute_path(cli_echo: str) -> bool:
|
|
114
|
+
"""``cli_echo`` is rendered verbatim on the bundle permalink and the
|
|
115
|
+
offline viewer -- a public page -- so it must never carry an operator's
|
|
116
|
+
absolute filesystem layout (home directory, machine username). Flags any
|
|
117
|
+
token that is a POSIX absolute path (a leading ``/``, with no allowlisted
|
|
118
|
+
exception defined) or a Windows drive path (``C:\\...``); callers that
|
|
119
|
+
build ``cli_echo`` from user-supplied paths must relativize them first
|
|
120
|
+
rather than adding an exception here."""
|
|
121
|
+
tokens = shlex.split(cli_echo.removeprefix("≡ "))
|
|
122
|
+
for token in tokens:
|
|
123
|
+
if token.startswith("/") or _WINDOWS_DRIVE_RE.match(token):
|
|
124
|
+
return True
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def summarize_action(capsule: dict) -> str:
|
|
129
|
+
"""The verb portion of ``action_id`` (e.g. "approve_purchase"), falling
|
|
130
|
+
back to ``action_type`` for records with no ``action_id``."""
|
|
131
|
+
action_id = capsule.get("action_id") or ""
|
|
132
|
+
verb = action_id.split("/", 1)[0] if action_id else ""
|
|
133
|
+
return verb or capsule.get("action_type") or "(unnamed action)"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def format_action_class(capsule: dict) -> str | None:
|
|
137
|
+
"""Convention-label line for this capsule's ``asg_payload.action_class``
|
|
138
|
+
(design principle item 2) -- ``None`` when the capsule carries no
|
|
139
|
+
``action_class`` at all (a legitimately different state from an
|
|
140
|
+
unregistered *value*, which renders instead of being hidden)."""
|
|
141
|
+
action_class = (capsule.get("asg_payload") or {}).get("action_class")
|
|
142
|
+
convention = describe_action_class(action_class)
|
|
143
|
+
if convention.action_class is None:
|
|
144
|
+
return None
|
|
145
|
+
if convention.registered:
|
|
146
|
+
return f"{convention.action_class} — {convention.label}"
|
|
147
|
+
return f"{convention.action_class} (unregistered)"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_ATTESTATION_LABELS = {"self_attested": "self-attested", "anchored": "anchored"}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def assurance_grade_parts(assurance: dict) -> tuple[str, bool]:
|
|
154
|
+
"""(label, badged) for this assurance block -- the one place that
|
|
155
|
+
decides which attestation modes are "badged" (design principle item 3).
|
|
156
|
+
Extensible to a future ``countersigned`` value the same way, once that
|
|
157
|
+
assurance mode is a real field this codebase produces; ``badged`` is
|
|
158
|
+
never about paid/free, only about attestation strength."""
|
|
159
|
+
mode = assurance.get("attestation_mode")
|
|
160
|
+
label = _ATTESTATION_LABELS.get(mode, mode or "(none)")
|
|
161
|
+
ledger_mode = assurance.get("ledger_mode")
|
|
162
|
+
grade = f"{label} · ledger: {ledger_mode}" if ledger_mode else label
|
|
163
|
+
return grade, mode == "anchored"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def format_assurance_grade(assurance: dict) -> str:
|
|
167
|
+
"""Assurance grades do the honesty work everywhere (design principle
|
|
168
|
+
item 3): ``self_attested`` renders plain, ``anchored`` is badged
|
|
169
|
+
(``[...]``) -- driven only by the real ``attestation_mode``/
|
|
170
|
+
``ledger_mode`` values on the record, never invented, never upsell
|
|
171
|
+
copy."""
|
|
172
|
+
grade, badged = assurance_grade_parts(assurance)
|
|
173
|
+
return f"[{grade}]" if badged else grade
|