From ff6fa4643e7a66c2a775e04f9c9999b6ab44189e Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Fri, 20 Feb 2026 23:11:16 -0800 Subject: [PATCH 1/4] examples --- README.md | 24 +++ examples/README.md | 73 +++++++ examples/basic_worker.py | 198 ++++++++++++++++++ examples/ecommerce_worker.py | 390 +++++++++++++++++++++++++++++++++++ examples/policy.json | 39 ++++ 5 files changed, 724 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/basic_worker.py create mode 100644 examples/ecommerce_worker.py create mode 100644 examples/policy.json diff --git a/README.md b/README.md index 45924a5..5b48326 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,30 @@ Temporal.io Worker Interceptor for Predicate Authority Zero-Trust authorization. This package provides a pre-execution security gate for all Temporal Activities, enforcing cryptographic authorization mandates before any activity code runs. +## Prerequisites + +This package requires the **Predicate Authority Sidecar** daemon to be running. The sidecar is a lightweight Rust binary that handles policy evaluation and mandate signing. + +| Resource | Link | +|----------|------| +| Sidecar Repository | [github.com/PredicateSystems/predicate-authority-sidecar](https://github.com/PredicateSystems/predicate-authority-sidecar) | +| Download Binaries | [Latest Releases](https://github.com/PredicateSystems/predicate-authority-sidecar/releases) | +| License | MIT / Apache 2.0 | + +### Quick Sidecar Setup + +```bash +# Download the latest release for your platform +# Linux x64, macOS x64/ARM64, Windows x64 available + +# Extract and run +tar -xzf predicate-authorityd-*.tar.gz +chmod +x predicate-authorityd + +# Start with a policy file +./predicate-authorityd --port 8787 --policy-file policy.json +``` + ## Installation ```bash diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..42548cc --- /dev/null +++ b/examples/README.md @@ -0,0 +1,73 @@ +# Predicate Temporal Python Examples + +This directory contains examples demonstrating how to use `predicate-temporal` to secure Temporal activities. + +## Prerequisites + +1. Install dependencies: + ```bash + pip install temporalio predicate-authority predicate-temporal + ``` + +2. Start the Predicate Authority daemon: + ```bash + # Download from https://github.com/PredicateSystems/predicate-authority-sidecar/releases + ./predicate-authorityd --port 8787 --policy-file policy.json + ``` + +3. Start a local Temporal server: + ```bash + temporal server start-dev + ``` + +## Examples + +### Basic Example (`basic_worker.py`) + +A minimal example showing: +- Setting up activities with Predicate interceptor +- Running a workflow that executes secured activities +- Handling authorization denials + +Run with: +```bash +python basic_worker.py +``` + +### E-commerce Example (`ecommerce_worker.py`) + +A realistic e-commerce scenario with: +- Order processing activities +- Payment handling +- Inventory management +- Policy-based access control + +Run with: +```bash +python ecommerce_worker.py +``` + +## Policy File + +The `policy.json` file defines which activities are allowed. Example: + +```json +{ + "rules": [ + { + "name": "allow-order-processing", + "effect": "allow", + "principals": ["temporal-worker"], + "actions": ["process_order", "check_inventory", "send_confirmation"], + "resources": ["*"] + }, + { + "name": "deny-admin-actions", + "effect": "deny", + "principals": ["*"], + "actions": ["delete_*", "admin_*"], + "resources": ["*"] + } + ] +} +``` diff --git a/examples/basic_worker.py b/examples/basic_worker.py new file mode 100644 index 0000000..df47796 --- /dev/null +++ b/examples/basic_worker.py @@ -0,0 +1,198 @@ +""" +Basic example demonstrating Predicate Temporal interceptor. + +This example shows: +1. Setting up a Temporal worker with Predicate authorization +2. Defining activities that will be secured +3. Running a workflow that executes those activities + +Prerequisites: +- Temporal server running locally (temporal server start-dev) +- Predicate Authority daemon running (./predicate-authorityd --port 8787 --policy-file policy.json) +""" + +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +from datetime import timedelta + +from temporalio import activity, workflow +from temporalio.client import Client +from temporalio.worker import Worker + +from predicate_authority import AuthorityClient +from predicate_temporal import PredicateInterceptor + + +# ============================================================================ +# Activities - These will be secured by Predicate Authority +# ============================================================================ + + +@activity.defn +async def greet(name: str) -> str: + """Simple greeting activity - allowed by policy.""" + return f"Hello, {name}!" + + +@activity.defn +async def fetch_data(data_id: str) -> dict: + """Fetch data activity - allowed by policy.""" + # Simulate fetching data from a database + return {"id": data_id, "value": "sample_data", "status": "active"} + + +@activity.defn +async def process_data(data: dict) -> dict: + """Process data activity - allowed by policy.""" + # Simulate processing + return {**data, "processed": True, "processed_by": "temporal-worker"} + + +@activity.defn +async def delete_all_records() -> str: + """ + Dangerous activity - DENIED by policy. + + This activity matches the "deny-dangerous-operations" rule + and will be blocked before execution. + """ + # This code will NEVER run due to Predicate authorization + return "All records deleted!" + + +# ============================================================================ +# Workflow +# ============================================================================ + + +@dataclass +class WorkflowInput: + name: str + data_id: str + + +@dataclass +class WorkflowResult: + greeting: str + processed_data: dict + + +@workflow.defn +class BasicWorkflow: + """Basic workflow demonstrating secured activities.""" + + @workflow.run + async def run(self, input: WorkflowInput) -> WorkflowResult: + # This activity will be allowed + greeting = await workflow.execute_activity( + greet, + input.name, + start_to_close_timeout=timedelta(seconds=10), + ) + + # This activity will be allowed + data = await workflow.execute_activity( + fetch_data, + input.data_id, + start_to_close_timeout=timedelta(seconds=10), + ) + + # This activity will be allowed + processed = await workflow.execute_activity( + process_data, + data, + start_to_close_timeout=timedelta(seconds=10), + ) + + return WorkflowResult(greeting=greeting, processed_data=processed) + + +@workflow.defn +class DangerousWorkflow: + """Workflow that attempts a dangerous operation - will be blocked.""" + + @workflow.run + async def run(self) -> str: + # This will raise PermissionError due to Predicate denial + return await workflow.execute_activity( + delete_all_records, + start_to_close_timeout=timedelta(seconds=10), + ) + + +# ============================================================================ +# Main +# ============================================================================ + + +async def main(): + """Run the example worker and execute workflows.""" + + # Connect to Temporal + client = await Client.connect("localhost:7233") + + # Initialize Predicate Authority client + # This connects to the predicate-authorityd daemon + authority_ctx = AuthorityClient.from_policy_file( + policy_file="policy.json", + secret_key="demo-secret-key-for-signing", + ttl_seconds=300, + ) + + # Create the Predicate interceptor + interceptor = PredicateInterceptor( + authority_client=authority_ctx.client, + principal="temporal-worker", + ) + + # Create worker with the interceptor + worker = Worker( + client, + task_queue="predicate-demo-queue", + workflows=[BasicWorkflow, DangerousWorkflow], + activities=[greet, fetch_data, process_data, delete_all_records], + interceptors=[interceptor], + ) + + print("Starting worker with Predicate authorization...") + print("=" * 60) + + # Run worker in background + async with worker: + # Execute the basic workflow - should succeed + print("\n[1] Running BasicWorkflow (should succeed)...") + try: + result = await client.execute_workflow( + BasicWorkflow.run, + WorkflowInput(name="Alice", data_id="data-123"), + id="basic-workflow-1", + task_queue="predicate-demo-queue", + ) + print(f" Greeting: {result.greeting}") + print(f" Processed data: {result.processed_data}") + print(" Status: SUCCESS") + except Exception as e: + print(f" Status: FAILED - {e}") + + # Execute the dangerous workflow - should be blocked + print("\n[2] Running DangerousWorkflow (should be blocked)...") + try: + result = await client.execute_workflow( + DangerousWorkflow.run, + id="dangerous-workflow-1", + task_queue="predicate-demo-queue", + ) + print(f" Result: {result}") + print(" Status: SUCCESS (unexpected!)") + except Exception as e: + print(f" Status: BLOCKED by Predicate Authority") + print(f" Error: {type(e).__name__}: {e}") + + print("\n" + "=" * 60) + print("Demo complete!") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/ecommerce_worker.py b/examples/ecommerce_worker.py new file mode 100644 index 0000000..b61ae06 --- /dev/null +++ b/examples/ecommerce_worker.py @@ -0,0 +1,390 @@ +""" +E-commerce example demonstrating Predicate Temporal interceptor in a realistic scenario. + +This example simulates an order processing system with: +- Inventory management +- Payment processing +- Order confirmation +- Policy-based access control + +Prerequisites: +- Temporal server running locally (temporal server start-dev) +- Predicate Authority daemon running (./predicate-authorityd --port 8787 --policy-file policy.json) +""" + +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +from datetime import timedelta +from typing import List +import random + +from temporalio import activity, workflow +from temporalio.client import Client +from temporalio.worker import Worker +from temporalio.exceptions import ActivityError + +from predicate_authority import AuthorityClient +from predicate_temporal import PredicateInterceptor + + +# ============================================================================ +# Data Models +# ============================================================================ + + +@dataclass +class OrderItem: + product_id: str + quantity: int + price: float + + +@dataclass +class Order: + order_id: str + customer_email: str + items: List[OrderItem] + + @property + def total(self) -> float: + return sum(item.price * item.quantity for item in self.items) + + +@dataclass +class InventoryResult: + available: bool + reserved_items: List[str] + + +@dataclass +class PaymentResult: + success: bool + transaction_id: str + amount: float + + +@dataclass +class OrderResult: + order_id: str + status: str + transaction_id: str | None + confirmation_sent: bool + + +# ============================================================================ +# Activities - Secured by Predicate Authority +# ============================================================================ + + +@activity.defn +async def check_inventory(items: List[dict]) -> dict: + """Check if items are available in inventory.""" + activity.logger.info(f"Checking inventory for {len(items)} items") + + # Simulate inventory check + await asyncio.sleep(0.1) + + return { + "available": True, + "checked_items": [item["product_id"] for item in items], + } + + +@activity.defn +async def reserve_inventory(items: List[dict]) -> dict: + """Reserve items in inventory.""" + activity.logger.info(f"Reserving {len(items)} items") + + # Simulate reservation + await asyncio.sleep(0.1) + + return { + "reserved": True, + "reservation_id": f"res-{random.randint(1000, 9999)}", + } + + +@activity.defn +async def charge_payment(order_id: str, amount: float, email: str) -> dict: + """Process payment for the order.""" + activity.logger.info(f"Charging ${amount:.2f} for order {order_id}") + + # Simulate payment processing + await asyncio.sleep(0.2) + + return { + "success": True, + "transaction_id": f"txn-{random.randint(10000, 99999)}", + "amount": amount, + } + + +@activity.defn +async def refund_payment(transaction_id: str, amount: float) -> dict: + """Refund a payment (used for compensation).""" + activity.logger.info(f"Refunding ${amount:.2f} for transaction {transaction_id}") + + await asyncio.sleep(0.1) + + return { + "refunded": True, + "refund_id": f"ref-{random.randint(10000, 99999)}", + } + + +@activity.defn +async def send_confirmation(email: str, order_id: str, transaction_id: str) -> dict: + """Send order confirmation email.""" + activity.logger.info(f"Sending confirmation to {email} for order {order_id}") + + await asyncio.sleep(0.1) + + return { + "sent": True, + "email": email, + "order_id": order_id, + } + + +@activity.defn +async def process_order(order: dict) -> dict: + """Main order processing activity.""" + activity.logger.info(f"Processing order {order['order_id']}") + + return { + "processed": True, + "order_id": order["order_id"], + } + + +# Dangerous activities - will be BLOCKED by policy + + +@activity.defn +async def delete_order(order_id: str) -> dict: + """Delete an order - BLOCKED by policy.""" + # This will never execute + return {"deleted": True, "order_id": order_id} + + +@activity.defn +async def admin_override_payment(order_id: str) -> dict: + """Admin payment override - BLOCKED by policy.""" + # This will never execute + return {"overridden": True} + + +# ============================================================================ +# Workflows +# ============================================================================ + + +@workflow.defn +class OrderProcessingWorkflow: + """ + Order processing workflow with Predicate authorization. + + All activities are checked against the policy before execution. + """ + + @workflow.run + async def run(self, order: dict) -> dict: + order_id = order["order_id"] + items = order["items"] + email = order["customer_email"] + total = sum(item["price"] * item["quantity"] for item in items) + + workflow.logger.info(f"Starting order processing for {order_id}") + + # Step 1: Check inventory (allowed) + inventory = await workflow.execute_activity( + check_inventory, + items, + start_to_close_timeout=timedelta(seconds=30), + ) + + if not inventory["available"]: + return { + "order_id": order_id, + "status": "failed", + "reason": "inventory_unavailable", + } + + # Step 2: Reserve inventory (allowed) + reservation = await workflow.execute_activity( + reserve_inventory, + items, + start_to_close_timeout=timedelta(seconds=30), + ) + + # Step 3: Process payment (allowed) + payment = await workflow.execute_activity( + charge_payment, + args=[order_id, total, email], + start_to_close_timeout=timedelta(seconds=60), + ) + + if not payment["success"]: + # Compensation would go here + return { + "order_id": order_id, + "status": "failed", + "reason": "payment_failed", + } + + # Step 4: Process order (allowed) + await workflow.execute_activity( + process_order, + order, + start_to_close_timeout=timedelta(seconds=30), + ) + + # Step 5: Send confirmation (allowed) + confirmation = await workflow.execute_activity( + send_confirmation, + args=[email, order_id, payment["transaction_id"]], + start_to_close_timeout=timedelta(seconds=30), + ) + + return { + "order_id": order_id, + "status": "completed", + "transaction_id": payment["transaction_id"], + "confirmation_sent": confirmation["sent"], + } + + +@workflow.defn +class MaliciousWorkflow: + """ + Workflow attempting unauthorized operations. + + These activities will be BLOCKED by Predicate Authority. + """ + + @workflow.run + async def run(self, order_id: str) -> dict: + results = {"attempted": [], "blocked": []} + + # Attempt 1: Try to delete an order (BLOCKED) + try: + await workflow.execute_activity( + delete_order, + order_id, + start_to_close_timeout=timedelta(seconds=10), + ) + results["attempted"].append("delete_order") + except ActivityError: + results["blocked"].append("delete_order") + + # Attempt 2: Try admin override (BLOCKED) + try: + await workflow.execute_activity( + admin_override_payment, + order_id, + start_to_close_timeout=timedelta(seconds=10), + ) + results["attempted"].append("admin_override_payment") + except ActivityError: + results["blocked"].append("admin_override_payment") + + return results + + +# ============================================================================ +# Main +# ============================================================================ + + +async def main(): + """Run the e-commerce demo.""" + + client = await Client.connect("localhost:7233") + + # Initialize Predicate Authority + authority_ctx = AuthorityClient.from_policy_file( + policy_file="policy.json", + secret_key="ecommerce-demo-signing-key", + ttl_seconds=300, + ) + + interceptor = PredicateInterceptor( + authority_client=authority_ctx.client, + principal="temporal-worker", + tenant_id="ecommerce-store", + ) + + worker = Worker( + client, + task_queue="ecommerce-queue", + workflows=[OrderProcessingWorkflow, MaliciousWorkflow], + activities=[ + check_inventory, + reserve_inventory, + charge_payment, + refund_payment, + send_confirmation, + process_order, + delete_order, + admin_override_payment, + ], + interceptors=[interceptor], + ) + + print("=" * 70) + print("E-commerce Order Processing with Predicate Zero-Trust Authorization") + print("=" * 70) + + async with worker: + # Demo 1: Legitimate order processing + print("\n[Demo 1] Processing a legitimate order...") + print("-" * 50) + + order = { + "order_id": "ORD-12345", + "customer_email": "customer@example.com", + "items": [ + {"product_id": "PROD-001", "quantity": 2, "price": 29.99}, + {"product_id": "PROD-002", "quantity": 1, "price": 49.99}, + ], + } + + try: + result = await client.execute_workflow( + OrderProcessingWorkflow.run, + order, + id="order-workflow-1", + task_queue="ecommerce-queue", + ) + print(f" Order ID: {result['order_id']}") + print(f" Status: {result['status']}") + print(f" Transaction: {result.get('transaction_id', 'N/A')}") + print(f" Confirmation sent: {result.get('confirmation_sent', False)}") + except Exception as e: + print(f" Error: {e}") + + # Demo 2: Attempted malicious operations + print("\n[Demo 2] Attempting unauthorized operations...") + print("-" * 50) + + try: + result = await client.execute_workflow( + MaliciousWorkflow.run, + "ORD-12345", + id="malicious-workflow-1", + task_queue="ecommerce-queue", + ) + print(f" Blocked activities: {result['blocked']}") + print(f" (These activities were denied by Predicate Authority)") + except Exception as e: + print(f" Error: {e}") + + print("\n" + "=" * 70) + print("Demo complete! All dangerous operations were blocked.") + print("=" * 70) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/policy.json b/examples/policy.json new file mode 100644 index 0000000..ab7c526 --- /dev/null +++ b/examples/policy.json @@ -0,0 +1,39 @@ +{ + "rules": [ + { + "name": "allow-order-processing", + "effect": "allow", + "principals": ["temporal-worker"], + "actions": ["process_order", "check_inventory", "reserve_inventory", "send_confirmation"], + "resources": ["*"] + }, + { + "name": "allow-payment-processing", + "effect": "allow", + "principals": ["temporal-worker"], + "actions": ["charge_payment", "refund_payment"], + "resources": ["*"] + }, + { + "name": "allow-notifications", + "effect": "allow", + "principals": ["temporal-worker"], + "actions": ["send_email", "send_sms"], + "resources": ["*"] + }, + { + "name": "allow-basic-activities", + "effect": "allow", + "principals": ["temporal-worker"], + "actions": ["greet", "fetch_data", "process_data"], + "resources": ["*"] + }, + { + "name": "deny-dangerous-operations", + "effect": "deny", + "principals": ["*"], + "actions": ["delete_*", "admin_*", "drop_*"], + "resources": ["*"] + } + ] +} From 063a808fdc36ac7d21b0fd4c5259e6933b384972 Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Fri, 20 Feb 2026 23:13:03 -0800 Subject: [PATCH 2/4] fix tests --- .github/workflows/tests.yml | 2 +- pyproject.toml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7a67db6..596a2a7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index 9e4cdb6..98ebb1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "0.1.0" description = "Temporal.io Worker Interceptor for Predicate Authority Zero-Trust authorization" readme = "README.md" license = "MIT" -requires-python = ">=3.10" +requires-python = ">=3.11" authors = [ { name = "Predicate Systems", email = "hello@predicatesystems.dev" } ] @@ -26,9 +26,9 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Security", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", @@ -70,7 +70,7 @@ asyncio_mode = "auto" addopts = "-v --tb=short" [tool.mypy] -python_version = "3.10" +python_version = "3.11" strict = true warn_return_any = true warn_unused_ignores = true @@ -81,7 +81,7 @@ module = ["temporalio.*"] ignore_missing_imports = true [tool.ruff] -target-version = "py310" +target-version = "py311" line-length = 100 src = ["src", "tests"] From f148259793021df799b9a31a4827843625ca30c7 Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Fri, 20 Feb 2026 23:16:58 -0800 Subject: [PATCH 3/4] fix imports --- src/predicate_temporal/interceptor.py | 11 +++++------ tests/test_interceptor.py | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/predicate_temporal/interceptor.py b/src/predicate_temporal/interceptor.py index 1aeaa3a..6bfb52d 100644 --- a/src/predicate_temporal/interceptor.py +++ b/src/predicate_temporal/interceptor.py @@ -10,12 +10,6 @@ import json from typing import Any -from temporalio.worker import ( - ActivityInboundInterceptor, - ExecuteActivityInput, - Interceptor, -) - from predicate_authority import AuthorityClient from predicate_contracts import ( ActionRequest, @@ -24,6 +18,11 @@ StateEvidence, VerificationEvidence, ) +from temporalio.worker import ( + ActivityInboundInterceptor, + ExecuteActivityInput, + Interceptor, +) class PredicateActivityInterceptor(ActivityInboundInterceptor): diff --git a/tests/test_interceptor.py b/tests/test_interceptor.py index 24cc247..f181234 100644 --- a/tests/test_interceptor.py +++ b/tests/test_interceptor.py @@ -135,7 +135,6 @@ async def test_authorization_request_structure( self, interceptor: PredicateActivityInterceptor, mock_authority_client: MagicMock, - mock_next_interceptor: MagicMock, ) -> None: """Test that the authorization request has correct structure.""" mock_authority_client.authorize.return_value = MockAuthorizationDecision(allowed=True) From 965d94a5c183bc844edf0521ab82550de66c8868 Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Fri, 20 Feb 2026 23:22:12 -0800 Subject: [PATCH 4/4] fix mypy errors --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 98ebb1a..f8ec41a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,10 @@ show_error_codes = true module = ["temporalio.*"] ignore_missing_imports = true +[[tool.mypy.overrides]] +module = ["predicate_authority.*", "predicate_contracts.*"] +ignore_missing_imports = true + [tool.ruff] target-version = "py311" line-length = 100