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,281 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""A policy profile: one user's own values for the parameters a pack exposes,
|
|
3
|
+
kept as its own artifact so the pack and its wickets stay byte-identical for
|
|
4
|
+
every user.
|
|
5
|
+
|
|
6
|
+
Before this existed, accepting a limit (``packs/enforce.py``'s
|
|
7
|
+
``accept_thresholds``) merged the value into the ``caps`` wicket's config,
|
|
8
|
+
so 25.00 -> 40.00 moved the wicket's digest, then the pack's, then the
|
|
9
|
+
manifest's. That made "what was in force" provable, but it also meant two
|
|
10
|
+
households with different limits could not run the same wicket. The checks
|
|
11
|
+
already take their limits as arguments (``guards/checks/caps.py``), so the
|
|
12
|
+
fix is where the value is stored, not how it is evaluated: the wicket carries
|
|
13
|
+
the pack's default, a profile carries the user's value, and the manifest pins
|
|
14
|
+
the profile by digest (``Manifest.profile_digest``) the same way it pins
|
|
15
|
+
every fold and wicket. "What was in force" stays provable because the
|
|
16
|
+
manifest digest moves when the profile does, and the activation record
|
|
17
|
+
carries the profile's values (``policy/activation.py``).
|
|
18
|
+
|
|
19
|
+
Shape (``policy-profile/v0``)::
|
|
20
|
+
|
|
21
|
+
format: policy-profile/v0
|
|
22
|
+
packs:
|
|
23
|
+
- pack: asg/everyday # publisher/name -- no version
|
|
24
|
+
parameters:
|
|
25
|
+
caps: # the check the values configure
|
|
26
|
+
per_action_minor: {money.purchase: 4000}
|
|
27
|
+
caps_minor: {money.purchase: 20000}
|
|
28
|
+
counterparty_list: # a list, not class values
|
|
29
|
+
mode: deny # required: deny | allow
|
|
30
|
+
entries: # each names its kind and form
|
|
31
|
+
- {kind: payee, fp_alg: hmac-sha256-deal-key, value: <hex>}
|
|
32
|
+
- {kind: target, value: shop/example}
|
|
33
|
+
disposition: ask # optional: deny | ask
|
|
34
|
+
|
|
35
|
+
``pack`` names a pack without its version so a profile can outlive a pack
|
|
36
|
+
upgrade; carrying values across an upgrade is a later step, and today a
|
|
37
|
+
value the installed pack no longer defines fails closed at resolve time.
|
|
38
|
+
The profile names no owner: two households that chose the same values have
|
|
39
|
+
the same profile digest.
|
|
40
|
+
|
|
41
|
+
Which parameters a profile may set is a closed table (``CONFIGURABLE``), and
|
|
42
|
+
a value may only replace a default the pack's wicket already declares: a
|
|
43
|
+
profile never adds a class the pack does not limit. Anything else is refused
|
|
44
|
+
(``resolve.py``), never ignored.
|
|
45
|
+
|
|
46
|
+
``counterparty_list`` is the one check whose values are not per-class
|
|
47
|
+
amounts: its ``entries`` replace the wicket's (empty) list whole. A profile
|
|
48
|
+
that sets it must name ``mode``, so a list is never read as deny when the
|
|
49
|
+
user meant allow. Each entry names its ``kind`` (``target``, or a
|
|
50
|
+
fingerprint kind with its ``fp_alg``; see ``guards/checks/
|
|
51
|
+
counterparty_list.py``). Entries are stored in a canonical order, so the
|
|
52
|
+
profile digest does not depend on the order the user typed them.
|
|
53
|
+
"""
|
|
54
|
+
from __future__ import annotations
|
|
55
|
+
|
|
56
|
+
import json
|
|
57
|
+
import re
|
|
58
|
+
from collections.abc import Iterable
|
|
59
|
+
from dataclasses import dataclass, field
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
from typing import Any, TypedDict
|
|
62
|
+
|
|
63
|
+
from agent_action_capsule.canonical import json_digest
|
|
64
|
+
|
|
65
|
+
from ..guards.checks.counterparty_list import (
|
|
66
|
+
CLEAR_KINDS,
|
|
67
|
+
DISPOSITIONS,
|
|
68
|
+
FINGERPRINT_KINDS,
|
|
69
|
+
MODES,
|
|
70
|
+
ListEntry,
|
|
71
|
+
sorted_entries,
|
|
72
|
+
)
|
|
73
|
+
from .errors import MALFORMED_PROFILE, PolicyManifestError
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
"CONFIGURABLE",
|
|
77
|
+
"LIST_CHECK",
|
|
78
|
+
"LIST_KEYS",
|
|
79
|
+
"PROFILE_FORMAT",
|
|
80
|
+
"PackParameters",
|
|
81
|
+
"PolicyProfile",
|
|
82
|
+
"load_profile_file",
|
|
83
|
+
"pack_name",
|
|
84
|
+
"parse_profile",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
PROFILE_FORMAT = "policy-profile/v0"
|
|
88
|
+
|
|
89
|
+
# check -> the config keys a profile may set on a wicket running that check.
|
|
90
|
+
# Every value is a mapping of action class -> non-negative integer minor units.
|
|
91
|
+
CONFIGURABLE: dict[str, frozenset[str]] = {
|
|
92
|
+
"caps": frozenset({"caps_minor", "per_action_minor"}),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# The one check whose profile values are not per-class amounts, and the keys
|
|
96
|
+
# a profile may set on it. Kept out of ``CONFIGURABLE``, whose every key is a
|
|
97
|
+
# per-class limit to its readers.
|
|
98
|
+
LIST_CHECK = "counterparty_list"
|
|
99
|
+
LIST_KEYS = frozenset({"mode", "entries", "disposition"})
|
|
100
|
+
|
|
101
|
+
# publisher/name: a pack_id (``publisher/name/semver``) without its version.
|
|
102
|
+
PACK_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9_.-]*/[a-z0-9][a-z0-9-]*$")
|
|
103
|
+
|
|
104
|
+
# Largest integer every JSON implementation reads exactly.
|
|
105
|
+
_MAX_SAFE_INT = 2**53 - 1
|
|
106
|
+
|
|
107
|
+
ClassValues = dict[str, int]
|
|
108
|
+
# A ``caps`` key carries ClassValues; a ``counterparty_list`` key carries its
|
|
109
|
+
# mode or disposition (str) or its entries (list[ListEntry]).
|
|
110
|
+
ParameterValue = ClassValues | str | list[ListEntry]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class PackParametersDict(TypedDict):
|
|
114
|
+
pack: str
|
|
115
|
+
parameters: dict[str, dict[str, ParameterValue]]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class PolicyProfileDict(TypedDict):
|
|
119
|
+
format: str
|
|
120
|
+
packs: list[PackParametersDict]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class PackParameters:
|
|
125
|
+
"""The values one profile sets for one pack: check -> key -> class -> value."""
|
|
126
|
+
|
|
127
|
+
pack: str
|
|
128
|
+
parameters: dict[str, dict[str, ParameterValue]] = field(default_factory=dict)
|
|
129
|
+
|
|
130
|
+
def canonical_dict(self) -> PackParametersDict:
|
|
131
|
+
return {"pack": self.pack, "parameters": self.parameters}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class PolicyProfile:
|
|
136
|
+
packs: tuple[PackParameters, ...] = ()
|
|
137
|
+
|
|
138
|
+
def canonical_dict(self) -> PolicyProfileDict:
|
|
139
|
+
"""The JCS-canonicalizable form. Mapping order carries no meaning
|
|
140
|
+
(JCS sorts keys); ``packs`` order does, like every list a manifest
|
|
141
|
+
digests."""
|
|
142
|
+
return {"format": PROFILE_FORMAT, "packs": [p.canonical_dict() for p in self.packs]}
|
|
143
|
+
|
|
144
|
+
def profile_digest(self) -> str:
|
|
145
|
+
"""SHA-256 over the JCS bytes of ``canonical_dict()`` -- the digest
|
|
146
|
+
``Manifest.profile_digest`` pins."""
|
|
147
|
+
return json_digest(self.canonical_dict())
|
|
148
|
+
|
|
149
|
+
def for_pack(self, pack_id: str) -> PackParameters | None:
|
|
150
|
+
name = pack_name(pack_id)
|
|
151
|
+
return next((p for p in self.packs if p.pack == name), None)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def pack_name(pack_id: str) -> str:
|
|
155
|
+
"""``asg/everyday/0.1.0`` -> ``asg/everyday``."""
|
|
156
|
+
return pack_id.rsplit("/", 1)[0]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _refuse(message: str) -> PolicyManifestError:
|
|
160
|
+
return PolicyManifestError(MALFORMED_PROFILE, message)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _keys_exactly(keys: Iterable[object], expected: set[str], context: str) -> None:
|
|
164
|
+
actual = set(keys)
|
|
165
|
+
if actual != expected:
|
|
166
|
+
raise _refuse(f"{context} must have exactly the keys {sorted(expected)}, got {sorted(actual, key=str)}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _parse_class_values(raw: Any, context: str) -> ClassValues:
|
|
170
|
+
if not isinstance(raw, dict):
|
|
171
|
+
raise _refuse(f"{context} must be a mapping of action class to an integer")
|
|
172
|
+
out: ClassValues = {}
|
|
173
|
+
for action_class, value in raw.items():
|
|
174
|
+
if not isinstance(action_class, str) or not action_class:
|
|
175
|
+
raise _refuse(f"{context} has a key that is not a non-empty string: {action_class!r}")
|
|
176
|
+
# bool is an int subclass; True is not a limit.
|
|
177
|
+
if type(value) is not int or not 0 <= value <= _MAX_SAFE_INT:
|
|
178
|
+
raise _refuse(f"{context}[{action_class!r}] must be an integer from 0 to {_MAX_SAFE_INT}, got {value!r}")
|
|
179
|
+
out[action_class] = value
|
|
180
|
+
return out
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _nonempty_str(value: object) -> bool:
|
|
184
|
+
return isinstance(value, str) and bool(value)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _parse_list_entry(raw: Any, context: str) -> ListEntry:
|
|
188
|
+
"""``{kind: target, value}`` or ``{kind: <fingerprint kind>, fp_alg,
|
|
189
|
+
value}``. A bare string names no kind, and a name is never a kind."""
|
|
190
|
+
kind = raw.get("kind") if isinstance(raw, dict) else None
|
|
191
|
+
if kind in CLEAR_KINDS:
|
|
192
|
+
_keys_exactly(raw.keys(), {"kind", "value"}, f"{context} entry of kind {kind!r}")
|
|
193
|
+
elif kind in FINGERPRINT_KINDS:
|
|
194
|
+
_keys_exactly(raw.keys(), {"kind", "fp_alg", "value"}, f"{context} entry of kind {kind!r}")
|
|
195
|
+
if not _nonempty_str(raw["fp_alg"]):
|
|
196
|
+
raise _refuse(f"{context} entry has an fp_alg that is not a non-empty string: {raw['fp_alg']!r}")
|
|
197
|
+
else:
|
|
198
|
+
kinds = sorted(CLEAR_KINDS | FINGERPRINT_KINDS)
|
|
199
|
+
raise _refuse(f"{context} entries must be mappings whose kind is one of {kinds}, got {raw!r}")
|
|
200
|
+
if not _nonempty_str(raw["value"]):
|
|
201
|
+
raise _refuse(f"{context} entry has a value that is not a non-empty string: {raw['value']!r}")
|
|
202
|
+
entry: ListEntry = {"kind": kind, "value": raw["value"]}
|
|
203
|
+
if kind in FINGERPRINT_KINDS:
|
|
204
|
+
entry["fp_alg"] = raw["fp_alg"]
|
|
205
|
+
return entry
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _parse_list_parameters(raw: Any, context: str) -> dict[str, ParameterValue]:
|
|
209
|
+
keys = set(raw)
|
|
210
|
+
if not {"mode", "entries"} <= keys <= LIST_KEYS:
|
|
211
|
+
raise _refuse(
|
|
212
|
+
f"{context} must set 'mode' and 'entries' and may set 'disposition', got {sorted(keys, key=str)}"
|
|
213
|
+
)
|
|
214
|
+
if raw["mode"] not in MODES:
|
|
215
|
+
raise _refuse(f"{context}['mode'] must be one of {sorted(MODES)}, got {raw['mode']!r}")
|
|
216
|
+
if not isinstance(raw["entries"], list):
|
|
217
|
+
raise _refuse(f"{context}['entries'] must be a list, got {raw['entries']!r}")
|
|
218
|
+
entries = [_parse_list_entry(e, f"{context}['entries']") for e in raw["entries"]]
|
|
219
|
+
if len({json_digest(e) for e in entries}) != len(entries):
|
|
220
|
+
raise _refuse(f"{context}['entries'] lists a counterparty more than once")
|
|
221
|
+
out: dict[str, ParameterValue] = {"mode": raw["mode"], "entries": sorted_entries(entries)}
|
|
222
|
+
if "disposition" in raw:
|
|
223
|
+
if raw["disposition"] not in DISPOSITIONS:
|
|
224
|
+
raise _refuse(
|
|
225
|
+
f"{context}['disposition'] must be one of {sorted(DISPOSITIONS)}, got {raw['disposition']!r}"
|
|
226
|
+
)
|
|
227
|
+
out["disposition"] = raw["disposition"]
|
|
228
|
+
return out
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _parse_pack_entry(raw: Any) -> PackParameters:
|
|
232
|
+
if not isinstance(raw, dict):
|
|
233
|
+
raise _refuse(f"each packs entry must be a mapping: {raw!r}")
|
|
234
|
+
_keys_exactly(raw.keys(), {"pack", "parameters"}, "packs entry")
|
|
235
|
+
name = raw["pack"]
|
|
236
|
+
if not isinstance(name, str) or not PACK_NAME_RE.match(name):
|
|
237
|
+
raise _refuse(f"packs[].pack must be '<publisher>/<name>' with no version, got {name!r}")
|
|
238
|
+
raw_params = raw["parameters"]
|
|
239
|
+
if not isinstance(raw_params, dict):
|
|
240
|
+
raise _refuse(f"packs[{name!r}].parameters must be a mapping")
|
|
241
|
+
parameters: dict[str, dict[str, ParameterValue]] = {}
|
|
242
|
+
for check, keys in raw_params.items():
|
|
243
|
+
if not isinstance(check, str) or not isinstance(keys, dict):
|
|
244
|
+
raise _refuse(f"packs[{name!r}].parameters[{check!r}] must be a mapping of config key to values")
|
|
245
|
+
if check == LIST_CHECK:
|
|
246
|
+
parameters[check] = _parse_list_parameters(keys, f"packs[{name!r}].parameters[{check!r}]")
|
|
247
|
+
continue
|
|
248
|
+
parameters[check] = {
|
|
249
|
+
key: _parse_class_values(values, f"packs[{name!r}].parameters[{check!r}][{key!r}]")
|
|
250
|
+
for key, values in keys.items()
|
|
251
|
+
}
|
|
252
|
+
return PackParameters(pack=name, parameters=parameters)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def parse_profile(data: Any) -> PolicyProfile:
|
|
256
|
+
"""Validate a plain dict (as loaded from JSON or YAML) into a ``PolicyProfile``.
|
|
257
|
+
|
|
258
|
+
Checks shape only. Whether each value names a parameter the installed
|
|
259
|
+
pack defines is checked against the resolved wickets (``resolve.py``)."""
|
|
260
|
+
if not isinstance(data, dict):
|
|
261
|
+
raise _refuse("profile must be a mapping")
|
|
262
|
+
_keys_exactly(data.keys(), {"format", "packs"}, "profile")
|
|
263
|
+
if data["format"] != PROFILE_FORMAT:
|
|
264
|
+
raise _refuse(f"format must be {PROFILE_FORMAT!r}, got {data['format']!r}")
|
|
265
|
+
if not isinstance(data["packs"], list):
|
|
266
|
+
raise _refuse("packs must be a list")
|
|
267
|
+
packs = tuple(_parse_pack_entry(entry) for entry in data["packs"])
|
|
268
|
+
names = [p.pack for p in packs]
|
|
269
|
+
duplicated = sorted({n for n in names if names.count(n) > 1})
|
|
270
|
+
if duplicated:
|
|
271
|
+
raise _refuse(f"pack {duplicated[0]!r} appears more than once")
|
|
272
|
+
return PolicyProfile(packs=packs)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def load_profile_file(path: str | Path) -> PolicyProfile:
|
|
276
|
+
path = Path(path)
|
|
277
|
+
try:
|
|
278
|
+
data = json.loads(path.read_text())
|
|
279
|
+
except json.JSONDecodeError as exc:
|
|
280
|
+
raise _refuse(f"{path} is not valid JSON: {exc}") from exc
|
|
281
|
+
return parse_profile(data)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Resolve a parsed ``Manifest`` against the real fold/wicket catalogs it
|
|
3
|
+
cites, cross-checking every pinned digest.
|
|
4
|
+
|
|
5
|
+
A manifest that fails to resolve (a cited fold_id/wicket_id no longer
|
|
6
|
+
exists, its current catalog digest no longer matches what the manifest
|
|
7
|
+
pinned, it names an evaluation ``engine`` this build doesn't recognize, or
|
|
8
|
+
the policy profile supplied is not the one it pins)
|
|
9
|
+
is not "real, loadable" -- every caller in this task (``manifest show``,
|
|
10
|
+
``manifest activate``, ``guard dry-run``) treats a resolve failure as
|
|
11
|
+
fail-closed, never a best-effort partial load.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, replace
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from ..folds.catalog import Catalog as FoldCatalog
|
|
19
|
+
from ..folds.definition import FoldDefinition
|
|
20
|
+
from ..guards.plan import PlanDefinition, parse_plan_definition
|
|
21
|
+
from ..guards.wickets.catalog import Catalog as WicketCatalog
|
|
22
|
+
from ..guards.wickets.definition import WicketDefinition
|
|
23
|
+
from .errors import (
|
|
24
|
+
FOLD_DIGEST_DRIFT,
|
|
25
|
+
PROFILE_DIGEST_DRIFT,
|
|
26
|
+
PROFILE_MISSING,
|
|
27
|
+
PROFILE_UNKNOWN_PACK,
|
|
28
|
+
PROFILE_UNKNOWN_PARAMETER,
|
|
29
|
+
PROFILE_UNPINNED,
|
|
30
|
+
UNKNOWN_ENGINE,
|
|
31
|
+
UNKNOWN_FOLD_ID,
|
|
32
|
+
UNKNOWN_WICKET_ID,
|
|
33
|
+
WICKET_DIGEST_DRIFT,
|
|
34
|
+
PolicyManifestError,
|
|
35
|
+
)
|
|
36
|
+
from .manifest import Manifest
|
|
37
|
+
from .profile import CONFIGURABLE, LIST_CHECK, LIST_KEYS, PolicyProfile, pack_name
|
|
38
|
+
|
|
39
|
+
__all__ = ["SUPPORTED_FOLD_ENGINES", "SUPPORTED_WICKET_ENGINES", "ResolvedManifest", "resolve_manifest"]
|
|
40
|
+
|
|
41
|
+
# The only evaluation engines this build knows how to run a fold/wicket
|
|
42
|
+
# entry through -- this repo's own built-in evaluator, one engine per entry
|
|
43
|
+
# kind. A manifest naming anything else is rejected fail-closed rather than
|
|
44
|
+
# silently ignored (see the ``engine`` design note in ``manifest.py``).
|
|
45
|
+
SUPPORTED_FOLD_ENGINES = frozenset({"fold/1"})
|
|
46
|
+
SUPPORTED_WICKET_ENGINES = frozenset({"wicket/1"})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class ResolvedManifest:
|
|
51
|
+
"""A manifest plus the real, digest-verified definitions it cites."""
|
|
52
|
+
|
|
53
|
+
manifest: Manifest
|
|
54
|
+
manifest_digest: str
|
|
55
|
+
folds: dict[str, FoldDefinition]
|
|
56
|
+
wickets: dict[str, WicketDefinition]
|
|
57
|
+
# The profile the manifest pins, verified against its digest and against
|
|
58
|
+
# the wickets above. ``None`` when the manifest pins none.
|
|
59
|
+
profile: PolicyProfile | None = None
|
|
60
|
+
|
|
61
|
+
def wicket_config(self, check: str) -> dict:
|
|
62
|
+
"""The declarative ``config`` of the (first) resolved wicket
|
|
63
|
+
configuring the given check, or ``{}`` if none is active. These are
|
|
64
|
+
the pack's defaults; ``effective_values`` overlays the profile."""
|
|
65
|
+
for wicket in self.wickets.values():
|
|
66
|
+
if wicket.check == check:
|
|
67
|
+
return wicket.config
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
def effective_values(self, check: str, key: str) -> dict[str, int]:
|
|
71
|
+
"""The wicket's default ``config[key]`` with every value the profile
|
|
72
|
+
sets for that check laid over it -- what the check actually enforces."""
|
|
73
|
+
out = dict(self.wicket_config(check).get(key) or {})
|
|
74
|
+
if self.profile is not None:
|
|
75
|
+
for entry in self.profile.packs:
|
|
76
|
+
out.update(entry.parameters.get(check, {}).get(key, {}))
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
def caps_minor(self) -> dict[str, int]:
|
|
80
|
+
return self.effective_values("caps", "caps_minor")
|
|
81
|
+
|
|
82
|
+
def per_action_minor(self) -> dict[str, int]:
|
|
83
|
+
return self.effective_values("caps", "per_action_minor")
|
|
84
|
+
|
|
85
|
+
def per_action_reads(self) -> str | None:
|
|
86
|
+
return self.wicket_config("caps").get("per_action_reads")
|
|
87
|
+
|
|
88
|
+
def dedupe_window_days(self) -> int | None:
|
|
89
|
+
return self.wicket_config("dedupe").get("window_days")
|
|
90
|
+
|
|
91
|
+
def caps_fold(self) -> FoldDefinition | None:
|
|
92
|
+
fold_id = self.wicket_config("caps").get("fold_id")
|
|
93
|
+
return self.folds.get(fold_id) if fold_id else None
|
|
94
|
+
|
|
95
|
+
def configured_wickets(self, checks: frozenset[str]) -> tuple[WicketDefinition, ...]:
|
|
96
|
+
"""Every resolved wicket configuring one of ``checks``, in manifest
|
|
97
|
+
order. A ``counterparty_list`` wicket comes back with the profile's
|
|
98
|
+
list values laid over its config: that is what the check runs with,
|
|
99
|
+
while the manifest still pins the wicket's own digest."""
|
|
100
|
+
return tuple(self._with_profile_list(w) for w in self.wickets.values() if w.check in checks)
|
|
101
|
+
|
|
102
|
+
def _with_profile_list(self, wicket: WicketDefinition) -> WicketDefinition:
|
|
103
|
+
if wicket.check != LIST_CHECK or self.profile is None:
|
|
104
|
+
return wicket
|
|
105
|
+
config = dict(wicket.config)
|
|
106
|
+
for entry in self.profile.packs:
|
|
107
|
+
config.update(entry.parameters.get(LIST_CHECK, {}))
|
|
108
|
+
return replace(wicket, config=config)
|
|
109
|
+
|
|
110
|
+
def plan(self) -> PlanDefinition | None:
|
|
111
|
+
"""The compiled plan quoted directly in the resolved
|
|
112
|
+
``plan_containment`` wicket's own ``config`` (``guards/plan.py``'s
|
|
113
|
+
module docstring: "a compiled plan IS a wicket config") -- ``None``
|
|
114
|
+
when no ``plan_containment`` wicket is active in this manifest."""
|
|
115
|
+
config = self.wicket_config("plan_containment")
|
|
116
|
+
return parse_plan_definition(config) if config else None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _check_profile(manifest: Manifest, profile: PolicyProfile | None, wickets: dict[str, WicketDefinition]) -> None:
|
|
120
|
+
"""Fail closed unless ``profile`` is exactly the one the manifest pins and
|
|
121
|
+
every value in it replaces a default an installed pack's wicket declares.
|
|
122
|
+
A ``counterparty_list`` list replaces the wicket's list whole."""
|
|
123
|
+
if manifest.profile_digest is None:
|
|
124
|
+
if profile is not None:
|
|
125
|
+
raise PolicyManifestError(
|
|
126
|
+
PROFILE_UNPINNED, "a profile was supplied but the manifest pins none, so nothing records it"
|
|
127
|
+
)
|
|
128
|
+
return
|
|
129
|
+
if profile is None:
|
|
130
|
+
raise PolicyManifestError(
|
|
131
|
+
PROFILE_MISSING, f"manifest pins profile {manifest.profile_digest}, but no profile was supplied"
|
|
132
|
+
)
|
|
133
|
+
if profile.profile_digest() != manifest.profile_digest:
|
|
134
|
+
raise PolicyManifestError(
|
|
135
|
+
PROFILE_DIGEST_DRIFT,
|
|
136
|
+
f"manifest pins profile {manifest.profile_digest}, but the supplied profile digests to "
|
|
137
|
+
f"{profile.profile_digest()}",
|
|
138
|
+
)
|
|
139
|
+
installed = {pack_name(p.pack_id) for p in manifest.packs}
|
|
140
|
+
for entry in profile.packs:
|
|
141
|
+
if entry.pack not in installed:
|
|
142
|
+
raise PolicyManifestError(
|
|
143
|
+
PROFILE_UNKNOWN_PACK, f"profile sets values for pack {entry.pack!r}, which the manifest does not install"
|
|
144
|
+
)
|
|
145
|
+
for check, keys in entry.parameters.items():
|
|
146
|
+
wicket = next((w for w in wickets.values() if w.check == check), None)
|
|
147
|
+
settable = LIST_KEYS if check == LIST_CHECK else CONFIGURABLE.get(check, frozenset())
|
|
148
|
+
for key, values in keys.items():
|
|
149
|
+
if wicket is None or key not in settable:
|
|
150
|
+
raise PolicyManifestError(
|
|
151
|
+
PROFILE_UNKNOWN_PARAMETER, f"profile sets {check}.{key}, which no installed wicket lets a user set"
|
|
152
|
+
)
|
|
153
|
+
if check == LIST_CHECK:
|
|
154
|
+
continue
|
|
155
|
+
defaults = wicket.config.get(key) or {}
|
|
156
|
+
undeclared = sorted(set(values) - set(defaults))
|
|
157
|
+
if undeclared:
|
|
158
|
+
raise PolicyManifestError(
|
|
159
|
+
PROFILE_UNKNOWN_PARAMETER,
|
|
160
|
+
f"profile sets {check}.{key} for {undeclared}, which wicket {wicket.wicket_id!r} declares "
|
|
161
|
+
"no default for; a profile replaces a default, it never adds a limit",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def resolve_manifest(
|
|
166
|
+
manifest: Manifest,
|
|
167
|
+
*,
|
|
168
|
+
fold_catalog_dir: str | Path,
|
|
169
|
+
wicket_catalog_dir: str | Path,
|
|
170
|
+
profile: PolicyProfile | None = None,
|
|
171
|
+
) -> ResolvedManifest:
|
|
172
|
+
fold_catalog = FoldCatalog(fold_catalog_dir)
|
|
173
|
+
wicket_catalog = WicketCatalog(wicket_catalog_dir)
|
|
174
|
+
|
|
175
|
+
folds: dict[str, FoldDefinition] = {}
|
|
176
|
+
for ref in manifest.folds:
|
|
177
|
+
if ref.engine not in SUPPORTED_FOLD_ENGINES:
|
|
178
|
+
raise PolicyManifestError(
|
|
179
|
+
UNKNOWN_ENGINE,
|
|
180
|
+
f"manifest cites fold {ref.fold_id!r} with engine {ref.engine!r}, this build only "
|
|
181
|
+
f"evaluates fold engines {sorted(SUPPORTED_FOLD_ENGINES)}",
|
|
182
|
+
)
|
|
183
|
+
entry = fold_catalog.get(ref.fold_id)
|
|
184
|
+
if entry is None:
|
|
185
|
+
raise PolicyManifestError(
|
|
186
|
+
UNKNOWN_FOLD_ID, f"manifest cites fold_id {ref.fold_id!r}, not found in catalog {fold_catalog_dir}"
|
|
187
|
+
)
|
|
188
|
+
if entry.digest != ref.digest:
|
|
189
|
+
raise PolicyManifestError(
|
|
190
|
+
FOLD_DIGEST_DRIFT,
|
|
191
|
+
f"manifest pins fold {ref.fold_id!r} at digest {ref.digest}, but the catalog's current "
|
|
192
|
+
f"definition digests to {entry.digest} -- the fold definition has changed since the "
|
|
193
|
+
"manifest was written",
|
|
194
|
+
)
|
|
195
|
+
folds[ref.fold_id] = entry.definition
|
|
196
|
+
|
|
197
|
+
wickets: dict[str, WicketDefinition] = {}
|
|
198
|
+
for ref in manifest.wickets:
|
|
199
|
+
if ref.engine not in SUPPORTED_WICKET_ENGINES:
|
|
200
|
+
raise PolicyManifestError(
|
|
201
|
+
UNKNOWN_ENGINE,
|
|
202
|
+
f"manifest cites wicket {ref.wicket_id!r} with engine {ref.engine!r}, this build only "
|
|
203
|
+
f"evaluates wicket engines {sorted(SUPPORTED_WICKET_ENGINES)}",
|
|
204
|
+
)
|
|
205
|
+
entry = wicket_catalog.get(ref.wicket_id)
|
|
206
|
+
if entry is None:
|
|
207
|
+
raise PolicyManifestError(
|
|
208
|
+
UNKNOWN_WICKET_ID,
|
|
209
|
+
f"manifest cites wicket_id {ref.wicket_id!r}, not found in catalog {wicket_catalog_dir}",
|
|
210
|
+
)
|
|
211
|
+
if entry.digest != ref.digest:
|
|
212
|
+
raise PolicyManifestError(
|
|
213
|
+
WICKET_DIGEST_DRIFT,
|
|
214
|
+
f"manifest pins wicket {ref.wicket_id!r} at digest {ref.digest}, but the catalog's current "
|
|
215
|
+
f"definition digests to {entry.digest} -- the wicket definition has changed since the "
|
|
216
|
+
"manifest was written",
|
|
217
|
+
)
|
|
218
|
+
wickets[ref.wicket_id] = entry.definition
|
|
219
|
+
|
|
220
|
+
_check_profile(manifest, profile, wickets)
|
|
221
|
+
|
|
222
|
+
return ResolvedManifest(
|
|
223
|
+
manifest=manifest, manifest_digest=manifest.manifest_digest(), folds=folds, wickets=wickets, profile=profile
|
|
224
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Obligation register format v0:
|
|
3
|
+
a loadable, GRC-shaped list of register rows, plus the OSS sample compiler that
|
|
4
|
+
turns one into an obligation-profile ``packs.schema.EvidenceContract``.
|
|
5
|
+
|
|
6
|
+
See the module docstrings in ``schema.py`` (``RegisterRow``/``ObligationRegister``,
|
|
7
|
+
the parsed register shape), ``loader.py`` (a register file/dict -> a validated
|
|
8
|
+
``ObligationRegister``), and ``compiler.py`` (``EvidenceCompiler``, the row ->
|
|
9
|
+
requirement path) for the pieces.
|
|
10
|
+
"""
|
|
11
|
+
from .compiler import (
|
|
12
|
+
EVIDENCE_CLASS_DEFAULTS,
|
|
13
|
+
EXCLUDED_CONTESTED_CLAUSE,
|
|
14
|
+
EXCLUDED_JUDGMENT_REQUIRED,
|
|
15
|
+
EXCLUDED_SYSTEM_OF_RECORD_READ_REQUIRED,
|
|
16
|
+
EvidenceCompiler,
|
|
17
|
+
ExcludedRow,
|
|
18
|
+
ObligationsPack,
|
|
19
|
+
)
|
|
20
|
+
from .errors import RegisterCompilerError, RegisterDefinitionError
|
|
21
|
+
from .loader import load_register_dict, load_register_file
|
|
22
|
+
from .schema import EVIDENCE_CLASS_VALUES, ObligationRegister, RegisterRow
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"RegisterRow",
|
|
26
|
+
"ObligationRegister",
|
|
27
|
+
"EVIDENCE_CLASS_VALUES",
|
|
28
|
+
"load_register_dict",
|
|
29
|
+
"load_register_file",
|
|
30
|
+
"RegisterDefinitionError",
|
|
31
|
+
"EvidenceCompiler",
|
|
32
|
+
"EVIDENCE_CLASS_DEFAULTS",
|
|
33
|
+
"RegisterCompilerError",
|
|
34
|
+
"ObligationsPack",
|
|
35
|
+
"ExcludedRow",
|
|
36
|
+
"EXCLUDED_JUDGMENT_REQUIRED",
|
|
37
|
+
"EXCLUDED_SYSTEM_OF_RECORD_READ_REQUIRED",
|
|
38
|
+
"EXCLUDED_CONTESTED_CLAUSE",
|
|
39
|
+
]
|