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,266 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Builders for the four hold-lifecycle record types: ``hold.reserve``,
|
|
3
|
+
``hold.release``, ``hold.expire``, ``hold.reconcile``.
|
|
4
|
+
|
|
5
|
+
A reservation is a capsule, not side-band state (design premise shared by
|
|
6
|
+
capsule-emit #51/#52/#53): reserve/release/expire/reconcile are signed,
|
|
7
|
+
appended records built the same way ``guards/capsule.py``'s
|
|
8
|
+
``build_decision_capsule`` and ``events/capsule.py``'s ``build_event_capsule``
|
|
9
|
+
build every other capsule this codebase produces -- same ``Capsule``/``Disposition``/``Chain``/
|
|
10
|
+
``AssuranceBlock`` primitives, same digest-then-sign-then-reseal sequence, so
|
|
11
|
+
these records are ordinary, independently verifiable ledger capsules with no
|
|
12
|
+
special-cased verification path.
|
|
13
|
+
|
|
14
|
+
Record-type identity lives in the ``action_id`` verb (``hold.reserve/<uuid>``
|
|
15
|
+
etc.), the same place every other capsule in this codebase carries its verb
|
|
16
|
+
(``guards/checks/dedupe.py``'s ``_capsule_verb``) -- not a new top-level
|
|
17
|
+
field, and not the spec's closed ``action_type`` enum (-02 §5.1 restricts
|
|
18
|
+
``action_type`` to ``fyi``/``decide``; these are ``decide``, since a
|
|
19
|
+
reservation is a consequential decision about exposure).
|
|
20
|
+
|
|
21
|
+
Every numeric field here is integer minor units, MUST-FAIL on float --
|
|
22
|
+
mirrors ``folds/reducers.py``'s own discipline for the same reason: these
|
|
23
|
+
values flow into a fold's ``sum`` reducer (``hold.active_exposure``,
|
|
24
|
+
``folds/catalog_defs/hold.active_exposure.yaml``), which cannot reproduce a
|
|
25
|
+
float byte-exactly across implementations.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from agent_action_capsule import (
|
|
32
|
+
DEFAULT_FORMAT_VERSION,
|
|
33
|
+
AssuranceBlock,
|
|
34
|
+
Capsule,
|
|
35
|
+
Chain,
|
|
36
|
+
Disposition,
|
|
37
|
+
compute_capsule_id,
|
|
38
|
+
json_digest,
|
|
39
|
+
)
|
|
40
|
+
from agent_action_capsule.canonical import CANONICALIZATION_JCS
|
|
41
|
+
|
|
42
|
+
from ..guards.action import Action
|
|
43
|
+
from ..guards.signing import Signer
|
|
44
|
+
from .errors import FLOAT_IN_HOLD_AMOUNT, NON_INTEGER_HOLD_AMOUNT, HoldError
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"build_hold_reserve_capsule",
|
|
48
|
+
"build_hold_release_capsule",
|
|
49
|
+
"build_hold_expire_capsule",
|
|
50
|
+
"build_hold_reconcile_capsule",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
_SPEC_VERSION = "draft-mih-scitt-agent-action-capsule-02"
|
|
54
|
+
_FORMAT_VERSION = DEFAULT_FORMAT_VERSION
|
|
55
|
+
|
|
56
|
+
# The only registered chain.relation this task uses: "supersedes" for a
|
|
57
|
+
# terminal transition that closes/replaces the reserve's open state
|
|
58
|
+
# (release, expiry, a successful reconcile), matching the existing
|
|
59
|
+
# EUR150k-bridge precedent (`guards/capsule.py`'s own docstring; the
|
|
60
|
+
# registry's own definition: "resolution, expiry, escalation close/replace
|
|
61
|
+
# the parent's open state").
|
|
62
|
+
SUPERSEDES = "supersedes"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def check_integer_amount(value: Any, field: str) -> int:
|
|
66
|
+
"""Amounts are integer minor units -- floats MUST-FAIL (same discipline
|
|
67
|
+
as ``folds/reducers.py._check_integer``, applied at record-build time so
|
|
68
|
+
a bad amount fails loudly here with a named reason, not deep inside JCS
|
|
69
|
+
canonicalization when the capsule is later digested)."""
|
|
70
|
+
if isinstance(value, bool):
|
|
71
|
+
raise HoldError(NON_INTEGER_HOLD_AMOUNT, f"{field!r} is a bool, not an integer amount")
|
|
72
|
+
if isinstance(value, float):
|
|
73
|
+
raise HoldError(FLOAT_IN_HOLD_AMOUNT, f"{field!r} carries a float ({value!r}); amounts MUST be integer minor units")
|
|
74
|
+
if not isinstance(value, int):
|
|
75
|
+
raise HoldError(NON_INTEGER_HOLD_AMOUNT, f"{field!r} is {type(value).__name__}, not an integer")
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _hold_action(action: Action, verb: str) -> Action:
|
|
80
|
+
"""A fresh ``Action`` carrying this hold record's own verb/action_id,
|
|
81
|
+
otherwise identical to the action this hold is evaluated for."""
|
|
82
|
+
return Action(
|
|
83
|
+
verb=verb,
|
|
84
|
+
operator=action.operator,
|
|
85
|
+
developer=action.developer,
|
|
86
|
+
action_class=action.action_class,
|
|
87
|
+
action_type="decide",
|
|
88
|
+
timestamp=action.resolved_timestamp(),
|
|
89
|
+
amount_minor=action.amount_minor,
|
|
90
|
+
currency=action.currency,
|
|
91
|
+
target=action.target,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _seal(body: dict, signer: Signer) -> dict:
|
|
96
|
+
"""Sign the pre-signature canonical body, commit the signature into the
|
|
97
|
+
body, then compute capsule_id over everything (identical sequence to
|
|
98
|
+
``guards/capsule.py``'s two builders -- see that module's own comment on
|
|
99
|
+
why the signature is committed pre-digest)."""
|
|
100
|
+
presig_digest = json_digest(body)
|
|
101
|
+
body["asg_signature"] = {
|
|
102
|
+
"key_id": signer.key_id,
|
|
103
|
+
"alg": signer.algorithm,
|
|
104
|
+
"sig": signer.sign(presig_digest),
|
|
105
|
+
}
|
|
106
|
+
capsule_id = compute_capsule_id(body)
|
|
107
|
+
sealed = {"spec_version": body["spec_version"], "format_version": body["format_version"], "capsule_id": capsule_id}
|
|
108
|
+
for k, v in body.items():
|
|
109
|
+
if k not in sealed:
|
|
110
|
+
sealed[k] = v
|
|
111
|
+
return sealed
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _build(*, hold_action: Action, chain: Chain | None, asg_payload: dict, signer: Signer) -> dict:
|
|
115
|
+
disposition = Disposition(decision="accept", approver="policy", human_disposed=False, verdict_class=None)
|
|
116
|
+
capsule_obj = Capsule(
|
|
117
|
+
spec_version=_SPEC_VERSION,
|
|
118
|
+
format_version=_FORMAT_VERSION,
|
|
119
|
+
canonicalization_id=CANONICALIZATION_JCS,
|
|
120
|
+
action_id=hold_action.resolved_action_id(),
|
|
121
|
+
action_type="decide",
|
|
122
|
+
operator=hold_action.operator,
|
|
123
|
+
developer=hold_action.developer,
|
|
124
|
+
timestamp=hold_action.resolved_timestamp(),
|
|
125
|
+
assurance=AssuranceBlock(
|
|
126
|
+
attestation_mode="self_attested",
|
|
127
|
+
effect_mode="not_applicable",
|
|
128
|
+
ledger_mode="chained" if chain is not None else "standalone",
|
|
129
|
+
),
|
|
130
|
+
disposition=disposition,
|
|
131
|
+
chain=chain,
|
|
132
|
+
)
|
|
133
|
+
body = capsule_obj.to_dict()
|
|
134
|
+
body["asg_payload"] = asg_payload
|
|
135
|
+
return _seal(body, signer)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def build_hold_reserve_capsule(
|
|
139
|
+
*,
|
|
140
|
+
action: Action,
|
|
141
|
+
reserved_amount_minor: int,
|
|
142
|
+
fold_id: str,
|
|
143
|
+
fold_digest: str,
|
|
144
|
+
fold_envelope: dict,
|
|
145
|
+
checkpoint: dict,
|
|
146
|
+
signer: Signer,
|
|
147
|
+
manifest_digest: str | None = None,
|
|
148
|
+
) -> dict:
|
|
149
|
+
"""Reserve-at-seal (#51.1): cites the evaluation's own fold envelope
|
|
150
|
+
(fold digest, record range, checkpoint) and the reserved amount. No
|
|
151
|
+
``chain`` -- a fresh reservation opens a new hold, standalone; release/
|
|
152
|
+
expiry/reconcile are what chain back to *this* capsule's id."""
|
|
153
|
+
check_integer_amount(reserved_amount_minor, "reserved_amount_minor")
|
|
154
|
+
hold_action = _hold_action(action, "hold.reserve")
|
|
155
|
+
asg_payload: dict[str, Any] = {
|
|
156
|
+
"amount_minor": reserved_amount_minor,
|
|
157
|
+
"reserved_amount_minor": reserved_amount_minor,
|
|
158
|
+
"hold_scope": {"fold_id": fold_id, "fold_digest": fold_digest, "subject": action.developer},
|
|
159
|
+
"fold_envelope": fold_envelope,
|
|
160
|
+
"checkpoint": checkpoint,
|
|
161
|
+
}
|
|
162
|
+
if action.currency is not None:
|
|
163
|
+
asg_payload["currency"] = action.currency
|
|
164
|
+
if action.target is not None:
|
|
165
|
+
asg_payload["target"] = action.target
|
|
166
|
+
if manifest_digest is not None:
|
|
167
|
+
asg_payload["manifest_digest"] = manifest_digest
|
|
168
|
+
return _build(hold_action=hold_action, chain=None, asg_payload=asg_payload, signer=signer)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def build_hold_release_capsule(
|
|
172
|
+
*,
|
|
173
|
+
action: Action,
|
|
174
|
+
reserve_capsule_id: str,
|
|
175
|
+
reserved_amount_minor: int,
|
|
176
|
+
signer: Signer,
|
|
177
|
+
reason: str | None = None,
|
|
178
|
+
) -> dict:
|
|
179
|
+
"""Voluntary cancellation of a still-active hold. Terminal: the reserve's
|
|
180
|
+
exposure fully unwinds (``amount_minor`` is the negative of the reserved
|
|
181
|
+
amount, so the ``hold.active_exposure`` fold nets back to zero for this
|
|
182
|
+
hold)."""
|
|
183
|
+
check_integer_amount(reserved_amount_minor, "reserved_amount_minor")
|
|
184
|
+
hold_action = _hold_action(action, "hold.release")
|
|
185
|
+
chain = Chain(parent_capsule_id=reserve_capsule_id, relation=SUPERSEDES)
|
|
186
|
+
asg_payload: dict[str, Any] = {
|
|
187
|
+
"amount_minor": -reserved_amount_minor,
|
|
188
|
+
"released_amount_minor": reserved_amount_minor,
|
|
189
|
+
}
|
|
190
|
+
if reason is not None:
|
|
191
|
+
asg_payload["reason"] = reason
|
|
192
|
+
return _build(hold_action=hold_action, chain=chain, asg_payload=asg_payload, signer=signer)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def build_hold_expire_capsule(
|
|
196
|
+
*,
|
|
197
|
+
action: Action,
|
|
198
|
+
reserve_capsule_id: str,
|
|
199
|
+
reserved_amount_minor: int,
|
|
200
|
+
signer: Signer,
|
|
201
|
+
reason: str | None = None,
|
|
202
|
+
) -> dict:
|
|
203
|
+
"""Expiry (#52.1): TERMINAL for this hold -- after this capsule exists,
|
|
204
|
+
nothing may dispatch citing the original reservation. Same net-to-zero
|
|
205
|
+
exposure unwind as release; the two are distinguished by verb (a caller
|
|
206
|
+
choosing to cancel vs. a TTL/policy elapsing), not by any different
|
|
207
|
+
fold-visible effect."""
|
|
208
|
+
check_integer_amount(reserved_amount_minor, "reserved_amount_minor")
|
|
209
|
+
hold_action = _hold_action(action, "hold.expire")
|
|
210
|
+
chain = Chain(parent_capsule_id=reserve_capsule_id, relation=SUPERSEDES)
|
|
211
|
+
asg_payload: dict[str, Any] = {
|
|
212
|
+
"amount_minor": -reserved_amount_minor,
|
|
213
|
+
"expired_amount_minor": reserved_amount_minor,
|
|
214
|
+
}
|
|
215
|
+
if reason is not None:
|
|
216
|
+
asg_payload["reason"] = reason
|
|
217
|
+
return _build(hold_action=hold_action, chain=chain, asg_payload=asg_payload, signer=signer)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def build_hold_reconcile_capsule(
|
|
221
|
+
*,
|
|
222
|
+
action: Action,
|
|
223
|
+
reserve_capsule_id: str,
|
|
224
|
+
execution_capsule_id: str | None,
|
|
225
|
+
reserved_amount_minor: int,
|
|
226
|
+
executed_amount_minor: int,
|
|
227
|
+
tolerance_minor: int,
|
|
228
|
+
signer: Signer,
|
|
229
|
+
manifest_digest: str | None = None,
|
|
230
|
+
) -> dict:
|
|
231
|
+
"""Planned vs. executed (#53.1): reserve at planned amount, convert at
|
|
232
|
+
executed amount, the delta sealed as this record -- chained to the
|
|
233
|
+
reserve capsule via ``chain`` (the schema's single-parent link) and to
|
|
234
|
+
the execution capsule via the ``asg_payload.execution_capsule_id``
|
|
235
|
+
citation (a plain payload reference, not a registry relation -- the
|
|
236
|
+
schema has only one ``chain`` slot per capsule).
|
|
237
|
+
|
|
238
|
+
``amount_minor`` is the *delta* (``executed - reserved``): summed against
|
|
239
|
+
the reserve's own ``+reserved_amount_minor`` contribution, the
|
|
240
|
+
``hold.active_exposure`` fold nets to exactly ``executed_amount_minor``
|
|
241
|
+
once this record lands -- "executed once reconciled" (#53.4) falls out
|
|
242
|
+
of the fold algebra, not a special case in the fold definition.
|
|
243
|
+
|
|
244
|
+
Only called for an in-tolerance conversion; an over-tolerance conversion
|
|
245
|
+
routes through the existing cap-exceeded ALLOW/DENY/ESCALATE vocabulary
|
|
246
|
+
instead (``holds/engine.py``) and this capsule is never built for it --
|
|
247
|
+
"never silently adjusts the aggregate" (#53.3).
|
|
248
|
+
"""
|
|
249
|
+
check_integer_amount(reserved_amount_minor, "reserved_amount_minor")
|
|
250
|
+
check_integer_amount(executed_amount_minor, "executed_amount_minor")
|
|
251
|
+
check_integer_amount(tolerance_minor, "tolerance_minor")
|
|
252
|
+
delta_minor = executed_amount_minor - reserved_amount_minor
|
|
253
|
+
hold_action = _hold_action(action, "hold.reconcile")
|
|
254
|
+
chain = Chain(parent_capsule_id=reserve_capsule_id, relation=SUPERSEDES)
|
|
255
|
+
asg_payload: dict[str, Any] = {
|
|
256
|
+
"amount_minor": delta_minor,
|
|
257
|
+
"reserved_amount_minor": reserved_amount_minor,
|
|
258
|
+
"executed_amount_minor": executed_amount_minor,
|
|
259
|
+
"delta_minor": delta_minor,
|
|
260
|
+
"tolerance_minor": tolerance_minor,
|
|
261
|
+
}
|
|
262
|
+
if execution_capsule_id is not None:
|
|
263
|
+
asg_payload["execution_capsule_id"] = execution_capsule_id
|
|
264
|
+
if manifest_digest is not None:
|
|
265
|
+
asg_payload["manifest_digest"] = manifest_digest
|
|
266
|
+
return _build(hold_action=hold_action, chain=chain, asg_payload=asg_payload, signer=signer)
|