diff --git a/.github/workflows/py-sdk-build-publish.yml b/.github/workflows/py-sdk-build-publish.yml index b36f8ef..3594464 100644 --- a/.github/workflows/py-sdk-build-publish.yml +++ b/.github/workflows/py-sdk-build-publish.yml @@ -119,6 +119,7 @@ jobs: publish_test: name: Publish to Test PyPI + if: github.ref != 'refs/heads/main' needs: [build_wheels, build_sdist] runs-on: ubuntu-latest #environment: pypi @@ -205,8 +206,4 @@ jobs: - name: Publish package uses: pypa/gh-action-pypi-publish@release/v1 with: - packages-dir: dist - - - uses: softprops/action-gh-release@v2 - with: - files: dist/* \ No newline at end of file + packages-dir: dist \ No newline at end of file diff --git a/.gitignore b/.gitignore index 362e5fc..6b02796 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,6 @@ __pycache__/ lib_actra_core.dylib.dSYM/ .pytest_cache/ venv_test -wheelhouse \ No newline at end of file +wheelhouse +____*/ +____* \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 6b78db9..7487092 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.2.2" +version = "0.3.0" edition = "2021" license = "Apache-2.0" authors = ["Amit Saxena"] diff --git a/examples/python/core-sdk/basic_refund.py b/examples/python/core-sdk/basic_refund.py new file mode 100644 index 0000000..f550338 --- /dev/null +++ b/examples/python/core-sdk/basic_refund.py @@ -0,0 +1,124 @@ +""" +Basic Actra Example + +This example shows how Actra can enforce admission +control policies on normal Python functions. + +Actra evaluates a policy BEFORE the function executes +and blocks the call if the policy denies it. +""" + +from actra import Actra +from actra.runtime import ActraRuntime + + +# ------------------------------------------------------------ +# 1. Schema definition +# ------------------------------------------------------------ +# The schema defines the structure of data that policies +# are allowed to reference. +# +# Domains: +# - action : parameters passed to the function +# - actor : identity of the caller +# - snapshot : external system state +# +schema_yaml = """ +version: 1 + +actions: + refund: + fields: + amount: number + +actor: + fields: + role: string + +snapshot: + fields: + fraud_flag: boolean +""" + + +# ------------------------------------------------------------ +# 2. Policy definition +# ------------------------------------------------------------ +# This policy blocks refunds larger than 1000 +# +# Scope limits the rule to the "refund" action +# The rule inspects the action.amount field +# +policy_yaml = """ +version: 1 + +rules: + - id: block_large_refund + scope: + action: refund + when: + subject: + domain: action + field: amount + operator: greater_than + value: + literal: 1000 + effect: block +""" + +# ------------------------------------------------------------ +# 3. Compile policy +# ------------------------------------------------------------ + +policy = Actra.from_strings(schema_yaml, policy_yaml) + +# ------------------------------------------------------------ +# 4. Create runtime +# ------------------------------------------------------------ + +runtime = ActraRuntime(policy) + +# ------------------------------------------------------------ +# 5. Register context resolvers +# ------------------------------------------------------------ +# Resolvers dynamically supply runtime context used by policies. +# +# actor_resolver : information about the caller +# snapshot_resolver: external system state +# + +runtime.set_actor_resolver(lambda ctx: {"role": "support"}) +runtime.set_snapshot_resolver(lambda ctx: {"fraud_flag": False}) + + +# ------------------------------------------------------------ +# 6. Protect a function with Actra +# ------------------------------------------------------------ +# The @runtime.admit decorator intercepts the function call +# and evaluates policies before execution. +# +# 1. Default mapping +# @runtime.admit() : all kwargs become action fields +# +# 2. Field filtering +# @runtime.admit(fields=["amount"]) +# +# 3. Custom action builder +# @runtime.admit(action_builder=my_builder) +# + +@runtime.admit() +def refund(amount: int): + print("Refund executed:", amount) + + +# ------------------------------------------------------------ +# 7. Execute calls +# ------------------------------------------------------------ + +print("\n--- Allowed call ---") +refund(amount=200) + + +print("\n--- Blocked call ---") +refund(amount=1500) \ No newline at end of file diff --git a/examples/python/core-sdk/custom_action_builder.py b/examples/python/core-sdk/custom_action_builder.py new file mode 100644 index 0000000..65106c8 --- /dev/null +++ b/examples/python/core-sdk/custom_action_builder.py @@ -0,0 +1,107 @@ +""" +Custom Action Builder Example + +This example demonstrates how to provide a custom function +to construct the Actra action object. + +Custom builders are useful when application inputs do not +map directly to policy fields. +""" + +from actra import Actra +from actra.runtime import ActraRuntime + + +# ------------------------------------------------------------ +# 1. Schema +# ------------------------------------------------------------ + +schema_yaml = """ +version: 1 + +actions: + refund: + fields: + amount: number + +actor: + fields: + role: string + +snapshot: + fields: + fraud_flag: boolean +""" + + +# ------------------------------------------------------------ +# 2. Policy +# ------------------------------------------------------------ + +policy_yaml = """ +version: 1 + +rules: + - id: block_large_refund + scope: + action: refund + when: + subject: + domain: action + field: amount + operator: greater_than + value: + literal: 1000 + effect: block +""" + + +# ------------------------------------------------------------ +# 3. Compile policy +# ------------------------------------------------------------ + +policy = Actra.from_strings(schema_yaml, policy_yaml) +runtime = ActraRuntime(policy) + + +# ------------------------------------------------------------ +# 4. Register resolvers +# ------------------------------------------------------------ + +runtime.set_actor_resolver(lambda ctx: {"role": "support"}) +runtime.set_snapshot_resolver(lambda ctx: {"fraud_flag": False}) + + +# ------------------------------------------------------------ +# 5. Custom action builder +# ------------------------------------------------------------ + +def build_refund_action(action_type, args, kwargs, ctx): + """ + Convert application inputs into the policy action object. + """ + + return { + "type": action_type, + "amount": kwargs["amount"] + } + + +# ------------------------------------------------------------ +# 6. Protect function using custom builder +# ------------------------------------------------------------ + +@runtime.admit(action_builder=build_refund_action) +def refund(amount: int, currency: str): + print(f"Refund executed: {amount} {currency}") + + +# ------------------------------------------------------------ +# 7. Calls +# ------------------------------------------------------------ + +print("\nAllowed call") +refund(amount=200, currency="USD") + +print("\nBlocked call") +refund(amount=1500, currency="USD") \ No newline at end of file diff --git a/examples/python/core-sdk/custom_actor_snapshot_resolvers.py b/examples/python/core-sdk/custom_actor_snapshot_resolvers.py new file mode 100644 index 0000000..4202808 --- /dev/null +++ b/examples/python/core-sdk/custom_actor_snapshot_resolvers.py @@ -0,0 +1,88 @@ +""" +Custom Actor and Snapshot Resolvers Example + +Demonstrates how runtime context can be used to dynamically +resolve actor and system state. +""" + +from actra import Actra, ActraRuntime + + +schema_yaml = """ +version: 1 + +actions: + refund: + fields: + amount: number + +actor: + fields: + role: string + +snapshot: + fields: + fraud_flag: boolean +""" + + +policy_yaml = """ +version: 1 + +rules: + - id: block_large_refund_for_support + scope: + action: refund + when: + all: + - subject: + domain: action + field: amount + operator: greater_than + value: + literal: 1000 + - subject: + domain: actor + field: role + operator: equals + value: + literal: "support" + effect: block +""" + + +policy = Actra.from_strings(schema_yaml, policy_yaml) +runtime = ActraRuntime(policy) + + +# Example request context +class RequestContext: + def __init__(self, role, fraud_flag): + self.role = role + self.fraud_flag = fraud_flag + + +# Actor resolver +runtime.set_actor_resolver( + lambda ctx: {"role": ctx.role} +) + +# Snapshot resolver +runtime.set_snapshot_resolver( + lambda ctx: {"fraud_flag": ctx.fraud_flag} +) + + +ctx = RequestContext(role="support", fraud_flag=False) + + +action = runtime.build_action( + action_type="refund", + args=(), + kwargs={"amount": 2000}, + ctx=ctx +) + +decision = runtime.evaluate(action, ctx) + +print(decision) \ No newline at end of file diff --git a/examples/python/core-sdk/fields_filtering.py b/examples/python/core-sdk/fields_filtering.py new file mode 100644 index 0000000..16513ea --- /dev/null +++ b/examples/python/core-sdk/fields_filtering.py @@ -0,0 +1,88 @@ +""" +Field Filtering Example + +This example demonstrates how to restrict which parameters +become part of the Actra action object. + +This is useful when functions contain additional arguments +that should not be exposed to the policy engine. +""" + +from actra import Actra +from actra.runtime import ActraRuntime + + +# ------------------------------------------------------------ +# 1. Schema +# ------------------------------------------------------------ + +schema_yaml = """ +version: 1 + +actions: + refund: + fields: + amount: number + +actor: + fields: + role: string + +snapshot: + fields: + fraud_flag: boolean +""" + + +# ------------------------------------------------------------ +# 2. Policy +# ------------------------------------------------------------ + +policy_yaml = """ +version: 1 + +rules: + - id: block_large_refund + scope: + action: refund + when: + subject: + domain: action + field: amount + operator: greater_than + value: + literal: 1000 + effect: block +""" + + +# ------------------------------------------------------------ +# 3. Compile policy and create runtime +# ------------------------------------------------------------ + +policy = Actra.from_strings(schema_yaml, policy_yaml) +runtime = ActraRuntime(policy) + + +runtime.set_actor_resolver(lambda ctx: {"role": "support"}) +runtime.set_snapshot_resolver(lambda ctx: {"fraud_flag": False}) + + +# ------------------------------------------------------------ +# 4. Protect function with field filtering +# ------------------------------------------------------------ + +@runtime.admit(fields=["amount"]) +def refund(amount: int, currency: str): + print(f"Refund executed: {amount} {currency}") + + +# ------------------------------------------------------------ +# 5. Calls +# ------------------------------------------------------------ + +print("\nAllowed call") +refund(amount=200, currency="USD") + +print("\nBlocked call") +refund(amount=1500, currency="USD") \ No newline at end of file diff --git a/examples/python/core-sdk/manual_runtime_evaluation.py b/examples/python/core-sdk/manual_runtime_evaluation.py new file mode 100644 index 0000000..d0016a6 --- /dev/null +++ b/examples/python/core-sdk/manual_runtime_evaluation.py @@ -0,0 +1,120 @@ +""" +Manual Runtime Evaluation Example + +This example demonstrates how to use ActraRuntime directly +without decorators. + +This pattern is useful for integrations such as: +- API frameworks +- MCP servers +- background workers +- CLI tools + +The application constructs an action object and asks +the runtime to evaluate it. +""" + +from actra import Actra +from actra.runtime import ActraRuntime + +# ------------------------------------------------------------ +# 1. Schema +# ------------------------------------------------------------ + +schema_yaml = """ +version: 1 + +actions: + refund: + fields: + amount: number + +actor: + fields: + role: string + +snapshot: + fields: + fraud_flag: boolean +""" + +# ------------------------------------------------------------ +# 2. Policy +# ------------------------------------------------------------ + +policy_yaml = """ +version: 1 + +rules: + - id: block_large_refund + scope: + action: refund + when: + subject: + domain: action + field: amount + operator: greater_than + value: + literal: 1000 + effect: block +""" + +# ------------------------------------------------------------ +# 3. Compile policy +# ------------------------------------------------------------ + +policy = Actra.from_strings(schema_yaml, policy_yaml) + +# ------------------------------------------------------------ +# 4. Create runtime +# ------------------------------------------------------------ + +runtime = ActraRuntime(policy) + +# ------------------------------------------------------------ +# 5. Context resolvers +# ------------------------------------------------------------ + +runtime.set_actor_resolver(lambda ctx: {"role": "support"}) +runtime.set_snapshot_resolver(lambda ctx: {"fraud_flag": False}) + +# ------------------------------------------------------------ +# 6. Build an action manually +# ------------------------------------------------------------ +# In many integrations the action will come from: +# - HTTP requests +# - tool calls +# - message queues +# + +action = runtime.build_action( + action_type="refund", + args=(), + kwargs={"amount": 200}, + ctx=None +) + +# ------------------------------------------------------------ +# 7. Evaluate the action +# ------------------------------------------------------------ + +result = runtime.evaluate(action) + +print("\nEvaluation result:") +print(result) + +# ------------------------------------------------------------ +# 8. Blocked example +# ------------------------------------------------------------ + +blocked_action = runtime.build_action( + action_type="refund", + args=(), + kwargs={"amount": 1500}, + ctx=None +) + +blocked_result = runtime.evaluate(blocked_action) + +print("\nBlocked evaluation result:") +print(blocked_result) \ No newline at end of file diff --git a/examples/python/core-sdk/multiple_runtimes.py b/examples/python/core-sdk/multiple_runtimes.py new file mode 100644 index 0000000..0ee718c --- /dev/null +++ b/examples/python/core-sdk/multiple_runtimes.py @@ -0,0 +1,170 @@ +""" +Multiple Runtimes Example + +This example demonstrates that multiple Actra runtimes +can coexist in the same application. + +Each runtime can enforce a different policy. +""" + +from actra import Actra +from actra.runtime import ActraRuntime + + +# ------------------------------------------------------------ +# 1. Schema +# ------------------------------------------------------------ + +schema_yaml = """ +version: 1 + +actions: + refund: + fields: + amount: number + +actor: + fields: + role: string + +snapshot: + fields: + fraud_flag: boolean +""" + + +# ------------------------------------------------------------ +# 2. Policy A — support users +# ------------------------------------------------------------ + +support_policy_yaml = """ +version: 1 + +rules: + - id: support_limit + scope: + action: refund + when: + all: + - subject: + domain: action + field: amount + operator: greater_than + value: + literal: 1000 + - subject: + domain: actor + field: role + operator: equals + value: + literal: "support" + effect: block +""" + + +# ------------------------------------------------------------ +# 3. Policy B — admin users +# ------------------------------------------------------------ + +admin_policy_yaml = """ +version: 1 + +rules: + - id: admin_limit + scope: + action: refund + when: + all: + - subject: + domain: action + field: amount + operator: greater_than + value: + literal: 10000 + - subject: + domain: actor + field: role + operator: equals + value: + literal: "admin" + effect: block +""" + +# ------------------------------------------------------------ +# 4. Compile policies +# ------------------------------------------------------------ + +support_policy = Actra.from_strings(schema_yaml, support_policy_yaml) +admin_policy = Actra.from_strings(schema_yaml, admin_policy_yaml) + + +# ------------------------------------------------------------ +# 5. Create runtimes +# ------------------------------------------------------------ + +support_runtime = ActraRuntime(support_policy) +admin_runtime = ActraRuntime(admin_policy) + + +support_runtime.set_actor_resolver(lambda ctx: {"role": "support"}) +support_runtime.set_snapshot_resolver(lambda ctx: {"fraud_flag": False}) + +admin_runtime.set_actor_resolver(lambda ctx: {"role": "admin"}) +admin_runtime.set_snapshot_resolver(lambda ctx: {"fraud_flag": False}) + + +# ------------------------------------------------------------ +# 6. Protected functions +# ------------------------------------------------------------ + +# IMPORTANT: +# By default, the Actra decorator uses the Python function name +# as the action type when constructing the policy action. +# +# For example: +# def support_refund() : action.type = "support_refund" +# +# However, the policy in this example expects the action: +# +# scope: +# action: refund +# +# Therefore we override the action name using: +# +# action_type="refund" +# +# This maps both functions to the same policy action. +# + +@support_runtime.admit(action_type="refund") +def support_refund(amount: int): + print(f"Support refund executed: {amount}") + + +@admin_runtime.admit(action_type="refund") +def admin_refund(amount: int): + print(f"Admin refund executed: {amount}") + + +# ------------------------------------------------------------ +# 7. Calls +# ------------------------------------------------------------ + +print("\nSupport runtime") + +support_refund(amount=200) + +try: + support_refund(amount=5000) +except PermissionError as e: + print(e) + + +print("\nAdmin runtime") + +admin_refund(amount=5000) + +try: + admin_refund(amount=20000) +except PermissionError as e: + print(e) \ No newline at end of file diff --git a/examples/python/core-sdk/policy/policy.yaml b/examples/python/core-sdk/policy/policy.yaml new file mode 100644 index 0000000..cb9dd80 --- /dev/null +++ b/examples/python/core-sdk/policy/policy.yaml @@ -0,0 +1,14 @@ +version: 1 + +rules: + - id: block_large_refund + scope: + action: refund + when: + subject: + domain: action + field: amount + operator: greater_than + value: + literal: 1000 + effect: block \ No newline at end of file diff --git a/examples/python/core-sdk/policy/schema.yaml b/examples/python/core-sdk/policy/schema.yaml new file mode 100644 index 0000000..821c20a --- /dev/null +++ b/examples/python/core-sdk/policy/schema.yaml @@ -0,0 +1,14 @@ +version: 1 + +actions: + refund: + fields: + amount: number + +actor: + fields: + role: string + +snapshot: + fields: + fraud_flag: boolean \ No newline at end of file diff --git a/examples/python/core-sdk/policy_assertion.py b/examples/python/core-sdk/policy_assertion.py new file mode 100644 index 0000000..1e6f2dc --- /dev/null +++ b/examples/python/core-sdk/policy_assertion.py @@ -0,0 +1,18 @@ +""" +Policy Assertion Example + +Demonstrates how to validate policies in tests. +""" + +from actra import Actra + +#Make sure required folder exists +policy = Actra.from_directory("policy") + +policy.assert_effect({ + "action": {"type": "refund", "amount": 200}, + "actor": {"role": "support"}, + "snapshot": {"fraud_flag": False} +}, "allow") + +print("Policy test passed") \ No newline at end of file diff --git a/examples/python/core-sdk/policy_direct_evaluation.py b/examples/python/core-sdk/policy_direct_evaluation.py new file mode 100644 index 0000000..4fe3b31 --- /dev/null +++ b/examples/python/core-sdk/policy_direct_evaluation.py @@ -0,0 +1,51 @@ +""" +Policy Direct Evaluation Example + +Demonstrates how to evaluate policies without using the runtime. +""" + +from actra import Actra + +schema_yaml = """ +version: 1 + +actions: + refund: + fields: + amount: number + +actor: + fields: + role: string + +snapshot: + fields: + fraud_flag: boolean +""" + +policy_yaml = """ +version: 1 + +rules: + - id: block_large_refund + scope: + action: refund + when: + subject: + domain: action + field: amount + operator: greater_than + value: + literal: 1000 + effect: block +""" + +policy = Actra.from_strings(schema_yaml, policy_yaml) + +decision = policy.evaluate({ + "action": {"type": "refund", "amount": 1500}, + "actor": {"role": "support"}, + "snapshot": {"fraud_flag": False} +}) + +print(decision) \ No newline at end of file diff --git a/examples/python/core-sdk/policy_testing.py b/examples/python/core-sdk/policy_testing.py new file mode 100644 index 0000000..7916bb8 --- /dev/null +++ b/examples/python/core-sdk/policy_testing.py @@ -0,0 +1,65 @@ +""" +Policy Testing Example + +Demonstrates how to verify policy behaviour using assertions. +""" + +from actra import Actra + + +schema_yaml = """ +version: 1 + +actions: + refund: + fields: + amount: number + +actor: + fields: + role: string + +snapshot: + fields: + fraud_flag: boolean +""" + + +policy_yaml = """ +version: 1 + +rules: + - id: block_large_refund + scope: + action: refund + when: + subject: + domain: action + field: amount + operator: greater_than + value: + literal: 1000 + effect: block +""" + + +policy = Actra.from_strings(schema_yaml, policy_yaml) + + +allow_context = { + "action": {"type": "refund", "amount": 200}, + "actor": {"role": "support"}, + "snapshot": {"fraud_flag": False}, +} + +block_context = { + "action": {"type": "refund", "amount": 2000}, + "actor": {"role": "support"}, + "snapshot": {"fraud_flag": False}, +} + + +policy.assert_effect(allow_context, "allow") +policy.assert_effect(block_context, "block") + +print("Policy tests passed.") \ No newline at end of file diff --git a/sdk/python/README.md b/sdk/python/README.md index 12c1fe4..a2b3438 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -1,11 +1,21 @@ # Actra Python SDK -Deterministic admission control for state-changing operations in automated and agentic systems. +Deterministic admission control and policy evaluation for state-changing +operations in automated and agentic systems. The **Actra Python SDK** provides a simple interface for loading policies and evaluating decisions using the Actra engine written in Rust. --- +## Runtime Admission Control + +The SDK also provides a runtime layer for protecting functions using Actra policies. + +If a policy blocks the operation, the function will not execute and a +PermissionError will be raised. + +--- + ## Installation Install from PyPI: @@ -22,19 +32,24 @@ The package includes a compiled Rust engine, so no Rust toolchain is required du ```python import actra +from actra import ActraRuntime policy = actra.load_policy_from_file( "schema.yaml", "policy.yaml" ) -decision = policy.evaluate({ - "action": {"type": "deploy"}, - "actor": {"role": "admin"}, - "snapshot": {} -}) +runtime = ActraRuntime(policy) + +runtime.set_actor_resolver(lambda ctx: {"role": "admin"}) +runtime.set_snapshot_resolver(lambda ctx: {}) + +@runtime.admit() +def deploy(): + print("Deployment executed") + +deploy() -print(decision) ``` --- @@ -126,29 +141,6 @@ actra.Actra.compiler_version() --- -## Example - -```python -import actra - -policy = actra.load_policy_from_file( - "schema.yaml", - "policy.yaml" -) - -request = { - "action": {"type": "deploy"}, - "actor": {"role": "developer"}, - "snapshot": {} -} - -decision = policy.evaluate(request) - -print("Decision:", decision) -``` - ---- - ## Design Goals The Python SDK focuses on: diff --git a/sdk/python/actra/__init__.py b/sdk/python/actra/__init__.py index 6b5baa3..2bbc29e 100644 --- a/sdk/python/actra/__init__.py +++ b/sdk/python/actra/__init__.py @@ -1,6 +1,9 @@ -from .policy import Actra, Policy from importlib.metadata import version, PackageNotFoundError +from .policy import Actra, Policy +from .runtime import ActraRuntime +from .types import Action, Actor, Snapshot, Decision, Context + try: __version__ = version("actra") except PackageNotFoundError: @@ -13,8 +16,19 @@ __all__ = [ "Actra", "Policy", + "ActraRuntime", + + # Runtime types + "Action", + "Actor", + "Snapshot", + "Decision", + "Context", + + # Helpers "load_policy_from_file", "load_policy_from_string", "compiler_version", - "__version__" + + "__version__", ] \ No newline at end of file diff --git a/sdk/python/actra/policy.py b/sdk/python/actra/policy.py index 39ab60a..5a79da4 100644 --- a/sdk/python/actra/policy.py +++ b/sdk/python/actra/policy.py @@ -1,19 +1,45 @@ from pathlib import Path -from os import PathLike -from typing import Optional, Dict, Any, Union +from typing import Optional # Rust binding from .actra import PyActra as _RustActra - -ActionInput = Dict[str, Any] -Decision = Dict[str, Any] -PathType = Union[str, PathLike] +from .types import Action, Actor, Snapshot, Decision, ActionInput, PathType class Policy: """ - Compiled policy object. + Compiled Actra policy. + + A `Policy` represents a fully compiled admission control policy produced + by the Actra compiler. It wraps the Rust policy engine and exposes a + Python-friendly runtime API for evaluating decisions. + + A policy evaluates structured input containing three domains: + + action : operation being requested + actor : identity of the caller + snapshot : external system state + + Example context: + + { + "action": {"type": "refund", "amount": 200}, + "actor": {"role": "support"}, + "snapshot": {"fraud_flag": False} + } + + Policies are deterministic and side-effect free. """ def __init__(self, engine: _RustActra): + """ + Internal constructor. + + Users should not instantiate `Policy` directly. + Instead use the `Actra` loader helpers such as: + + Actra.from_strings(...) + Actra.from_files(...) + Actra.from_directory(...) + """ self._engine = engine # ------------------------------------------------------------------ @@ -24,36 +50,203 @@ def evaluate(self, context: ActionInput) -> Decision: """ Evaluate a policy decision. - Expected input shape: - { - "action": {...}, - "actor": {...}, - "snapshot": {...} - } + Args: + context: + Dictionary containing the evaluation context with + the following structure: + { + "action": {...}, + "actor": {...}, + "snapshot": {...} + } + + Returns: + Decision dictionary produced by the policy engine. + Example result: + { + "effect": "allow" + } + or + { + "effect": "block", + "rule_id": "block_large_refund" + } """ - try: - return self._engine.evaluate(context) - except Exception as e: - raise RuntimeError(f"Actra evaluation failed: {e}") from e + return self._engine.evaluate(context) + + def evaluate_action( + self, + action: Action, + actor: Actor, + snapshot: Snapshot, + ) -> Decision: + """ + Evaluate a policy decision using separate domain inputs. + + This is a convenience wrapper around `evaluate()` that allows + callers to provide action, actor, and snapshot independently. + + Args: + action: + Action object describing the requested operation. + + Example: + {"type": "refund", "amount": 200} + + actor: + Actor identity performing the action. + + Example: + {"role": "support"} + + snapshot: + External system state relevant to the decision. + + Example: + {"fraud_flag": False} + + Returns: + Policy decision dictionary. + """ + return self._engine.evaluate({ + "action": action, + "actor": actor, + "snapshot": snapshot + }) + + def explain(self, context: ActionInput) -> Decision: + """ + Evaluate a decision and print a human-readable explanation. + + This method is intended for debugging, experimentation, and + interactive use (for example in notebooks or REPL sessions). + + It prints: + - action input + - actor input + - snapshot input + - final policy decision + Args: + context: + Evaluation context passed to the policy engine. + Returns: + The policy decision dictionary. + """ + + result = self.evaluate(context) + + print("\nActra Decision") + print("-" * 14) + + for section in ["action", "actor", "snapshot"]: + data = context.get(section, {}) + print(f"\n{section.capitalize()}:") + for k, v in data.items(): + print(f" {k}: {v}") + + print("\nResult:") + for k, v in result.items(): + print(f" {k}: {v}") + + return result def policy_hash(self) -> str: """ - Returns deterministic policy hash. + Return a deterministic hash representing the compiled policy. + + The hash uniquely identifies the compiled schema, policy rules. + + This is useful for: + + - policy versioning + - auditing + - caching + - debugging + + Returns: + A stable string hash for the compiled policy. """ return self._engine.policy_hash() def __repr__(self) -> str: - return f"Policy(policy_hash={self.policy_hash()})" + try: + h = self.policy_hash() + except Exception: + h = "unknown" + return f"Policy(hash={h})" + + def __str__(self) -> str: + return self.__repr__() + + def _repr_html_(self): + """ + Rich HTML representation for Jupyter notebooks. + + Displays a compact policy summary including the policy hash. + """ + try: + policy_hash = self.policy_hash() + except Exception: + policy_hash = "unknown" + + return f""" +