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,35 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Named-reason errors for Result v0 construction (mirrors
|
|
3
|
+
``packs/errors.py``'s ``PackDefinitionError`` discipline: a stable reason
|
|
4
|
+
code for tests/tooling, a message that names the field and what was
|
|
5
|
+
expected).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
INVALID_HEX_DIGEST = "invalid_hex_digest"
|
|
10
|
+
INVALID_TIER = "invalid_result_tier"
|
|
11
|
+
INVALID_GRADE = "invalid_result_grade"
|
|
12
|
+
INVALID_SUFFICIENCY = "invalid_sufficiency"
|
|
13
|
+
INVALID_VERDICT = "invalid_verdict"
|
|
14
|
+
SUFFICIENCY_VERDICT_MISMATCH = "sufficiency_verdict_mismatch"
|
|
15
|
+
INVALID_DISCLOSED_STATUS = "invalid_disclosed_status"
|
|
16
|
+
INVALID_EVIDENCE_STATUS = "invalid_evidence_status"
|
|
17
|
+
DISCLOSURE_NOT_LEGAL_FOR_STATUS = "disclosure_not_legal_for_status"
|
|
18
|
+
DUPLICATE_CLAIM_ID = "duplicate_claim_id"
|
|
19
|
+
BUCKET_CLAIM_MISMATCH = "bucket_claim_mismatch"
|
|
20
|
+
COVERAGE_CLAIM_MISMATCH = "coverage_claim_mismatch"
|
|
21
|
+
SCHEMA_VALIDATION_FAILED = "schema_validation_failed"
|
|
22
|
+
INVALID_COVERAGE_REPORT = "invalid_coverage_report"
|
|
23
|
+
INVALID_LAYER_TALLY = "invalid_layer_tally"
|
|
24
|
+
COVERAGE_NOT_COMPUTED = "coverage_not_computed"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ResultError(ValueError):
|
|
28
|
+
"""A Result v0 document, or one of its parts, fails to validate --
|
|
29
|
+
"an untiered claim is a bug here, not a rendering problem".
|
|
30
|
+
Always fail-closed at
|
|
31
|
+
construction time, same discipline as ``PackDefinitionError``."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, reason: str, message: str) -> None:
|
|
34
|
+
self.reason = reason
|
|
35
|
+
super().__init__(f"{reason}: {message}")
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""One live check, decided the way a capsulectl rules checker decides it.
|
|
3
|
+
|
|
4
|
+
capsulectl hands its rules checker one external-check-input/v0 envelope per
|
|
5
|
+
check. ``decide_check_input`` decides one envelope under one pack: it installs
|
|
6
|
+
the pack in observe mode, builds the engine from the installed manifest as
|
|
7
|
+
``build_engine`` does (its runnable checks and ask sets, the caps limits read
|
|
8
|
+
from the ledger's activation records, so each limit names its source, and
|
|
9
|
+
the per-action reads), writes the envelope's history into a throwaway ledger
|
|
10
|
+
(``history_ledger``, with the pack's caps fold), and checks the record entry
|
|
11
|
+
(``action_for_check_input``) with what capsulectl passes beside it: the
|
|
12
|
+
task-authority record, the floor opening, and the record entry's
|
|
13
|
+
representing opening. The Go guard decides the same envelope with
|
|
14
|
+
``DecodeCheckInput`` and ``CheckWithHistory``; the shared conformance vectors
|
|
15
|
+
hold the two to the same decision.
|
|
16
|
+
|
|
17
|
+
The ledger lives in a temporary directory and is gone when the call returns;
|
|
18
|
+
its records are signed by ``signer`` (a fixed local key when none is given),
|
|
19
|
+
and no record leaves the call except as the returned capsules.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import tempfile
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from cll.ledger import LedgerStore
|
|
28
|
+
|
|
29
|
+
from ..guards import Action, GuardDecision, GuardEngine, LocalSigner
|
|
30
|
+
from ..guards.checks import RUNNABLE_CHECKS
|
|
31
|
+
from ..packs import install_pack
|
|
32
|
+
from ..packs.install import engine_ask_sets
|
|
33
|
+
from ..packs.schema import PackDefinition
|
|
34
|
+
from ..policy.limits import read_caps_limits
|
|
35
|
+
from .live_history import HistoryLedger, history_ledger
|
|
36
|
+
from .replay import action_for_check_input
|
|
37
|
+
|
|
38
|
+
__all__ = ["LiveCheck", "decide_check_input", "decide_sale_bundle", "decide_steps", "live_engine"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class LiveCheck:
|
|
43
|
+
"""A live check's decision, the ledger it was decided on (each record's
|
|
44
|
+
capsule, in ledger order), and what ``history_ledger`` wrote."""
|
|
45
|
+
|
|
46
|
+
decision: GuardDecision
|
|
47
|
+
ledger: list[dict]
|
|
48
|
+
written: HistoryLedger
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _default_signer() -> LocalSigner:
|
|
52
|
+
return LocalSigner(key_id="live-check", secret=b"live-check")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def live_engine(pack: PackDefinition, store: LedgerStore, project_dir: str | Path, *, signer=None) -> GuardEngine:
|
|
56
|
+
"""The engine a live check runs over ``store``: ``pack`` installed in
|
|
57
|
+
observe mode under ``project_dir``, wired as ``build_engine`` wires it. A
|
|
58
|
+
live decision never evaluates an action's class under another taxonomy
|
|
59
|
+
than the engine's (``guards/engine.py``); only a replay does."""
|
|
60
|
+
return _wired(install_pack(pack, project_dir=project_dir, mode="observe"), store, signer)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _wired(installed, store: LedgerStore, signer) -> GuardEngine:
|
|
64
|
+
resolved = installed.resolved
|
|
65
|
+
wickets = resolved.configured_wickets(RUNNABLE_CHECKS)
|
|
66
|
+
gates, asks = engine_ask_sets(installed.pack, wickets)
|
|
67
|
+
signer = signer or _default_signer()
|
|
68
|
+
return GuardEngine(
|
|
69
|
+
ledger=store, caps_fold=resolved.caps_fold(),
|
|
70
|
+
caps_limits=lambda s: read_caps_limits(resolved, store, s),
|
|
71
|
+
per_action_reads=resolved.per_action_reads(), signer_provider=lambda: signer,
|
|
72
|
+
manifest_digest=resolved.manifest_digest, wickets=wickets,
|
|
73
|
+
ask_gate_selectors=gates, ask_wickets=asks,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def decide_check_input(
|
|
78
|
+
pack: PackDefinition, envelope: dict, *, with_history: bool = True, signer=None
|
|
79
|
+
) -> LiveCheck:
|
|
80
|
+
"""The decision on one external-check-input/v0 envelope under ``pack``,
|
|
81
|
+
by a fresh engine whose ledger is the envelope's history, or empty when
|
|
82
|
+
``with_history`` is false (the live path before history was passed)."""
|
|
83
|
+
with tempfile.TemporaryDirectory() as tmp, LedgerStore(Path(tmp) / "ledger") as store:
|
|
84
|
+
installed = install_pack(pack, project_dir=Path(tmp) / "live-project", mode="observe")
|
|
85
|
+
engine = _wired(installed, store, signer)
|
|
86
|
+
caps_fold = installed.resolved.caps_fold()
|
|
87
|
+
written = history_ledger(envelope, store, caps_fold=caps_fold) if with_history else HistoryLedger(True, 0, 0, 0)
|
|
88
|
+
ledger = [r.capsule for r in store.scan()]
|
|
89
|
+
record = envelope["record"]
|
|
90
|
+
action = action_for_check_input(record, item_ref=envelope.get("item_ref"))
|
|
91
|
+
decision = engine.check(
|
|
92
|
+
action,
|
|
93
|
+
task_authority_record=envelope.get("task_authority_record"),
|
|
94
|
+
commercial_bounds_opening=envelope.get("commercial_bounds_opening"),
|
|
95
|
+
representing_opening=record.get("representing_opening"),
|
|
96
|
+
)
|
|
97
|
+
return LiveCheck(decision=decision, ledger=ledger, written=written)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def decide_steps(pack: PackDefinition, steps: list[dict], *, signer=None) -> list[GuardDecision]:
|
|
101
|
+
"""The decisions on a run of actions under ``pack``, each one an
|
|
102
|
+
``Action``'s fields as JSON (an array is a tuple), decided in order by one
|
|
103
|
+
engine whose ledger starts empty and holds each decision before the next
|
|
104
|
+
step is checked."""
|
|
105
|
+
with tempfile.TemporaryDirectory() as tmp, LedgerStore(Path(tmp) / "ledger") as store:
|
|
106
|
+
engine = _wired(install_pack(pack, project_dir=Path(tmp) / "live-project", mode="observe"), store, signer)
|
|
107
|
+
return [engine.check(Action(**{k: tuple(v) if isinstance(v, list) else v for k, v in step.items()})) for step in steps]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def decide_sale_bundle(pack: PackDefinition, bundle: dict):
|
|
111
|
+
"""A sale bundle read and verified (``read_sale_bundle``) and replayed
|
|
112
|
+
under ``pack`` (``replay_sale``), as a replay of a sale decides each of
|
|
113
|
+
its checks: the sale, and the replay's result."""
|
|
114
|
+
from .sale_bundle import read_sale_bundle, replay_sale
|
|
115
|
+
|
|
116
|
+
sale = read_sale_bundle(bundle)
|
|
117
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
118
|
+
installed = install_pack(pack, project_dir=Path(tmp) / "replay-project", mode="observe")
|
|
119
|
+
resolved = installed.resolved
|
|
120
|
+
result = replay_sale(
|
|
121
|
+
sale, caps_fold=resolved.caps_fold(), caps_minor=resolved.caps_minor(),
|
|
122
|
+
per_action_minor=resolved.per_action_minor(), per_action_reads=resolved.per_action_reads(),
|
|
123
|
+
manifest_digest=resolved.manifest_digest, wickets=resolved.configured_wickets(RUNNABLE_CHECKS),
|
|
124
|
+
pack=installed.pack,
|
|
125
|
+
)
|
|
126
|
+
return sale, result
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The ledger a live check decides on, built from its check input's history.
|
|
3
|
+
|
|
4
|
+
capsulectl hands its rules checker one external-check-input/v0 envelope per
|
|
5
|
+
check: the ``record`` being checked and, as ``history``, the profile's
|
|
6
|
+
earlier acts from every deal, newest first, each one the sealed capsule of an
|
|
7
|
+
act (an ``x-deal-v0`` action step, or a typed ``action-record/v0``) with its
|
|
8
|
+
disclosed ``agent_input`` and, beside it, the deal's ``item_ref`` when the
|
|
9
|
+
deal is a sale's thread.
|
|
10
|
+
``history_scope.complete`` says whether every act in the window is there.
|
|
11
|
+
|
|
12
|
+
``history_ledger`` writes those acts into the ledger the engine reads, once,
|
|
13
|
+
before the check, so every check that reads earlier acts (``dedupe``,
|
|
14
|
+
``counterparty_seen_before``, ``single_commitment``) reads one ledger built
|
|
15
|
+
one way. Each act is read by ``action_for_history_entry`` and written as the
|
|
16
|
+
record a decision on it would carry (``act_payload``), in ledger order:
|
|
17
|
+
oldest first by the capsule's ``timestamp``, with acts of the same second in
|
|
18
|
+
the order the envelope gives them (capsulectl's sort is stable, so that is
|
|
19
|
+
the order they were sealed in). An act given twice (the same ``capsule_id``)
|
|
20
|
+
is written once. A history entry that is a claim the user's
|
|
21
|
+
agent made to the counterparty is written as the replay writes it
|
|
22
|
+
(``statement_for_history_entry``), in state ``statement``, so ``required_disclosure``
|
|
23
|
+
reads a statement made first in the deal the same way live and in a replay.
|
|
24
|
+
It is not an act, so it is never read as an unread one and never adds to a
|
|
25
|
+
spend window. Only a claim sealed at or before the checked record's
|
|
26
|
+
``timestamp`` is written, so a claim made after the check never counts for
|
|
27
|
+
it; when the checked record's time cannot be read, no claim is written. A
|
|
28
|
+
claim given twice is written once. The claims capsulectl passes beside the
|
|
29
|
+
checked record as ``deal_claims`` (AMENDMENT 9) are written the same way,
|
|
30
|
+
after the history's records, as ``deal_claim_statements`` reads them.
|
|
31
|
+
|
|
32
|
+
Each record states what is known about it (``guards/history_state.py``),
|
|
33
|
+
and an act read carries, beside ``act_payload``, the counterparty its check
|
|
34
|
+
sealed (``thread_counterparty``), which no decision seals. Its
|
|
35
|
+
``disposition`` is the one the act's capsule seals, and nothing else: an act
|
|
36
|
+
sealed with none is written as ``no_disposition``, never as accepted and
|
|
37
|
+
never as declined, and no decision is inferred from its ``authority_basis``.
|
|
38
|
+
An entry whose act cannot be read is written as ``unread``, with its
|
|
39
|
+
``item_ref`` when that is in its agreed shape (64 lowercase hex). When the
|
|
40
|
+
history is missing, is not a list, is not declared complete, or holds an
|
|
41
|
+
entry with no readable time or an ``item_ref`` in another shape, one
|
|
42
|
+
``incomplete`` record is written after the acts.
|
|
43
|
+
|
|
44
|
+
Under a caps fold that counts executed acts (``spend.weekly/3.1.0``), each act
|
|
45
|
+
is also written as the spend window's record of it (``ExecutedActs``), right
|
|
46
|
+
after it: every history entry is an executed act, counted once. An entry
|
|
47
|
+
whose ``agent_input`` its capsule does not bind, or whose spend cannot be
|
|
48
|
+
read, is written with ``spend_unreadable``, so the window is not evaluated
|
|
49
|
+
rather than counted without it. Under any other fold nothing more is
|
|
50
|
+
written.
|
|
51
|
+
|
|
52
|
+
The capsules are read as given: their signatures are not verified here.
|
|
53
|
+
capsulectl hands the envelope to the user's own checker, and every record
|
|
54
|
+
this writes stays in that check's ledger.
|
|
55
|
+
"""
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
import re
|
|
59
|
+
from dataclasses import dataclass
|
|
60
|
+
from datetime import datetime
|
|
61
|
+
from typing import TypedDict
|
|
62
|
+
|
|
63
|
+
from agent_action_capsule import json_digest
|
|
64
|
+
from cll.ledger.api import LedgerAPI
|
|
65
|
+
|
|
66
|
+
from ..folds.definition import FoldDefinition
|
|
67
|
+
from ..folds.duration import parse_duration_seconds
|
|
68
|
+
from ..guards.action import Action
|
|
69
|
+
from ..guards.capsule import act_payload
|
|
70
|
+
from ..guards.checks.caps import counts_executed_acts, in_window_count
|
|
71
|
+
from ..guards.classes import ENGINE_TAXONOMY, same_classes
|
|
72
|
+
from ..guards.history_state import (
|
|
73
|
+
DISPOSITION,
|
|
74
|
+
INCOMPLETE,
|
|
75
|
+
LIVE_HISTORY,
|
|
76
|
+
NO_DISPOSITION,
|
|
77
|
+
STATEMENT,
|
|
78
|
+
THREAD_COUNTERPARTY,
|
|
79
|
+
UNREAD,
|
|
80
|
+
WINDOW_HELD,
|
|
81
|
+
ThreadCounterparty,
|
|
82
|
+
)
|
|
83
|
+
from ..guards.statements import StatementRecord, claim_of
|
|
84
|
+
from .replay import (
|
|
85
|
+
MONEY_IN,
|
|
86
|
+
ExecutedActs,
|
|
87
|
+
action_for_history_entry,
|
|
88
|
+
bound_agent_input,
|
|
89
|
+
deal_claim_statements,
|
|
90
|
+
names_two_deals,
|
|
91
|
+
statement_for_history_entry,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
__all__ = ["HistoryLedger", "history_act_payload", "history_ledger"]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _Disposition(TypedDict):
|
|
101
|
+
decision: str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class _Sealed(TypedDict, total=False):
|
|
105
|
+
disposition: _Disposition
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class _HistoryRecord(_Sealed):
|
|
109
|
+
"""A record ``history_ledger`` writes for one history entry, with a
|
|
110
|
+
``disposition`` only when its capsule seals one. Its ``asg_payload`` is
|
|
111
|
+
what a decision on the act seals (``act_payload``) plus
|
|
112
|
+
``live_history``."""
|
|
113
|
+
|
|
114
|
+
capsule_id: str
|
|
115
|
+
operator: object
|
|
116
|
+
developer: object
|
|
117
|
+
action_type: object
|
|
118
|
+
action_id: object
|
|
119
|
+
timestamp: object
|
|
120
|
+
asg_payload: dict[str, object]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class HistoryLedger:
|
|
125
|
+
"""What ``history_ledger`` wrote: how many records in each state (acts,
|
|
126
|
+
and statements, which are not acts), and whether the history was known to
|
|
127
|
+
be complete."""
|
|
128
|
+
|
|
129
|
+
complete: bool
|
|
130
|
+
disposition: int
|
|
131
|
+
no_disposition: int
|
|
132
|
+
unread: int
|
|
133
|
+
statement: int = 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def history_ledger(envelope: dict, ledger: LedgerAPI, *, caps_fold: FoldDefinition | None = None) -> HistoryLedger:
|
|
137
|
+
"""Write the acts of ``envelope``'s ``history`` into ``ledger``, oldest
|
|
138
|
+
first, and an ``incomplete`` record after them when the history is not
|
|
139
|
+
known to be complete (module docstring). ``caps_fold`` is the engine's;
|
|
140
|
+
when it counts executed acts, each act's spend record follows it."""
|
|
141
|
+
history = envelope.get("history")
|
|
142
|
+
scope = envelope.get("history_scope")
|
|
143
|
+
complete = isinstance(history, list) and isinstance(scope, dict) and scope.get("complete") is True
|
|
144
|
+
dated: list[tuple[datetime, int, dict]] = []
|
|
145
|
+
for index, entry in enumerate(history if isinstance(history, list) else []):
|
|
146
|
+
at = _time(entry.get("timestamp")) if isinstance(entry, dict) else None
|
|
147
|
+
if at is None:
|
|
148
|
+
complete = False
|
|
149
|
+
continue
|
|
150
|
+
if "item_ref" in entry and not _is_item_ref(entry["item_ref"]):
|
|
151
|
+
# Which sale the act belongs to cannot be read.
|
|
152
|
+
complete = False
|
|
153
|
+
dated.append((at, index, entry))
|
|
154
|
+
dated.sort(key=lambda item: (item[0], item[1]))
|
|
155
|
+
checked = envelope.get("record")
|
|
156
|
+
checked_at = _time(checked.get("timestamp")) if isinstance(checked, dict) else None
|
|
157
|
+
counts = {DISPOSITION: 0, NO_DISPOSITION: 0, UNREAD: 0, STATEMENT: 0}
|
|
158
|
+
executed = ExecutedActs([entry for _, _, entry in dated]) if counts_executed_acts(caps_fold) else None
|
|
159
|
+
written: set[str] = set()
|
|
160
|
+
claims: set[str] = set()
|
|
161
|
+
for at, _, entry in dated:
|
|
162
|
+
stated = _statement_record(entry)
|
|
163
|
+
if stated is not None and (checked_at is None or at > checked_at):
|
|
164
|
+
# A claim sealed after the check, or with no check time to hold it to.
|
|
165
|
+
continue
|
|
166
|
+
record = stated or _act_record(entry)
|
|
167
|
+
if record["capsule_id"] in written or (stated is not None and claim_of(stated) in claims):
|
|
168
|
+
# The same act, or claim, given twice: written, and counted, once.
|
|
169
|
+
continue
|
|
170
|
+
written.add(record["capsule_id"])
|
|
171
|
+
if stated is not None:
|
|
172
|
+
claims.add(claim_of(stated))
|
|
173
|
+
counts[str(record["asg_payload"][LIVE_HISTORY])] += 1
|
|
174
|
+
ledger.append(dict(record), consequential=False)
|
|
175
|
+
if executed is not None and stated is None:
|
|
176
|
+
shown = bound_agent_input(entry)
|
|
177
|
+
spend = executed.record(entry, shown, json_digest(shown) if shown is not None else record["capsule_id"],
|
|
178
|
+
every_record_is_an_act=True)
|
|
179
|
+
if spend is not None:
|
|
180
|
+
ledger.append(dict(spend), consequential=False)
|
|
181
|
+
if isinstance(checked, dict):
|
|
182
|
+
for stated in deal_claim_statements(checked).statements:
|
|
183
|
+
if claim_of(stated) in claims:
|
|
184
|
+
# Given in the history too: one claim, one statement.
|
|
185
|
+
continue
|
|
186
|
+
claims.add(claim_of(stated))
|
|
187
|
+
counts[STATEMENT] += 1
|
|
188
|
+
ledger.append(dict(_marked(stated)), consequential=False)
|
|
189
|
+
if not complete:
|
|
190
|
+
ledger.append(_incomplete_record(envelope.get("record")), consequential=False)
|
|
191
|
+
else:
|
|
192
|
+
held = window_held_reason(scope, [entry for _, _, entry in dated], caps_fold, checked)
|
|
193
|
+
if held is not None:
|
|
194
|
+
ledger.append(_window_held_record(envelope.get("record"), held), consequential=False)
|
|
195
|
+
return HistoryLedger(
|
|
196
|
+
complete=complete,
|
|
197
|
+
disposition=counts[DISPOSITION],
|
|
198
|
+
no_disposition=counts[NO_DISPOSITION],
|
|
199
|
+
unread=counts[UNREAD],
|
|
200
|
+
statement=counts[STATEMENT],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _statement_record(entry: dict) -> StatementRecord | None:
|
|
205
|
+
"""The record the replay writes for a history entry that is a claim the
|
|
206
|
+
user's agent made (``statement_for_history_entry``), marked
|
|
207
|
+
``statement``; ``None`` for any other entry."""
|
|
208
|
+
stated = statement_for_history_entry(entry)
|
|
209
|
+
return _marked(stated) if stated is not None else None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _marked(stated: StatementRecord) -> StatementRecord:
|
|
213
|
+
"""A statement record with its ``live_history`` marker."""
|
|
214
|
+
return StatementRecord(**{**stated, "asg_payload": {**stated["asg_payload"], LIVE_HISTORY: STATEMENT}})
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _time(value: object) -> datetime | None:
|
|
218
|
+
"""``value`` as an aware time, when it is an RFC 3339 string with a zone."""
|
|
219
|
+
if not isinstance(value, str):
|
|
220
|
+
return None
|
|
221
|
+
try:
|
|
222
|
+
at = datetime.fromisoformat(value)
|
|
223
|
+
except ValueError:
|
|
224
|
+
# Not a time: the caller reads the history as incomplete.
|
|
225
|
+
return None
|
|
226
|
+
return at if at.tzinfo is not None else None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _sealed_decision(entry: dict) -> str | None:
|
|
230
|
+
"""The ``disposition.decision`` an entry's capsule seals, or ``None``."""
|
|
231
|
+
disposition = entry.get("disposition")
|
|
232
|
+
decision = disposition.get("decision") if isinstance(disposition, dict) else None
|
|
233
|
+
return decision if isinstance(decision, str) and decision else None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _act_record(entry: dict) -> _HistoryRecord:
|
|
237
|
+
"""The record ``history_ledger`` writes for one history entry: its
|
|
238
|
+
capsule's own identity fields, the act as ``act_payload`` carries it, and
|
|
239
|
+
the disposition its capsule seals, when it seals one."""
|
|
240
|
+
action = action_for_history_entry(entry)
|
|
241
|
+
decision = _sealed_decision(entry)
|
|
242
|
+
if action is None:
|
|
243
|
+
payload: dict[str, object] = {LIVE_HISTORY: UNREAD}
|
|
244
|
+
item_ref = entry.get("item_ref")
|
|
245
|
+
if _is_item_ref(item_ref):
|
|
246
|
+
payload["item_ref"] = item_ref
|
|
247
|
+
else:
|
|
248
|
+
payload = history_act_payload(action, sealed_disposition=decision is not None)
|
|
249
|
+
capsule_id = entry.get("capsule_id")
|
|
250
|
+
record = _HistoryRecord(
|
|
251
|
+
capsule_id=capsule_id if isinstance(capsule_id, str) and capsule_id else json_digest(entry),
|
|
252
|
+
operator=entry.get("operator", ""),
|
|
253
|
+
developer=entry.get("developer", ""),
|
|
254
|
+
action_type=entry.get("action_type", ""),
|
|
255
|
+
action_id=entry.get("action_id"),
|
|
256
|
+
timestamp=entry.get("timestamp"),
|
|
257
|
+
asg_payload=payload,
|
|
258
|
+
)
|
|
259
|
+
if decision is not None:
|
|
260
|
+
record["disposition"] = _Disposition(decision=decision)
|
|
261
|
+
return record
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def history_act_payload(action: Action, *, sealed_disposition: bool) -> dict[str, object]:
|
|
265
|
+
"""The ``asg_payload`` ``history_ledger`` writes for a history act it
|
|
266
|
+
read: what a decision on ``action`` seals about the act (``act_payload``),
|
|
267
|
+
its state (``disposition`` when its capsule seals one), and, beside them,
|
|
268
|
+
the counterparty its check sealed as ``thread_counterparty`` when the
|
|
269
|
+
action carries one, whole (``Action.counterparty_malformed``). That member is this ledger's own state: it is never in
|
|
270
|
+
``act_payload``, so no decision seals it (``guards/history_state.py``)."""
|
|
271
|
+
payload: dict[str, object] = {**act_payload(action),
|
|
272
|
+
LIVE_HISTORY: DISPOSITION if sealed_disposition else NO_DISPOSITION}
|
|
273
|
+
if action.counterparty_ids and action.counterparty_fp_alg and not action.counterparty_malformed:
|
|
274
|
+
payload[THREAD_COUNTERPARTY] = ThreadCounterparty(fp_alg=action.counterparty_fp_alg,
|
|
275
|
+
ids=dict(action.counterparty_ids))
|
|
276
|
+
return payload
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _incomplete_record(checked: object) -> dict:
|
|
280
|
+
"""The one record saying the history may be missing acts, under the
|
|
281
|
+
checked record's operator, at its time."""
|
|
282
|
+
checked = checked if isinstance(checked, dict) else {}
|
|
283
|
+
body = {
|
|
284
|
+
"operator": checked.get("operator", ""),
|
|
285
|
+
"timestamp": checked.get("timestamp"),
|
|
286
|
+
"asg_payload": {LIVE_HISTORY: INCOMPLETE},
|
|
287
|
+
}
|
|
288
|
+
return {**body, "capsule_id": json_digest(body)}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def window_held_reason(scope: dict, entries: list[dict], caps_fold: FoldDefinition | None,
|
|
292
|
+
checked: object) -> str | None:
|
|
293
|
+
"""Why a complete history cannot decide the caps window, or None: it
|
|
294
|
+
reaches back fewer days (``history_scope.days``, 0 when absent) than the
|
|
295
|
+
window, some act the window would count carries no recorded decision and
|
|
296
|
+
a spend that cannot be read, or some act names a taxonomy version whose
|
|
297
|
+
classes are not the engine's. A history that holds an act it reaches back
|
|
298
|
+
over is no proof that the acts before its first are absent, so a short one
|
|
299
|
+
is held; an undecided act's spend is not in a total that counts accepted
|
|
300
|
+
acts only, and when it cannot be read the total is not known (Sherlock2,
|
|
301
|
+
ops u504); an act classed under other names cannot be summed against this
|
|
302
|
+
engine's limits. ``checked`` is the record being checked, whose key and
|
|
303
|
+
time place the window. With no caps fold there is no window to hold. The
|
|
304
|
+
Go guard holds the same window with the same reasons, in this order
|
|
305
|
+
(go/guard/engine.go windowCoverage)."""
|
|
306
|
+
if caps_fold is None or caps_fold.window is None or not caps_fold.window.duration:
|
|
307
|
+
return None
|
|
308
|
+
window = caps_fold.window.duration
|
|
309
|
+
days = scope.get("days")
|
|
310
|
+
days = days if isinstance(days, int) and not isinstance(days, bool) else 0
|
|
311
|
+
if days * 86400 < parse_duration_seconds(window):
|
|
312
|
+
return f"the history supplied reaches back {days} day(s); the {window} total needs {window}"
|
|
313
|
+
undecided: list[dict] = []
|
|
314
|
+
other = 0
|
|
315
|
+
for entry in entries:
|
|
316
|
+
if _statement_record(entry) is not None:
|
|
317
|
+
continue
|
|
318
|
+
shown = bound_agent_input(entry)
|
|
319
|
+
if not counts_executed_acts(caps_fold) and _undecided_unreadable(entry, shown):
|
|
320
|
+
undecided.append(entry)
|
|
321
|
+
version = _act_taxonomy_version(shown)
|
|
322
|
+
if version is not None and not same_classes(version):
|
|
323
|
+
other += 1
|
|
324
|
+
held = _in_window(caps_fold, undecided, checked)
|
|
325
|
+
if held:
|
|
326
|
+
return (
|
|
327
|
+
f"{held} earlier act(s) carry no recorded decision; the {window} total as the engine computes it "
|
|
328
|
+
"counts accepted acts only, so it cannot be computed"
|
|
329
|
+
)
|
|
330
|
+
if other:
|
|
331
|
+
return (
|
|
332
|
+
f"{other} earlier act(s) were classed under another taxonomy version than the engine's "
|
|
333
|
+
f"({ENGINE_TAXONOMY.version}), so the {window} total cannot be computed"
|
|
334
|
+
)
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _undecided_unreadable(entry: dict, shown: dict | None) -> bool:
|
|
339
|
+
"""Whether a history act is one the window cannot leave out: checked,
|
|
340
|
+
with no recorded decision, not moving money in, and with a spend that
|
|
341
|
+
cannot be read. As the Go guard reads it (``DecodeHistoryAct``,
|
|
342
|
+
``windowCoverage``): a decision is any ``disposition.decision`` the
|
|
343
|
+
capsule seals, and an act naming two deals is decided (refused); an act
|
|
344
|
+
taken without a check is counted as spend, and one that moved money in
|
|
345
|
+
never adds to the window; the spend is the act's ``spend_minor``, an
|
|
346
|
+
integer of at least 0. A capsule that does not bind its ``agent_input``
|
|
347
|
+
states no spend that can be read. Under a fold that counts executed acts
|
|
348
|
+
the window's own records decide this instead (``ExecutedActs``), so it
|
|
349
|
+
is not read there."""
|
|
350
|
+
disposition = entry.get("disposition")
|
|
351
|
+
if isinstance(disposition, dict) and disposition.get("decision") is not None:
|
|
352
|
+
return False
|
|
353
|
+
acted, unchecked = _act_member(shown)
|
|
354
|
+
if acted is None:
|
|
355
|
+
return True
|
|
356
|
+
if unchecked or names_two_deals(shown) or acted.get("direction") == MONEY_IN:
|
|
357
|
+
return False
|
|
358
|
+
spend = acted.get("spend_minor")
|
|
359
|
+
return not (isinstance(spend, int) and not isinstance(spend, bool) and spend >= 0)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _in_window(caps_fold: FoldDefinition, acts: list[dict], checked: object) -> int:
|
|
363
|
+
"""How many of ``acts`` the caps window would count, by key and time
|
|
364
|
+
alone: under the checked record's value for the fold's key, in the window
|
|
365
|
+
as of its time (``caps.in_window_count``). When the checked record's time
|
|
366
|
+
cannot be read, every one: caps cannot place the window."""
|
|
367
|
+
if not acts:
|
|
368
|
+
return 0
|
|
369
|
+
checked = checked if isinstance(checked, dict) else {}
|
|
370
|
+
if _time(checked.get("timestamp")) is None:
|
|
371
|
+
return len(acts)
|
|
372
|
+
key_value = checked.get(caps_fold.key, "") if caps_fold.key is not None else None
|
|
373
|
+
placed = [{"timestamp": act.get("timestamp"), "developer": act.get("developer"), "operator": act.get("operator")}
|
|
374
|
+
for act in acts]
|
|
375
|
+
return in_window_count(caps_fold, placed, key_value, checked["timestamp"])
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _act_member(shown: object) -> tuple[dict | None, bool]:
|
|
379
|
+
"""The member of a disclosed act that records the act, and whether it
|
|
380
|
+
was taken without a check, as the Go guard reads it
|
|
381
|
+
(``DecodeHistoryAct``): ``body.attempted`` of a typed
|
|
382
|
+
``action-outcome/v0`` or ``body.unchecked``, else the body itself;
|
|
383
|
+
``(None, False)`` when there is no body."""
|
|
384
|
+
body = shown.get("body") if isinstance(shown, dict) else None
|
|
385
|
+
if not isinstance(body, dict):
|
|
386
|
+
return None, False
|
|
387
|
+
if shown.get("type") == "action-outcome/v0" and isinstance(body.get("attempted"), dict):
|
|
388
|
+
return body["attempted"], True
|
|
389
|
+
if isinstance(body.get("unchecked"), dict):
|
|
390
|
+
return body["unchecked"], True
|
|
391
|
+
return body, False
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _act_taxonomy_version(shown: object) -> object:
|
|
395
|
+
"""The taxonomy version an act names, as the Go guard reads it
|
|
396
|
+
(``DecodeHistoryAct``): an act taken without a check names it in the
|
|
397
|
+
member that records the act (``_act_member``), and the body's own stands
|
|
398
|
+
in when that member names none."""
|
|
399
|
+
acted, unchecked = _act_member(shown)
|
|
400
|
+
if acted is None:
|
|
401
|
+
return None
|
|
402
|
+
version = acted.get("taxonomy_version") if unchecked else None
|
|
403
|
+
return version if version is not None else shown["body"].get("taxonomy_version")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _window_held_record(checked: object, reason: str) -> dict:
|
|
407
|
+
"""The one record saying the caps window cannot be summed over this
|
|
408
|
+
history, why, under the checked record's operator, at its time."""
|
|
409
|
+
checked = checked if isinstance(checked, dict) else {}
|
|
410
|
+
body = {
|
|
411
|
+
"operator": checked.get("operator", ""),
|
|
412
|
+
"timestamp": checked.get("timestamp"),
|
|
413
|
+
"asg_payload": {LIVE_HISTORY: WINDOW_HELD, "held_reason": reason},
|
|
414
|
+
}
|
|
415
|
+
return {**body, "capsule_id": json_digest(body)}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _is_item_ref(value: object) -> bool:
|
|
419
|
+
"""Whether ``value`` is an ``item_ref`` in its agreed shape."""
|
|
420
|
+
return isinstance(value, str) and _HEX64.fullmatch(value) is not None
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Dry-run report data shapes.
|
|
3
|
+
|
|
4
|
+
These carry only what the design's headline folds / per-guard tables / the
|
|
5
|
+
single "most consequential" callout need. Every field here is either read
|
|
6
|
+
straight off a real ``GuardDecision`` (T3) or computed from one — nothing in
|
|
7
|
+
this module invents a number; see ``build.py`` for where each field comes
|
|
8
|
+
from.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
__all__ = ["ReportRow", "GuardSection", "ModelNote", "DryRunReport"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ReportRow:
|
|
19
|
+
"""One would-have-held decision, attributed to the guard whose
|
|
20
|
+
constraint failed it. ``capsule`` is this decision's own sealed capsule
|
|
21
|
+
(dry_run decisions are still built, signed, and appended to the guard's
|
|
22
|
+
own replay-local view — see ``docs/failure-semantics.md``'s "an unsigned
|
|
23
|
+
record is not a record" and ``engine.py``'s dry_run handling). ``cited_capsule``
|
|
24
|
+
is the prior record this row's check matched against (e.g. the earlier
|
|
25
|
+
payment a dedupe hit repeats), when the check produced one — carried in
|
|
26
|
+
full so the browser can re-verify it, not just display its id.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
when: str
|
|
30
|
+
agent: str
|
|
31
|
+
why: str
|
|
32
|
+
capsule: dict
|
|
33
|
+
amount_minor: int | None = None
|
|
34
|
+
currency: str | None = None
|
|
35
|
+
cited_capsule_id: str | None = None
|
|
36
|
+
cited_capsule: dict | None = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def fingerprint(self) -> str:
|
|
40
|
+
capsule_id = self.capsule.get("capsule_id", "")
|
|
41
|
+
return capsule_id[:8] + "…" if capsule_id else ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class GuardSection:
|
|
46
|
+
guard_id: str
|
|
47
|
+
what: str
|
|
48
|
+
rows: tuple[ReportRow, ...] = field(default_factory=tuple)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class ModelNote:
|
|
53
|
+
"""Optional, operator-supplied model commentary. Never generated by this
|
|
54
|
+
module — see ``build.py``'s docstring. Omitted entirely (``None``) is the
|
|
55
|
+
default and fully supported path."""
|
|
56
|
+
|
|
57
|
+
quote: str
|
|
58
|
+
model_id: str
|
|
59
|
+
record_count: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class DryRunReport:
|
|
64
|
+
operator: str
|
|
65
|
+
agents: tuple[str, ...]
|
|
66
|
+
since_label: str | None
|
|
67
|
+
actions_replayed: int
|
|
68
|
+
record_range: tuple[int, int]
|
|
69
|
+
checkpoint: int
|
|
70
|
+
guards: tuple[GuardSection, ...]
|
|
71
|
+
replay_command: str
|
|
72
|
+
generated_at: str
|
|
73
|
+
model_note: ModelNote | None = None
|
|
74
|
+
# The active policy manifest's own digest (``capsule_ledger.policy``), when
|
|
75
|
+
# one was resolved for this replay -- "evaluated under manifest <digest>"
|
|
76
|
+
# (see ``cli/guard_cmds.py``). ``None`` when no manifest was configured.
|
|
77
|
+
manifest_digest: str | None = None
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def held_rows(self) -> list[tuple[str, ReportRow]]:
|
|
81
|
+
return [(section.guard_id, row) for section in self.guards for row in section.rows]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def would_have_held_count(self) -> int:
|
|
85
|
+
return len(self.held_rows)
|
|
86
|
+
|
|
87
|
+
def consequential(self) -> tuple[str, ReportRow] | None:
|
|
88
|
+
"""The single highest-amount would-have-held row, across all guards.
|
|
89
|
+
|
|
90
|
+
A pick, never a sum -- money in different currencies is never added
|
|
91
|
+
together (see ``value_held_label`` in ``build.py``)."""
|
|
92
|
+
candidates = [(g, r) for g, r in self.held_rows if r.amount_minor is not None]
|
|
93
|
+
if not candidates:
|
|
94
|
+
return self.held_rows[0] if self.held_rows else None
|
|
95
|
+
return max(candidates, key=lambda gr: gr[1].amount_minor)
|