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,393 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Action-class taxonomy (gating decisions doc §1), the classification
|
|
3
|
+
default, and the measurement mapping.
|
|
4
|
+
|
|
5
|
+
The taxonomy is one versioned data table, ``action_taxonomy.json``. Each row
|
|
6
|
+
is a canonical action class and declares, explicitly and with no defaults:
|
|
7
|
+
its trigger class, ``consequential``, ``fail_open_allowed``,
|
|
8
|
+
``approver_role`` and its legacy aliases. ``load_taxonomy_table`` rejects a
|
|
9
|
+
row missing any of them, so a new action cannot silently inherit a policy.
|
|
10
|
+
|
|
11
|
+
Two axes, two jobs:
|
|
12
|
+
|
|
13
|
+
- **Gating** (``classify``): an action with no declared class -- or one not
|
|
14
|
+
in this taxonomy -- is CONSEQUENTIAL, fail-closed. This is deliberate: it
|
|
15
|
+
is the loophole every other rule in the failure-semantics table would
|
|
16
|
+
otherwise escape through.
|
|
17
|
+
- **Measurement** (``trigger_class``): the canonical action classes collapse
|
|
18
|
+
many-to-one onto a small set of trigger classes, which are the population
|
|
19
|
+
a "should this action have been checked" count is taken over. A trigger
|
|
20
|
+
class is derived from the action class, never authored per record. An
|
|
21
|
+
unknown name raises ``UnmappedActionClass`` here instead of degrading,
|
|
22
|
+
because the failure this guards against is an action counting toward no
|
|
23
|
+
population at all.
|
|
24
|
+
|
|
25
|
+
``CHANGE`` is a trigger class no row may declare: it is a transition after a
|
|
26
|
+
prior approval, so it is derived from state by the caller
|
|
27
|
+
(``after_prior_approval=True``), not from the action's name.
|
|
28
|
+
|
|
29
|
+
Sealed records are never rewritten. A legacy class name already sealed into
|
|
30
|
+
a record resolves through ``legacy_aliases`` to its canonical row on read.
|
|
31
|
+
A decision capsule carries ``taxonomy_version`` in ``asg_payload`` only when
|
|
32
|
+
its ``Action.taxonomy_version`` was set (``guards/capsule.py``); nothing in
|
|
33
|
+
this package sets it by default yet, so existing records keep their bytes. A
|
|
34
|
+
record without one is read as ``unversioned_records_read_as``.
|
|
35
|
+
|
|
36
|
+
The engine evaluates under the current table only, except that a replay of
|
|
37
|
+
historical records evaluates each under the table it was sealed with
|
|
38
|
+
(``carried_taxonomy``). Earlier tables are carried byte-identical to what
|
|
39
|
+
shipped, as ``action_taxonomy_v<N>.json``, for every version a sealed record
|
|
40
|
+
can name; version 1 never had a table (it is what an unversioned record is
|
|
41
|
+
read as), so it is not carried.
|
|
42
|
+
|
|
43
|
+
Caps config resolves through this table (``checks.caps.resolve_caps_minor``),
|
|
44
|
+
so a cap keyed by a legacy name and one keyed by its canonical name are one
|
|
45
|
+
cap. Tolerances and fail-open opt-ins are still keyed by the raw
|
|
46
|
+
``action_class`` string a caller sends.
|
|
47
|
+
"""
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import json
|
|
51
|
+
from collections.abc import Mapping
|
|
52
|
+
from dataclasses import dataclass
|
|
53
|
+
from importlib import resources
|
|
54
|
+
from types import MappingProxyType
|
|
55
|
+
from typing import TypedDict
|
|
56
|
+
|
|
57
|
+
from agent_action_capsule import json_digest
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"ActionClass",
|
|
61
|
+
"CARRIED_TAXONOMY_VERSIONS",
|
|
62
|
+
"ENGINE_TAXONOMY",
|
|
63
|
+
"TAXONOMY",
|
|
64
|
+
"TAXONOMY_DIGEST",
|
|
65
|
+
"TAXONOMY_VERSION",
|
|
66
|
+
"TRIGGER_CLASSES",
|
|
67
|
+
"UNCLASSIFIED_DEFAULT",
|
|
68
|
+
"UNVERSIONED_TAXONOMY_VERSION",
|
|
69
|
+
"TaxonomyDocument",
|
|
70
|
+
"TaxonomyRow",
|
|
71
|
+
"TaxonomyTable",
|
|
72
|
+
"TaxonomyTableError",
|
|
73
|
+
"UnmappedActionClass",
|
|
74
|
+
"VersionedPayload",
|
|
75
|
+
"carried_taxonomy",
|
|
76
|
+
"check_connector_declaration",
|
|
77
|
+
"classify",
|
|
78
|
+
"is_known_action_class",
|
|
79
|
+
"legacy_aliases",
|
|
80
|
+
"load_taxonomy_table",
|
|
81
|
+
"record_taxonomy_version",
|
|
82
|
+
"resolve",
|
|
83
|
+
"same_classes",
|
|
84
|
+
"trigger_class",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
_TABLE_FILE = "action_taxonomy.json"
|
|
88
|
+
# Earlier tables, as shipped: "<version>" -> file. The current one is _TABLE_FILE.
|
|
89
|
+
_EARLIER_TABLE_FILES = {v: f"action_taxonomy_v{v}.json" for v in ("2", "3", "4", "5")}
|
|
90
|
+
_ROW_KEYS = frozenset(
|
|
91
|
+
{"name", "trigger_class", "consequential", "fail_open_allowed", "approver_role", "legacy_aliases"}
|
|
92
|
+
)
|
|
93
|
+
_TOP_KEYS = frozenset(
|
|
94
|
+
{"taxonomy_version", "unversioned_records_read_as", "trigger_classes", "state_derived_trigger_classes", "actions"}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class ActionClass:
|
|
100
|
+
name: str
|
|
101
|
+
consequential: bool
|
|
102
|
+
# Whether this class MAY be configured to fail open on a stale view or an
|
|
103
|
+
# unreachable engine (gating doc §1: "fail-open permitted for low-risk
|
|
104
|
+
# classes when explicitly configured"). This never makes fail-open a
|
|
105
|
+
# default by itself -- a caller must still opt in per-class on the engine.
|
|
106
|
+
fail_open_allowed: bool = False
|
|
107
|
+
# The role that can resolve a cap-exceeded hold on this class via the
|
|
108
|
+
# HITL bridge (D2, 2026-08-05). None means no approver is configured for
|
|
109
|
+
# this class -- a cap-exceeded action in it hard-denies rather than
|
|
110
|
+
# escalating; this is the "classes explicitly marked deny" half of D2.
|
|
111
|
+
approver_role: str | None = None
|
|
112
|
+
# The measurement population this class counts toward. None only for a
|
|
113
|
+
# non-consequential class (outside every population) and for
|
|
114
|
+
# UNCLASSIFIED_DEFAULT (which ``trigger_class`` refuses to count).
|
|
115
|
+
trigger_class: str | None = None
|
|
116
|
+
legacy_aliases: tuple[str, ...] = ()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TaxonomyRow(TypedDict):
|
|
120
|
+
"""One row of ``action_taxonomy.json``, as authored."""
|
|
121
|
+
|
|
122
|
+
name: str
|
|
123
|
+
trigger_class: str | None
|
|
124
|
+
consequential: bool
|
|
125
|
+
fail_open_allowed: bool
|
|
126
|
+
approver_role: str | None
|
|
127
|
+
legacy_aliases: list[str]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TaxonomyDocument(TypedDict):
|
|
131
|
+
"""``action_taxonomy.json``, as authored."""
|
|
132
|
+
|
|
133
|
+
taxonomy_version: str
|
|
134
|
+
unversioned_records_read_as: str
|
|
135
|
+
trigger_classes: list[str]
|
|
136
|
+
state_derived_trigger_classes: list[str]
|
|
137
|
+
actions: list[TaxonomyRow]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class VersionedPayload(TypedDict, total=False):
|
|
141
|
+
"""The part of a decision capsule's ``asg_payload`` this module reads."""
|
|
142
|
+
|
|
143
|
+
action_class: str
|
|
144
|
+
taxonomy_version: str
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TaxonomyTableError(ValueError):
|
|
148
|
+
"""The taxonomy table is malformed or under-declared."""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class UnmappedActionClass(LookupError):
|
|
152
|
+
"""A name has no row in the taxonomy, so it belongs to no population."""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class TaxonomyTable:
|
|
157
|
+
version: str
|
|
158
|
+
unversioned_read_as: str
|
|
159
|
+
trigger_classes: tuple[str, ...]
|
|
160
|
+
classes: Mapping[str, ActionClass]
|
|
161
|
+
aliases: Mapping[str, str]
|
|
162
|
+
|
|
163
|
+
def resolve(self, action_class: str) -> ActionClass | None:
|
|
164
|
+
"""The canonical row for a canonical or legacy name, or None."""
|
|
165
|
+
return self.classes.get(self.aliases.get(action_class, action_class))
|
|
166
|
+
|
|
167
|
+
def classify(self, action_class: str | None) -> ActionClass:
|
|
168
|
+
"""``classify`` under this table."""
|
|
169
|
+
if action_class is None:
|
|
170
|
+
return UNCLASSIFIED_DEFAULT
|
|
171
|
+
return self.resolve(action_class) or UNCLASSIFIED_DEFAULT
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _require_bool(value: object, where: str) -> bool:
|
|
175
|
+
if not isinstance(value, bool):
|
|
176
|
+
raise TaxonomyTableError(f"{where} must be true or false, got {value!r}")
|
|
177
|
+
return value
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _require_optional_str(value: object, where: str) -> str | None:
|
|
181
|
+
if value is None:
|
|
182
|
+
return None
|
|
183
|
+
return _require_str(value, f"{where} (or null)")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _require_str(value: object, where: str) -> str:
|
|
187
|
+
if not isinstance(value, str) or not value:
|
|
188
|
+
raise TaxonomyTableError(f"{where} must be a non-empty string, got {value!r}")
|
|
189
|
+
return value
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _require_str_list(value: object, where: str) -> tuple[str, ...]:
|
|
193
|
+
if not isinstance(value, list):
|
|
194
|
+
raise TaxonomyTableError(f"{where} must be a list, got {value!r}")
|
|
195
|
+
return tuple(_require_str(item, f"{where}[{i}]") for i, item in enumerate(value))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _parse_row(row: object, idx: int, declarable: frozenset[str]) -> ActionClass:
|
|
199
|
+
where = f"actions[{idx}]"
|
|
200
|
+
if not isinstance(row, dict):
|
|
201
|
+
raise TaxonomyTableError(f"{where} must be an object")
|
|
202
|
+
missing = _ROW_KEYS - row.keys()
|
|
203
|
+
if missing:
|
|
204
|
+
raise TaxonomyTableError(f"{where} does not declare {sorted(missing)}; every row declares every key")
|
|
205
|
+
extra = row.keys() - _ROW_KEYS
|
|
206
|
+
if extra:
|
|
207
|
+
raise TaxonomyTableError(f"{where} carries unknown keys {sorted(extra)}")
|
|
208
|
+
name = _require_str(row["name"], f"{where}.name")
|
|
209
|
+
where = f"actions[{name!r}]"
|
|
210
|
+
consequential = _require_bool(row["consequential"], f"{where}.consequential")
|
|
211
|
+
fail_open_allowed = _require_bool(row["fail_open_allowed"], f"{where}.fail_open_allowed")
|
|
212
|
+
approver_role = _require_optional_str(row["approver_role"], f"{where}.approver_role")
|
|
213
|
+
trigger = _require_optional_str(row["trigger_class"], f"{where}.trigger_class")
|
|
214
|
+
if consequential and fail_open_allowed:
|
|
215
|
+
raise TaxonomyTableError(f"{where} is consequential and sets fail_open_allowed; only a non-consequential class may")
|
|
216
|
+
if consequential and trigger is None:
|
|
217
|
+
raise TaxonomyTableError(f"{where} is consequential and has no trigger_class")
|
|
218
|
+
if not consequential and trigger is not None:
|
|
219
|
+
raise TaxonomyTableError(f"{where} is non-consequential but declares trigger_class {trigger!r}")
|
|
220
|
+
if trigger is not None and trigger not in declarable:
|
|
221
|
+
raise TaxonomyTableError(f"{where}.trigger_class={trigger!r} is not a declarable trigger class {sorted(declarable)}")
|
|
222
|
+
return ActionClass(
|
|
223
|
+
name=name,
|
|
224
|
+
consequential=consequential,
|
|
225
|
+
fail_open_allowed=fail_open_allowed,
|
|
226
|
+
approver_role=approver_role,
|
|
227
|
+
trigger_class=trigger,
|
|
228
|
+
legacy_aliases=_require_str_list(row["legacy_aliases"], f"{where}.legacy_aliases"),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def load_taxonomy_table(raw: object) -> TaxonomyTable:
|
|
233
|
+
"""Validate a parsed taxonomy table (a ``TaxonomyDocument`` once it passes). Raises ``TaxonomyTableError`` on any
|
|
234
|
+
under-declared row, duplicate name, or alias collision."""
|
|
235
|
+
if not isinstance(raw, dict):
|
|
236
|
+
raise TaxonomyTableError("taxonomy table must be an object")
|
|
237
|
+
missing = _TOP_KEYS - raw.keys()
|
|
238
|
+
if missing:
|
|
239
|
+
raise TaxonomyTableError(f"taxonomy table does not declare {sorted(missing)}")
|
|
240
|
+
version = _require_str(raw["taxonomy_version"], "taxonomy_version")
|
|
241
|
+
unversioned = _require_str(raw["unversioned_records_read_as"], "unversioned_records_read_as")
|
|
242
|
+
triggers = _require_str_list(raw["trigger_classes"], "trigger_classes")
|
|
243
|
+
derived = _require_str_list(raw["state_derived_trigger_classes"], "state_derived_trigger_classes")
|
|
244
|
+
if not set(derived) <= set(triggers):
|
|
245
|
+
raise TaxonomyTableError(f"state_derived_trigger_classes {list(derived)} must be trigger classes")
|
|
246
|
+
declarable = frozenset(triggers) - frozenset(derived)
|
|
247
|
+
rows = raw["actions"]
|
|
248
|
+
if not isinstance(rows, list) or not rows:
|
|
249
|
+
raise TaxonomyTableError("actions must be a non-empty list")
|
|
250
|
+
|
|
251
|
+
classes: dict[str, ActionClass] = {}
|
|
252
|
+
for idx, row in enumerate(rows):
|
|
253
|
+
ac = _parse_row(row, idx, declarable)
|
|
254
|
+
if ac.name in classes:
|
|
255
|
+
raise TaxonomyTableError(f"action class {ac.name!r} declared more than once")
|
|
256
|
+
classes[ac.name] = ac
|
|
257
|
+
aliases: dict[str, str] = {}
|
|
258
|
+
for ac in classes.values():
|
|
259
|
+
for alias in ac.legacy_aliases:
|
|
260
|
+
if alias in classes or alias in aliases:
|
|
261
|
+
raise TaxonomyTableError(f"legacy alias {alias!r} on {ac.name!r} collides with another name")
|
|
262
|
+
aliases[alias] = ac.name
|
|
263
|
+
if not any(not ac.consequential for ac in classes.values()):
|
|
264
|
+
raise TaxonomyTableError("taxonomy has no non-consequential class; the negative set would vanish")
|
|
265
|
+
return TaxonomyTable(
|
|
266
|
+
version=version,
|
|
267
|
+
unversioned_read_as=unversioned,
|
|
268
|
+
trigger_classes=triggers,
|
|
269
|
+
classes=MappingProxyType(classes),
|
|
270
|
+
aliases=MappingProxyType(aliases),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _load_packaged(name: str = _TABLE_FILE) -> tuple[TaxonomyTable, str]:
|
|
275
|
+
"""The packaged table ``name`` and the SHA-256 over its JCS bytes."""
|
|
276
|
+
raw = json.loads(resources.files("capsule_engine.guards").joinpath(name).read_text(encoding="utf-8"))
|
|
277
|
+
return load_taxonomy_table(raw), json_digest(raw)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _load_earlier() -> dict[str, TaxonomyTable]:
|
|
281
|
+
tables: dict[str, TaxonomyTable] = {}
|
|
282
|
+
for version, name in _EARLIER_TABLE_FILES.items():
|
|
283
|
+
table, _ = _load_packaged(name)
|
|
284
|
+
if table.version != version:
|
|
285
|
+
raise TaxonomyTableError(f"{name} declares taxonomy_version {table.version!r}, not {version!r}")
|
|
286
|
+
tables[version] = table
|
|
287
|
+
return tables
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
_TABLE, _DIGEST = _load_packaged()
|
|
291
|
+
_CARRIED: Mapping[str, TaxonomyTable] = MappingProxyType({**_load_earlier(), _TABLE.version: _TABLE})
|
|
292
|
+
|
|
293
|
+
# The table this engine evaluates live decisions under.
|
|
294
|
+
ENGINE_TAXONOMY: TaxonomyTable = _TABLE
|
|
295
|
+
# Every taxonomy version a replay can evaluate a sealed record under.
|
|
296
|
+
CARRIED_TAXONOMY_VERSIONS: tuple[str, ...] = tuple(_CARRIED)
|
|
297
|
+
|
|
298
|
+
# A pack may pin this with its version (``packs/schema.py`` ``TaxonomyPin``),
|
|
299
|
+
# so a taxonomy change, even one keeping its version, moves the pack digest.
|
|
300
|
+
TAXONOMY_DIGEST: str = _DIGEST
|
|
301
|
+
|
|
302
|
+
TAXONOMY_VERSION: str = _TABLE.version
|
|
303
|
+
UNVERSIONED_TAXONOMY_VERSION: str = _TABLE.unversioned_read_as
|
|
304
|
+
TRIGGER_CLASSES: tuple[str, ...] = _TABLE.trigger_classes
|
|
305
|
+
# Canonical rows only; legacy names resolve through ``resolve``/``classify``.
|
|
306
|
+
TAXONOMY: Mapping[str, ActionClass] = MappingProxyType(dict(_TABLE.classes))
|
|
307
|
+
|
|
308
|
+
MONEY_TRANSFER = TAXONOMY["money.transfer"]
|
|
309
|
+
DATA_DELETE = TAXONOMY["data.delete"]
|
|
310
|
+
COMMS_EXTERNAL = TAXONOMY["communication.send"]
|
|
311
|
+
# The one low-risk class, so "fail-open only where declared" is a real,
|
|
312
|
+
# exercised path rather than a theoretical one.
|
|
313
|
+
INFO_QUERY = TAXONOMY["info.query"]
|
|
314
|
+
|
|
315
|
+
UNCLASSIFIED_DEFAULT = ActionClass("unclassified", consequential=True)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def carried_taxonomy(version: object) -> TaxonomyTable | None:
|
|
319
|
+
"""The table for taxonomy ``version`` as it shipped, or None when this
|
|
320
|
+
engine does not carry it (only a string names a version)."""
|
|
321
|
+
return _CARRIED.get(version) if isinstance(version, str) else None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def same_classes(version: object) -> bool:
|
|
325
|
+
"""Whether a record sealed under taxonomy ``version`` names its class
|
|
326
|
+
from a table with exactly the engine's class names, in its order: the
|
|
327
|
+
engine's own version, or a carried one that differs only in approvers."""
|
|
328
|
+
table = ENGINE_TAXONOMY if version == ENGINE_TAXONOMY.version else carried_taxonomy(version)
|
|
329
|
+
return table is not None and tuple(table.classes) == tuple(ENGINE_TAXONOMY.classes)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def resolve(action_class: str) -> ActionClass | None:
|
|
333
|
+
"""The canonical row for a canonical or legacy name, or None."""
|
|
334
|
+
return _TABLE.resolve(action_class)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def is_known_action_class(action_class: str) -> bool:
|
|
338
|
+
return resolve(action_class) is not None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def legacy_aliases() -> Mapping[str, str]:
|
|
342
|
+
"""``{legacy name: canonical name}`` for every alias in the table."""
|
|
343
|
+
return _TABLE.aliases
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def classify(action_class: str | None) -> ActionClass:
|
|
347
|
+
"""Resolve a declared class name to its policy.
|
|
348
|
+
|
|
349
|
+
Absent, or not present in the taxonomy, both resolve to the
|
|
350
|
+
consequential/fail-closed default -- the classification default is not
|
|
351
|
+
conditioned on the name being *recognized*, only on it being *declared
|
|
352
|
+
and known*. A legacy name resolves to its canonical row.
|
|
353
|
+
"""
|
|
354
|
+
return _TABLE.classify(action_class)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def trigger_class(action_class: str, *, after_prior_approval: bool = False) -> str | None:
|
|
358
|
+
"""The measurement population ``action_class`` counts toward.
|
|
359
|
+
|
|
360
|
+
``None`` means the class is non-consequential and outside every
|
|
361
|
+
population. ``after_prior_approval`` is the caller's state: a
|
|
362
|
+
consequential action that amends something already approved counts as
|
|
363
|
+
``CHANGE``. Raises ``UnmappedActionClass`` for a name with no row.
|
|
364
|
+
"""
|
|
365
|
+
ac = resolve(action_class)
|
|
366
|
+
if ac is None:
|
|
367
|
+
raise UnmappedActionClass(
|
|
368
|
+
f"action class {action_class!r} has no row in taxonomy version {TAXONOMY_VERSION}; "
|
|
369
|
+
"add it to guards/action_taxonomy.json before it can be counted"
|
|
370
|
+
)
|
|
371
|
+
if not ac.consequential:
|
|
372
|
+
return None
|
|
373
|
+
if after_prior_approval:
|
|
374
|
+
return "CHANGE"
|
|
375
|
+
return ac.trigger_class
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def check_connector_declaration(exposed: Mapping[str, str]) -> dict[str, ActionClass]:
|
|
379
|
+
"""Validate a connector's ``{tool name: canonical action class}``
|
|
380
|
+
declaration. Every target must be a canonical row (not a legacy alias);
|
|
381
|
+
raises ``UnmappedActionClass`` naming every tool that is not."""
|
|
382
|
+
bad = sorted(tool for tool, name in exposed.items() if name not in TAXONOMY)
|
|
383
|
+
if bad:
|
|
384
|
+
raise UnmappedActionClass(f"connector tools {bad} declare no canonical action class in taxonomy version {TAXONOMY_VERSION}")
|
|
385
|
+
return {tool: TAXONOMY[name] for tool, name in exposed.items()}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def record_taxonomy_version(asg_payload: VersionedPayload) -> str:
|
|
389
|
+
"""The taxonomy version a sealed decision capsule's ``action_class`` was
|
|
390
|
+
written under; records that predate the table carry none."""
|
|
391
|
+
if "taxonomy_version" not in asg_payload:
|
|
392
|
+
return UNVERSIONED_TAXONOMY_VERSION
|
|
393
|
+
return _require_str(asg_payload["taxonomy_version"], "asg_payload.taxonomy_version")
|