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,214 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""promise_requires_approval check: a class of statement the agent may make
|
|
3
|
+
only inside its task authority or with a one-shot approval.
|
|
4
|
+
|
|
5
|
+
Reads ``Action.representation_class``, one member of the closed set
|
|
6
|
+
``representation_classes``. A class outside ``requires_approval`` passes. A
|
|
7
|
+
class inside it passes on either of two bases, named in the evidence's
|
|
8
|
+
``authority_basis``:
|
|
9
|
+
|
|
10
|
+
- ``task_authority_ref``: the task authority already allows it. The sealed
|
|
11
|
+
task-authority record is read only when the engine's own digest of it
|
|
12
|
+
equals ``Action.task_authority_ref``
|
|
13
|
+
(``task_authority.bind_task_authority_record``), and its
|
|
14
|
+
``body.authorized_representation_classes`` (``AUTHORIZED_PATH``) lists the
|
|
15
|
+
class. No approval event is needed. From 1.1.0 the wicket config names
|
|
16
|
+
the path (``authorized_path``): capsulectl's task authority seals the
|
|
17
|
+
classes the user ordered at ``body.ordered_representations``, and the
|
|
18
|
+
list counts only when every member is a class in
|
|
19
|
+
``representation_classes``; a malformed list allows nothing. 1.0.0 names
|
|
20
|
+
none and reads ``AUTHORIZED_PATH`` as it always did.
|
|
21
|
+
- ``authorized_by``: a bound approval for a statement outside the task
|
|
22
|
+
authority. ``Action.authorized_by`` is the SHA-256 digest of the WHOLE
|
|
23
|
+
sealed approval record, that record is supplied beside the action for one
|
|
24
|
+
decision (``GuardEngine.check(..., authorization_record=...)``), the
|
|
25
|
+
engine recomputes its digest itself (``authorization_record_digest``) and
|
|
26
|
+
reads it ONLY when that equals the reference, and the record's
|
|
27
|
+
``body.representation_class`` (``CLASS_PATH``) names this action's class.
|
|
28
|
+
|
|
29
|
+
Anything less fails: no reference, no record, a record the reference does
|
|
30
|
+
not bind, a record with no digest, or an approval for another class. Never
|
|
31
|
+
``n/a``: an unreadable approval is no approval. Under 1.1.0 a failure on an
|
|
32
|
+
action that cites no task authority (an x-deal-v0 deal has none) reads
|
|
33
|
+
"no order or approval of yours covers this promise"; 1.0.0 keeps its
|
|
34
|
+
own reasons for that failure. Both name a class after its article ("an
|
|
35
|
+
authenticity statement"). A class no approval may
|
|
36
|
+
clear belongs to ``promise_never``, not here.
|
|
37
|
+
|
|
38
|
+
A class outside the closed set fails closed. Applies only to the configured
|
|
39
|
+
``action_classes``.
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from typing import TypedDict
|
|
44
|
+
|
|
45
|
+
from agent_action_capsule import json_digest
|
|
46
|
+
from agent_action_capsule.canonical import FloatInDigestError, UnsafeIntegerError
|
|
47
|
+
|
|
48
|
+
from ..action import Action
|
|
49
|
+
from ..capsule import ConstraintOutcome, NotApplicableEvidence, not_applicable_evidence
|
|
50
|
+
from .base import CheckOutcome
|
|
51
|
+
from .promise_never import with_article
|
|
52
|
+
from .required_disclosure import require_known_classes
|
|
53
|
+
from .task_authority import TaskAuthorityRecord, UnboundRecord, bind_task_authority_record
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"AUTHORIZED_PATH",
|
|
57
|
+
"CLASS_PATH",
|
|
58
|
+
"AuthorizationBody",
|
|
59
|
+
"AuthorizationRecord",
|
|
60
|
+
"authorization_record_digest",
|
|
61
|
+
"check_promise_requires_approval",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
_CHECK_ID = "promise_requires_approval"
|
|
65
|
+
_METHOD = "authority_or_bound_approval_v0"
|
|
66
|
+
|
|
67
|
+
# Where the approved class sits inside the sealed approval record, one member
|
|
68
|
+
# name per level.
|
|
69
|
+
CLASS_PATH: tuple[str, ...] = ("body", "representation_class")
|
|
70
|
+
# Where the classes the task authority allows sit inside the sealed
|
|
71
|
+
# task-authority record.
|
|
72
|
+
AUTHORIZED_PATH: tuple[str, ...] = ("body", "authorized_representation_classes")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AuthorizationBody(TypedDict):
|
|
76
|
+
representation_class: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AuthorizationRecord(TypedDict):
|
|
80
|
+
"""The whole sealed approval record as decoded JSON, digested
|
|
81
|
+
byte-for-byte as supplied. Only the approved class is named here; any
|
|
82
|
+
other member the producer seals is covered by the digest and otherwise
|
|
83
|
+
unread."""
|
|
84
|
+
|
|
85
|
+
body: AuthorizationBody
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class PromiseEvidence(TypedDict):
|
|
89
|
+
representation_class: str
|
|
90
|
+
recognised: bool
|
|
91
|
+
requires_approval: bool
|
|
92
|
+
authority_basis: str | None
|
|
93
|
+
task_authority_ref: str | None
|
|
94
|
+
authorized_by: str | None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def authorization_record_digest(record: AuthorizationRecord) -> str:
|
|
98
|
+
"""The digest ``authorized_by`` carries for ``record``: SHA-256 over the
|
|
99
|
+
record's JCS bytes."""
|
|
100
|
+
return json_digest(record)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class _Unapproved(Exception):
|
|
104
|
+
"""The supplied approval does not approve this statement; the message says why."""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _approved_class(action: Action, record: AuthorizationRecord | None) -> str:
|
|
108
|
+
"""The class the bound approval names. Raises ``_Unapproved`` when the
|
|
109
|
+
record is missing, unbound or names no class."""
|
|
110
|
+
ref = action.authorized_by
|
|
111
|
+
if ref is None:
|
|
112
|
+
raise _Unapproved("the action cites no approval (authorized_by)")
|
|
113
|
+
if record is None:
|
|
114
|
+
raise _Unapproved("no approval record was supplied for authorized_by")
|
|
115
|
+
try:
|
|
116
|
+
bound = authorization_record_digest(record) == ref
|
|
117
|
+
except (FloatInDigestError, UnsafeIntegerError) as exc:
|
|
118
|
+
raise _Unapproved(f"the supplied approval record has no digest ({exc})") from exc
|
|
119
|
+
if not bound:
|
|
120
|
+
raise _Unapproved("ref mismatch: the supplied approval record is not the record authorized_by names")
|
|
121
|
+
node: object = record
|
|
122
|
+
for name in CLASS_PATH:
|
|
123
|
+
# Decoded JSON: each level is checked before it is read.
|
|
124
|
+
if not isinstance(node, dict) or name not in node:
|
|
125
|
+
raise _Unapproved(f"the approval record names no class at {'.'.join(CLASS_PATH)}")
|
|
126
|
+
node = node[name]
|
|
127
|
+
if not isinstance(node, str):
|
|
128
|
+
raise _Unapproved(f"the approval record's {'.'.join(CLASS_PATH)} is not one class")
|
|
129
|
+
return node
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _authority_allows(action: Action, record: TaskAuthorityRecord | None, cls: str,
|
|
133
|
+
path: tuple[str, ...], known: list[str] | None = None) -> bool:
|
|
134
|
+
"""Whether the bound task authority lists ``cls`` at ``path``. An unbound
|
|
135
|
+
or silent record allows nothing; the approval is then the only basis
|
|
136
|
+
left. With ``known`` (1.1.0), the list counts only when every member is
|
|
137
|
+
one of those class names: a list holding anything else is malformed and
|
|
138
|
+
allows nothing."""
|
|
139
|
+
try:
|
|
140
|
+
_, bound = bind_task_authority_record(action, record)
|
|
141
|
+
except UnboundRecord:
|
|
142
|
+
# No bound authority is not an error here: the approval is then the
|
|
143
|
+
# only basis, and a failure's reason names what the approval lacked.
|
|
144
|
+
return False
|
|
145
|
+
node: object = bound
|
|
146
|
+
for name in path:
|
|
147
|
+
# Decoded JSON: each level is checked before it is read.
|
|
148
|
+
if not isinstance(node, dict) or name not in node:
|
|
149
|
+
return False
|
|
150
|
+
node = node[name]
|
|
151
|
+
if not isinstance(node, list):
|
|
152
|
+
return False
|
|
153
|
+
if known is not None and not all(isinstance(c, str) and c in known for c in node):
|
|
154
|
+
return False
|
|
155
|
+
return cls in node
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _outcome(result: str, reason: str, evidence: PromiseEvidence | NotApplicableEvidence) -> CheckOutcome:
|
|
159
|
+
return CheckOutcome(
|
|
160
|
+
constraint=ConstraintOutcome(
|
|
161
|
+
id=_CHECK_ID, result=result, reason=reason, evidence=evidence, check_type="policy", method=_METHOD
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def check_promise_requires_approval(
|
|
167
|
+
action: Action,
|
|
168
|
+
task_authority_record: TaskAuthorityRecord | None,
|
|
169
|
+
authorization_record: AuthorizationRecord | None,
|
|
170
|
+
*,
|
|
171
|
+
representation_classes: list[str],
|
|
172
|
+
requires_approval: list[str],
|
|
173
|
+
action_classes: list[str],
|
|
174
|
+
authorized_path: list[str] | None = None,
|
|
175
|
+
) -> CheckOutcome:
|
|
176
|
+
require_known_classes(requires_approval, representation_classes, what="requires_approval")
|
|
177
|
+
if action.action_class not in action_classes:
|
|
178
|
+
return _outcome("n/a", "the rule is not configured for this action class",
|
|
179
|
+
not_applicable_evidence(_CHECK_ID, in_scope=False))
|
|
180
|
+
cls = action.representation_class
|
|
181
|
+
if cls is None:
|
|
182
|
+
return _outcome("n/a", "the action carries no representation_class; the statement could not be classed",
|
|
183
|
+
not_applicable_evidence(_CHECK_ID, in_scope=True, missing_field="representation_class"))
|
|
184
|
+
recognised = cls in representation_classes
|
|
185
|
+
needs = cls in requires_approval
|
|
186
|
+
|
|
187
|
+
def evidence(basis: str | None) -> PromiseEvidence:
|
|
188
|
+
return PromiseEvidence(representation_class=cls, recognised=recognised, requires_approval=needs,
|
|
189
|
+
authority_basis=basis, task_authority_ref=action.task_authority_ref,
|
|
190
|
+
authorized_by=action.authorized_by)
|
|
191
|
+
|
|
192
|
+
if not recognised:
|
|
193
|
+
return _outcome("fail", f"representation_class {cls!r} is not in {representation_classes}", evidence(None))
|
|
194
|
+
if not needs:
|
|
195
|
+
return _outcome("pass", f"{with_article(cls)} statement needs no approval", evidence(None))
|
|
196
|
+
path = AUTHORIZED_PATH if authorized_path is None else tuple(authorized_path)
|
|
197
|
+
known = representation_classes if authorized_path is not None else None
|
|
198
|
+
if _authority_allows(action, task_authority_record, cls, path, known):
|
|
199
|
+
return _outcome("pass", f"the task authority allows {with_article(cls)} statement", evidence("task_authority_ref"))
|
|
200
|
+
try:
|
|
201
|
+
approved = _approved_class(action, authorization_record)
|
|
202
|
+
except _Unapproved as exc:
|
|
203
|
+
why = str(exc)
|
|
204
|
+
else:
|
|
205
|
+
if approved == cls:
|
|
206
|
+
return _outcome("pass", f"the {cls} statement cites a bound approval for {cls}",
|
|
207
|
+
evidence("authorized_by"))
|
|
208
|
+
why = f"the approval is for {approved}, not {cls}"
|
|
209
|
+
reason = f"{with_article(cls)} statement is outside the task authority and needs a bound approval: {why}"
|
|
210
|
+
if authorized_path is not None and action.task_authority_ref is None:
|
|
211
|
+
# What the user's card says (design note capsule-engine#194 §2): the
|
|
212
|
+
# evidence still records the class and what was cited.
|
|
213
|
+
reason = "no order or approval of yours covers this promise"
|
|
214
|
+
return _outcome("fail", reason, evidence(None))
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""recipient_role check: membership of the disclosure recipient's role.
|
|
3
|
+
|
|
4
|
+
Reads ``Action.recipient_role``, one member of the closed set ``roles``
|
|
5
|
+
the wicket pins (``fulfilling_merchant``, ``third_party``, ``self``; a
|
|
6
|
+
seller's set is ``buyer``, ``third_party``, ``self``), and passes when it is
|
|
7
|
+
one of ``allowed_roles``. A role outside ``roles`` fails closed and the
|
|
8
|
+
evidence says it was not recognised. It records the role, never who the
|
|
9
|
+
recipient is. Applies only to the configured ``action_classes``.
|
|
10
|
+
|
|
11
|
+
``role_classes`` (optional) names roles that pass only for some classes of
|
|
12
|
+
data: ``{platform: [email]}`` passes a disclosure to a platform when every
|
|
13
|
+
class in ``Action.disclosed_classes`` is ``email``. One that gives any other
|
|
14
|
+
class fails, and so does one that names no class, since it cannot be shown
|
|
15
|
+
to give only those. A wicket that sets it records the classes it read and the
|
|
16
|
+
roles' classes in the evidence; one that does not keeps the evidence it had.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TypedDict
|
|
21
|
+
|
|
22
|
+
from ..action import Action
|
|
23
|
+
from ..capsule import ConstraintOutcome, NotApplicableEvidence, not_applicable_evidence
|
|
24
|
+
from .base import CheckOutcome
|
|
25
|
+
|
|
26
|
+
__all__ = ["check_recipient_role"]
|
|
27
|
+
|
|
28
|
+
_CHECK_ID = "recipient_role"
|
|
29
|
+
_METHOD = "set_membership_v0"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RoleEvidence(TypedDict):
|
|
33
|
+
recipient_role: str
|
|
34
|
+
recognised: bool
|
|
35
|
+
allowed_roles: list[str]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RoleClassesEvidence(RoleEvidence):
|
|
39
|
+
role_classes: dict[str, list[str]]
|
|
40
|
+
disclosed_classes: list[str] | None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _outcome(
|
|
44
|
+
result: str, reason: str, evidence: RoleEvidence | RoleClassesEvidence | NotApplicableEvidence
|
|
45
|
+
) -> CheckOutcome:
|
|
46
|
+
return CheckOutcome(
|
|
47
|
+
constraint=ConstraintOutcome(
|
|
48
|
+
id=_CHECK_ID, result=result, reason=reason, evidence=evidence, check_type="policy", method=_METHOD
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _either(words: list[str]) -> str:
|
|
54
|
+
"""``a``, ``a or b``, ``a, b or c``; ``no role`` for none."""
|
|
55
|
+
if not words:
|
|
56
|
+
return "no role"
|
|
57
|
+
return words[0] if len(words) == 1 else f"{', '.join(words[:-1])} or {words[-1]}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _passing(allowed: list[str], role_classes: dict[str, list[str]]) -> str:
|
|
61
|
+
"""The roles that pass, once: ``fulfilling_merchant or self, or a
|
|
62
|
+
platform given only email``."""
|
|
63
|
+
limited = [f"a {role} given only {_either(classes)}" for role, classes in sorted(role_classes.items())]
|
|
64
|
+
if not allowed:
|
|
65
|
+
return _either(limited)
|
|
66
|
+
return _either(allowed) + "".join(f", or {phrase}" for phrase in limited)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def check_recipient_role(
|
|
70
|
+
action: Action,
|
|
71
|
+
*,
|
|
72
|
+
roles: list[str],
|
|
73
|
+
allowed_roles: list[str],
|
|
74
|
+
action_classes: list[str],
|
|
75
|
+
role_classes: dict[str, list[str]] | None = None,
|
|
76
|
+
) -> CheckOutcome:
|
|
77
|
+
limited = {role: sorted(classes) for role, classes in (role_classes or {}).items()}
|
|
78
|
+
misplaced = sorted(set(limited) - set(roles)) + sorted(set(limited) & set(allowed_roles))
|
|
79
|
+
if misplaced or any(not classes for classes in limited.values()):
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"role_classes must name roles in roles and not in allowed_roles, each with at least one class; "
|
|
82
|
+
f"got {sorted(limited)}"
|
|
83
|
+
)
|
|
84
|
+
if action.action_class not in action_classes:
|
|
85
|
+
return _outcome("n/a", "the rule is not configured for this action class",
|
|
86
|
+
not_applicable_evidence(_CHECK_ID, in_scope=False))
|
|
87
|
+
role = action.recipient_role
|
|
88
|
+
if role is None:
|
|
89
|
+
return _outcome("n/a", "the action carries no recipient_role; the recipient's role could not be checked",
|
|
90
|
+
not_applicable_evidence(_CHECK_ID, in_scope=True, missing_field="recipient_role"))
|
|
91
|
+
recognised = role in roles
|
|
92
|
+
allowed = sorted(allowed_roles)
|
|
93
|
+
evidence: RoleEvidence | RoleClassesEvidence = RoleEvidence(
|
|
94
|
+
recipient_role=role, recognised=recognised, allowed_roles=allowed
|
|
95
|
+
)
|
|
96
|
+
if role_classes is not None:
|
|
97
|
+
disclosed = list(action.disclosed_classes) if action.disclosed_classes is not None else None
|
|
98
|
+
evidence = RoleClassesEvidence(**evidence, role_classes=limited, disclosed_classes=disclosed)
|
|
99
|
+
passing = _passing(allowed, limited)
|
|
100
|
+
# A role outside the closed set fails even if ``allowed_roles`` names it.
|
|
101
|
+
if not recognised:
|
|
102
|
+
return _outcome("fail", f"recipient_role {role!r} is not a role this rule knows; personal data "
|
|
103
|
+
f"passes only to {passing}", evidence)
|
|
104
|
+
if role in allowed:
|
|
105
|
+
return _outcome("pass", f"the recipient is the {role}", evidence)
|
|
106
|
+
if role in limited:
|
|
107
|
+
classes = limited[role]
|
|
108
|
+
if action.disclosed_classes is None:
|
|
109
|
+
return _outcome("fail", f"the recipient is a {role}, which may be given only {_either(classes)}; "
|
|
110
|
+
"the disclosure names no class of data", evidence)
|
|
111
|
+
others = [c for c in action.disclosed_classes if c not in classes]
|
|
112
|
+
if others:
|
|
113
|
+
return _outcome("fail", f"the recipient is a {role}, which may be given only {_either(classes)}; "
|
|
114
|
+
f"this disclosure also gives {_either(others)}", evidence)
|
|
115
|
+
return _outcome("pass", f"the recipient is a {role}, given only {_either(list(action.disclosed_classes))}",
|
|
116
|
+
evidence)
|
|
117
|
+
return _outcome("fail", f"the recipient is a {role}; personal data passes only to {passing}", evidence)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""recurring_charge check: a read of the action's declared recurrence.
|
|
3
|
+
|
|
4
|
+
Fails when ``Action.recurrence`` is present and is not one of the configured
|
|
5
|
+
``one_time_values`` -- that is, when the payment sets up a charge that
|
|
6
|
+
repeats. Applies only to the configured ``action_classes``.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TypedDict
|
|
11
|
+
|
|
12
|
+
from ..action import Action
|
|
13
|
+
from ..capsule import ConstraintOutcome, NotApplicableEvidence, not_applicable_evidence
|
|
14
|
+
from .base import CheckOutcome
|
|
15
|
+
|
|
16
|
+
__all__ = ["check_recurring_charge"]
|
|
17
|
+
|
|
18
|
+
_CHECK_ID = "recurring_charge"
|
|
19
|
+
_METHOD = "structured_field_v0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RecurrenceEvidence(TypedDict):
|
|
23
|
+
recurrence: str
|
|
24
|
+
one_time_values: list[str]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _outcome(result: str, reason: str, evidence: NotApplicableEvidence | RecurrenceEvidence) -> CheckOutcome:
|
|
28
|
+
return CheckOutcome(
|
|
29
|
+
constraint=ConstraintOutcome(
|
|
30
|
+
id=_CHECK_ID, result=result, reason=reason, evidence=evidence, check_type="policy", method=_METHOD
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_recurring_charge(action: Action, *, one_time_values: list[str], action_classes: list[str]) -> CheckOutcome:
|
|
36
|
+
if action.action_class not in action_classes:
|
|
37
|
+
return _outcome(
|
|
38
|
+
"n/a", "the rule is not configured for this action class", not_applicable_evidence(_CHECK_ID, in_scope=False)
|
|
39
|
+
)
|
|
40
|
+
if action.recurrence is None:
|
|
41
|
+
return _outcome(
|
|
42
|
+
"n/a",
|
|
43
|
+
"the action carries no recurrence; whether it repeats could not be read",
|
|
44
|
+
not_applicable_evidence(_CHECK_ID, in_scope=True, missing_field="recurrence"),
|
|
45
|
+
)
|
|
46
|
+
evidence = RecurrenceEvidence(recurrence=action.recurrence, one_time_values=sorted(one_time_values))
|
|
47
|
+
if action.recurrence in one_time_values:
|
|
48
|
+
return _outcome("pass", f"recurrence {action.recurrence!r} is a one-time payment", evidence)
|
|
49
|
+
return _outcome("fail", f"recurrence {action.recurrence!r} sets up a repeating charge", evidence)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""refundability check: can the payment be refunded?
|
|
3
|
+
|
|
4
|
+
Reads ``Action.refundable`` and fails when it is ``False``. It records the
|
|
5
|
+
value it read and the rail beside it (``None`` when the action names none);
|
|
6
|
+
it does not read the refund terms themselves. Applies only to the configured
|
|
7
|
+
``action_classes``.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TypedDict
|
|
12
|
+
|
|
13
|
+
from ..action import Action
|
|
14
|
+
from ..capsule import ConstraintOutcome, NotApplicableEvidence, not_applicable_evidence
|
|
15
|
+
from .base import CheckOutcome
|
|
16
|
+
|
|
17
|
+
__all__ = ["check_refundability"]
|
|
18
|
+
|
|
19
|
+
_CHECK_ID = "refundability"
|
|
20
|
+
_METHOD = "declared_boolean_v0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RefundabilityEvidence(TypedDict):
|
|
24
|
+
refundable: bool
|
|
25
|
+
rail: str | None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _outcome(result: str, reason: str, evidence: RefundabilityEvidence | NotApplicableEvidence) -> CheckOutcome:
|
|
29
|
+
return CheckOutcome(
|
|
30
|
+
constraint=ConstraintOutcome(
|
|
31
|
+
id=_CHECK_ID, result=result, reason=reason, evidence=evidence, check_type="policy", method=_METHOD
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_refundability(action: Action, *, action_classes: list[str]) -> CheckOutcome:
|
|
37
|
+
if action.action_class not in action_classes:
|
|
38
|
+
return _outcome("n/a", "the rule is not configured for this action class",
|
|
39
|
+
not_applicable_evidence(_CHECK_ID, in_scope=False))
|
|
40
|
+
if action.refundable is None:
|
|
41
|
+
return _outcome("n/a", "the action carries no refundable; whether it can be undone could not be checked",
|
|
42
|
+
not_applicable_evidence(_CHECK_ID, in_scope=True, missing_field="refundable"))
|
|
43
|
+
evidence = RefundabilityEvidence(refundable=action.refundable, rail=action.rail)
|
|
44
|
+
if action.refundable:
|
|
45
|
+
return _outcome("pass", "the payment is refundable", evidence)
|
|
46
|
+
return _outcome("fail", "the payment is not refundable", evidence)
|