pypredicate-temporal 0.1.0__tar.gz

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.
@@ -0,0 +1,87 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ *.manifest
31
+ *.spec
32
+
33
+ # Installer logs
34
+ pip-log.txt
35
+ pip-delete-this-directory.txt
36
+
37
+ # Unit test / coverage reports
38
+ htmlcov/
39
+ .tox/
40
+ .nox/
41
+ .coverage
42
+ .coverage.*
43
+ .cache
44
+ nosetests.xml
45
+ coverage.xml
46
+ *.cover
47
+ *.py,cover
48
+ .hypothesis/
49
+ .pytest_cache/
50
+ cover/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Environments
57
+ .env
58
+ .venv
59
+ env/
60
+ venv/
61
+ ENV/
62
+ env.bak/
63
+ venv.bak/
64
+
65
+ # pyenv
66
+ .python-version
67
+
68
+ # IDEs
69
+ .idea/
70
+ .vscode/
71
+ *.swp
72
+ *.swo
73
+ *~
74
+
75
+ # mypy
76
+ .mypy_cache/
77
+ .dmypy.json
78
+ dmypy.json
79
+
80
+ # Ruff
81
+ .ruff_cache/
82
+
83
+ # macOS
84
+ .DS_Store
85
+
86
+ # Jupyter
87
+ .ipynb_checkpoints/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Predicate Systems
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: pypredicate-temporal
3
+ Version: 0.1.0
4
+ Summary: Temporal.io Worker Interceptor for Predicate Authority Zero-Trust authorization
5
+ Project-URL: Homepage, https://github.com/PredicateSystems/predicate-temporal-python
6
+ Project-URL: Documentation, https://docs.predicatesystems.dev/integrations/temporal
7
+ Project-URL: Repository, https://github.com/PredicateSystems/predicate-temporal-python
8
+ Project-URL: Issues, https://github.com/PredicateSystems/predicate-temporal-python/issues
9
+ Author-email: Predicate Systems <hello@predicatesystems.dev>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-agents,authorization,predicate-authority,security,temporal,zero-trust
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: predicate-authority>=0.1.0
26
+ Requires-Dist: temporalio>=1.5.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
30
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
31
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
32
+ Requires-Dist: ruff>=0.2.0; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # predicate-temporal
36
+
37
+ Temporal.io Worker Interceptor for Predicate Authority Zero-Trust authorization.
38
+
39
+ This package provides a pre-execution security gate for all Temporal Activities, enforcing cryptographic authorization mandates before any activity code runs.
40
+
41
+ ## Prerequisites
42
+
43
+ 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.
44
+
45
+ | Resource | Link |
46
+ |----------|------|
47
+ | Sidecar Repository | [github.com/PredicateSystems/predicate-authority-sidecar](https://github.com/PredicateSystems/predicate-authority-sidecar) |
48
+ | Download Binaries | [Latest Releases](https://github.com/PredicateSystems/predicate-authority-sidecar/releases) |
49
+ | License | MIT / Apache 2.0 |
50
+
51
+ ### Quick Sidecar Setup
52
+
53
+ ```bash
54
+ # Download the latest release for your platform
55
+ # Linux x64, macOS x64/ARM64, Windows x64 available
56
+
57
+ # Extract and run
58
+ tar -xzf predicate-authorityd-*.tar.gz
59
+ chmod +x predicate-authorityd
60
+
61
+ # Start with a policy file
62
+ ./predicate-authorityd --port 8787 --policy-file policy.json
63
+ ```
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ pip install predicate-temporal
69
+ ```
70
+
71
+ ## Quick Start
72
+
73
+ ```python
74
+ from temporalio.worker import Worker
75
+ from predicate_temporal import PredicateInterceptor
76
+ from predicate_authority import AuthorityClient
77
+
78
+ # Initialize the Predicate Authority client
79
+ ctx = AuthorityClient.from_env()
80
+
81
+ # Create the interceptor
82
+ interceptor = PredicateInterceptor(
83
+ authority_client=ctx.client,
84
+ principal="temporal-worker",
85
+ )
86
+
87
+ # Create worker with the interceptor
88
+ worker = Worker(
89
+ client=temporal_client,
90
+ task_queue="my-task-queue",
91
+ workflows=[MyWorkflow],
92
+ activities=[my_activity],
93
+ interceptors=[interceptor],
94
+ )
95
+ ```
96
+
97
+ ## How It Works
98
+
99
+ The interceptor sits in the Temporal activity execution pipeline:
100
+
101
+ 1. Temporal dispatches an activity to your worker
102
+ 2. **Before** the activity code runs, the interceptor extracts:
103
+ - Activity name (action)
104
+ - Activity arguments (context)
105
+ 3. The interceptor calls `AuthorityClient.authorize()` to request a mandate
106
+ 4. If **denied**: raises `PermissionError` - activity never executes
107
+ 5. If **approved**: activity proceeds normally
108
+
109
+ This ensures that no untrusted code or payload reaches your OS until it has been cryptographically authorized.
110
+
111
+ ## Configuration
112
+
113
+ ### Environment Variables
114
+
115
+ Set these environment variables for the Authority client:
116
+
117
+ ```bash
118
+ export PREDICATE_AUTHORITY_POLICY_FILE=/path/to/policy.json
119
+ export PREDICATE_AUTHORITY_SIGNING_KEY=your-secret-key
120
+ export PREDICATE_AUTHORITY_MANDATE_TTL_SECONDS=300
121
+ ```
122
+
123
+ ### Policy File
124
+
125
+ Create a policy file that defines allowed activities:
126
+
127
+ ```json
128
+ {
129
+ "rules": [
130
+ {
131
+ "name": "allow-safe-activities",
132
+ "effect": "allow",
133
+ "principals": ["temporal-worker"],
134
+ "actions": ["process_order", "send_notification"],
135
+ "resources": ["*"]
136
+ },
137
+ {
138
+ "name": "deny-dangerous-activities",
139
+ "effect": "deny",
140
+ "principals": ["*"],
141
+ "actions": ["delete_*", "admin_*"],
142
+ "resources": ["*"]
143
+ }
144
+ ]
145
+ }
146
+ ```
147
+
148
+ ## API Reference
149
+
150
+ ### PredicateInterceptor
151
+
152
+ ```python
153
+ PredicateInterceptor(
154
+ authority_client: AuthorityClient,
155
+ principal: str = "temporal-worker",
156
+ tenant_id: str | None = None,
157
+ session_id: str | None = None,
158
+ )
159
+ ```
160
+
161
+ **Parameters:**
162
+
163
+ - `authority_client`: The Predicate Authority client instance
164
+ - `principal`: Principal ID used for authorization requests (default: "temporal-worker")
165
+ - `tenant_id`: Optional tenant ID for multi-tenant setups
166
+ - `session_id`: Optional session ID for request correlation
167
+
168
+ ### PredicateActivityInterceptor
169
+
170
+ The inbound interceptor that performs the actual authorization check. Created automatically by `PredicateInterceptor`.
171
+
172
+ ## Error Handling
173
+
174
+ When authorization is denied, the interceptor raises a `PermissionError`:
175
+
176
+ ```python
177
+ try:
178
+ await workflow.execute_activity(
179
+ dangerous_activity,
180
+ args,
181
+ start_to_close_timeout=timedelta(seconds=30),
182
+ )
183
+ except ActivityError as e:
184
+ if isinstance(e.cause, ApplicationError):
185
+ # Handle authorization denial
186
+ print(f"Activity blocked: {e.cause.message}")
187
+ ```
188
+
189
+ ## Development
190
+
191
+ ```bash
192
+ # Install dev dependencies
193
+ pip install -e ".[dev]"
194
+
195
+ # Run tests
196
+ pytest
197
+
198
+ # Type checking
199
+ mypy src
200
+
201
+ # Linting
202
+ ruff check src tests
203
+ ruff format src tests
204
+ ```
205
+
206
+ ## License
207
+
208
+ MIT
@@ -0,0 +1,174 @@
1
+ # predicate-temporal
2
+
3
+ Temporal.io Worker Interceptor for Predicate Authority Zero-Trust authorization.
4
+
5
+ This package provides a pre-execution security gate for all Temporal Activities, enforcing cryptographic authorization mandates before any activity code runs.
6
+
7
+ ## Prerequisites
8
+
9
+ 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.
10
+
11
+ | Resource | Link |
12
+ |----------|------|
13
+ | Sidecar Repository | [github.com/PredicateSystems/predicate-authority-sidecar](https://github.com/PredicateSystems/predicate-authority-sidecar) |
14
+ | Download Binaries | [Latest Releases](https://github.com/PredicateSystems/predicate-authority-sidecar/releases) |
15
+ | License | MIT / Apache 2.0 |
16
+
17
+ ### Quick Sidecar Setup
18
+
19
+ ```bash
20
+ # Download the latest release for your platform
21
+ # Linux x64, macOS x64/ARM64, Windows x64 available
22
+
23
+ # Extract and run
24
+ tar -xzf predicate-authorityd-*.tar.gz
25
+ chmod +x predicate-authorityd
26
+
27
+ # Start with a policy file
28
+ ./predicate-authorityd --port 8787 --policy-file policy.json
29
+ ```
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install predicate-temporal
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from temporalio.worker import Worker
41
+ from predicate_temporal import PredicateInterceptor
42
+ from predicate_authority import AuthorityClient
43
+
44
+ # Initialize the Predicate Authority client
45
+ ctx = AuthorityClient.from_env()
46
+
47
+ # Create the interceptor
48
+ interceptor = PredicateInterceptor(
49
+ authority_client=ctx.client,
50
+ principal="temporal-worker",
51
+ )
52
+
53
+ # Create worker with the interceptor
54
+ worker = Worker(
55
+ client=temporal_client,
56
+ task_queue="my-task-queue",
57
+ workflows=[MyWorkflow],
58
+ activities=[my_activity],
59
+ interceptors=[interceptor],
60
+ )
61
+ ```
62
+
63
+ ## How It Works
64
+
65
+ The interceptor sits in the Temporal activity execution pipeline:
66
+
67
+ 1. Temporal dispatches an activity to your worker
68
+ 2. **Before** the activity code runs, the interceptor extracts:
69
+ - Activity name (action)
70
+ - Activity arguments (context)
71
+ 3. The interceptor calls `AuthorityClient.authorize()` to request a mandate
72
+ 4. If **denied**: raises `PermissionError` - activity never executes
73
+ 5. If **approved**: activity proceeds normally
74
+
75
+ This ensures that no untrusted code or payload reaches your OS until it has been cryptographically authorized.
76
+
77
+ ## Configuration
78
+
79
+ ### Environment Variables
80
+
81
+ Set these environment variables for the Authority client:
82
+
83
+ ```bash
84
+ export PREDICATE_AUTHORITY_POLICY_FILE=/path/to/policy.json
85
+ export PREDICATE_AUTHORITY_SIGNING_KEY=your-secret-key
86
+ export PREDICATE_AUTHORITY_MANDATE_TTL_SECONDS=300
87
+ ```
88
+
89
+ ### Policy File
90
+
91
+ Create a policy file that defines allowed activities:
92
+
93
+ ```json
94
+ {
95
+ "rules": [
96
+ {
97
+ "name": "allow-safe-activities",
98
+ "effect": "allow",
99
+ "principals": ["temporal-worker"],
100
+ "actions": ["process_order", "send_notification"],
101
+ "resources": ["*"]
102
+ },
103
+ {
104
+ "name": "deny-dangerous-activities",
105
+ "effect": "deny",
106
+ "principals": ["*"],
107
+ "actions": ["delete_*", "admin_*"],
108
+ "resources": ["*"]
109
+ }
110
+ ]
111
+ }
112
+ ```
113
+
114
+ ## API Reference
115
+
116
+ ### PredicateInterceptor
117
+
118
+ ```python
119
+ PredicateInterceptor(
120
+ authority_client: AuthorityClient,
121
+ principal: str = "temporal-worker",
122
+ tenant_id: str | None = None,
123
+ session_id: str | None = None,
124
+ )
125
+ ```
126
+
127
+ **Parameters:**
128
+
129
+ - `authority_client`: The Predicate Authority client instance
130
+ - `principal`: Principal ID used for authorization requests (default: "temporal-worker")
131
+ - `tenant_id`: Optional tenant ID for multi-tenant setups
132
+ - `session_id`: Optional session ID for request correlation
133
+
134
+ ### PredicateActivityInterceptor
135
+
136
+ The inbound interceptor that performs the actual authorization check. Created automatically by `PredicateInterceptor`.
137
+
138
+ ## Error Handling
139
+
140
+ When authorization is denied, the interceptor raises a `PermissionError`:
141
+
142
+ ```python
143
+ try:
144
+ await workflow.execute_activity(
145
+ dangerous_activity,
146
+ args,
147
+ start_to_close_timeout=timedelta(seconds=30),
148
+ )
149
+ except ActivityError as e:
150
+ if isinstance(e.cause, ApplicationError):
151
+ # Handle authorization denial
152
+ print(f"Activity blocked: {e.cause.message}")
153
+ ```
154
+
155
+ ## Development
156
+
157
+ ```bash
158
+ # Install dev dependencies
159
+ pip install -e ".[dev]"
160
+
161
+ # Run tests
162
+ pytest
163
+
164
+ # Type checking
165
+ mypy src
166
+
167
+ # Linting
168
+ ruff check src tests
169
+ ruff format src tests
170
+ ```
171
+
172
+ ## License
173
+
174
+ MIT
@@ -0,0 +1,73 @@
1
+ # Predicate Temporal Python Examples
2
+
3
+ This directory contains examples demonstrating how to use `predicate-temporal` to secure Temporal activities.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. Install dependencies:
8
+ ```bash
9
+ pip install temporalio predicate-authority predicate-temporal
10
+ ```
11
+
12
+ 2. Start the Predicate Authority daemon:
13
+ ```bash
14
+ # Download from https://github.com/PredicateSystems/predicate-authority-sidecar/releases
15
+ ./predicate-authorityd --port 8787 --policy-file policy.json
16
+ ```
17
+
18
+ 3. Start a local Temporal server:
19
+ ```bash
20
+ temporal server start-dev
21
+ ```
22
+
23
+ ## Examples
24
+
25
+ ### Basic Example (`basic_worker.py`)
26
+
27
+ A minimal example showing:
28
+ - Setting up activities with Predicate interceptor
29
+ - Running a workflow that executes secured activities
30
+ - Handling authorization denials
31
+
32
+ Run with:
33
+ ```bash
34
+ python basic_worker.py
35
+ ```
36
+
37
+ ### E-commerce Example (`ecommerce_worker.py`)
38
+
39
+ A realistic e-commerce scenario with:
40
+ - Order processing activities
41
+ - Payment handling
42
+ - Inventory management
43
+ - Policy-based access control
44
+
45
+ Run with:
46
+ ```bash
47
+ python ecommerce_worker.py
48
+ ```
49
+
50
+ ## Policy File
51
+
52
+ The `policy.json` file defines which activities are allowed. Example:
53
+
54
+ ```json
55
+ {
56
+ "rules": [
57
+ {
58
+ "name": "allow-order-processing",
59
+ "effect": "allow",
60
+ "principals": ["temporal-worker"],
61
+ "actions": ["process_order", "check_inventory", "send_confirmation"],
62
+ "resources": ["*"]
63
+ },
64
+ {
65
+ "name": "deny-admin-actions",
66
+ "effect": "deny",
67
+ "principals": ["*"],
68
+ "actions": ["delete_*", "admin_*"],
69
+ "resources": ["*"]
70
+ }
71
+ ]
72
+ }
73
+ ```
@@ -0,0 +1,120 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pypredicate-temporal"
7
+ version = "0.1.0"
8
+ description = "Temporal.io Worker Interceptor for Predicate Authority Zero-Trust authorization"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [
13
+ { name = "Predicate Systems", email = "hello@predicatesystems.dev" }
14
+ ]
15
+ keywords = [
16
+ "temporal",
17
+ "authorization",
18
+ "zero-trust",
19
+ "security",
20
+ "ai-agents",
21
+ "predicate-authority"
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Intended Audience :: Developers",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Operating System :: OS Independent",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Topic :: Security",
33
+ "Topic :: Software Development :: Libraries :: Python Modules",
34
+ "Typing :: Typed",
35
+ ]
36
+ dependencies = [
37
+ "temporalio>=1.5.0",
38
+ "predicate-authority>=0.1.0",
39
+ ]
40
+
41
+ [project.optional-dependencies]
42
+ dev = [
43
+ "pytest>=8.0.0",
44
+ "pytest-asyncio>=0.23.0",
45
+ "pytest-cov>=4.1.0",
46
+ "mypy>=1.8.0",
47
+ "ruff>=0.2.0",
48
+ ]
49
+
50
+ [project.urls]
51
+ Homepage = "https://github.com/PredicateSystems/predicate-temporal-python"
52
+ Documentation = "https://docs.predicatesystems.dev/integrations/temporal"
53
+ Repository = "https://github.com/PredicateSystems/predicate-temporal-python"
54
+ Issues = "https://github.com/PredicateSystems/predicate-temporal-python/issues"
55
+
56
+ [tool.hatch.build.targets.wheel]
57
+ packages = ["src/predicate_temporal"]
58
+
59
+ [tool.hatch.build.targets.sdist]
60
+ include = [
61
+ "/src",
62
+ "/tests",
63
+ "README.md",
64
+ "LICENSE",
65
+ ]
66
+
67
+ [tool.pytest.ini_options]
68
+ testpaths = ["tests"]
69
+ asyncio_mode = "auto"
70
+ addopts = "-v --tb=short"
71
+
72
+ [tool.mypy]
73
+ python_version = "3.11"
74
+ strict = true
75
+ warn_return_any = true
76
+ warn_unused_ignores = true
77
+ show_error_codes = true
78
+
79
+ [[tool.mypy.overrides]]
80
+ module = ["temporalio.*"]
81
+ ignore_missing_imports = true
82
+
83
+ [[tool.mypy.overrides]]
84
+ module = ["predicate_authority.*", "predicate_contracts.*"]
85
+ ignore_missing_imports = true
86
+
87
+ [tool.ruff]
88
+ target-version = "py311"
89
+ line-length = 100
90
+ src = ["src", "tests"]
91
+
92
+ [tool.ruff.lint]
93
+ select = [
94
+ "E", # pycodestyle errors
95
+ "W", # pycodestyle warnings
96
+ "F", # Pyflakes
97
+ "I", # isort
98
+ "B", # flake8-bugbear
99
+ "C4", # flake8-comprehensions
100
+ "UP", # pyupgrade
101
+ "ARG", # flake8-unused-arguments
102
+ "SIM", # flake8-simplify
103
+ ]
104
+ ignore = [
105
+ "E501", # line too long (handled by formatter)
106
+ ]
107
+
108
+ [tool.ruff.lint.isort]
109
+ known-first-party = ["predicate_temporal"]
110
+
111
+ [tool.coverage.run]
112
+ source = ["src/predicate_temporal"]
113
+ branch = true
114
+
115
+ [tool.coverage.report]
116
+ exclude_lines = [
117
+ "pragma: no cover",
118
+ "if TYPE_CHECKING:",
119
+ "raise NotImplementedError",
120
+ ]
@@ -0,0 +1,13 @@
1
+ """Temporal.io Worker Interceptor for Predicate Authority Zero-Trust authorization."""
2
+
3
+ from predicate_temporal.interceptor import (
4
+ PredicateActivityInterceptor,
5
+ PredicateInterceptor,
6
+ )
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = [
11
+ "PredicateActivityInterceptor",
12
+ "PredicateInterceptor",
13
+ ]
@@ -0,0 +1,196 @@
1
+ """Predicate Authority interceptor for Temporal.io activities.
2
+
3
+ This module provides a pre-execution security gate for all Temporal Activities,
4
+ enforcing cryptographic authorization mandates before any activity code runs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ from typing import Any
12
+
13
+ from predicate_authority import AuthorityClient
14
+ from predicate_contracts import (
15
+ ActionRequest,
16
+ ActionSpec,
17
+ PrincipalRef,
18
+ StateEvidence,
19
+ VerificationEvidence,
20
+ )
21
+ from temporalio.worker import (
22
+ ActivityInboundInterceptor,
23
+ ExecuteActivityInput,
24
+ Interceptor,
25
+ )
26
+
27
+
28
+ class PredicateActivityInterceptor(ActivityInboundInterceptor):
29
+ """Inbound interceptor that enforces Predicate Authority authorization for activities.
30
+
31
+ This interceptor sits in the Temporal activity execution pipeline and ensures
32
+ that every activity is authorized before execution. If authorization is denied,
33
+ a PermissionError is raised and the activity never executes.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ next_interceptor: ActivityInboundInterceptor,
39
+ authority_client: AuthorityClient,
40
+ principal: str,
41
+ tenant_id: str | None = None,
42
+ session_id: str | None = None,
43
+ ) -> None:
44
+ """Initialize the activity interceptor.
45
+
46
+ Args:
47
+ next_interceptor: The next interceptor in the chain.
48
+ authority_client: The Predicate Authority client for authorization.
49
+ principal: Principal ID used for authorization requests.
50
+ tenant_id: Optional tenant ID for multi-tenant setups.
51
+ session_id: Optional session ID for request correlation.
52
+ """
53
+ super().__init__(next_interceptor)
54
+ self._authority_client = authority_client
55
+ self._principal = principal
56
+ self._tenant_id = tenant_id
57
+ self._session_id = session_id
58
+
59
+ async def execute_activity(self, input: ExecuteActivityInput) -> Any:
60
+ """Execute activity with Predicate Authority authorization check.
61
+
62
+ This method intercepts the activity execution, extracts the activity name
63
+ and arguments, and requests authorization from Predicate Authority.
64
+ If denied, raises PermissionError. If approved, proceeds with execution.
65
+
66
+ Args:
67
+ input: The activity execution input containing activity name and args.
68
+
69
+ Returns:
70
+ The result of the activity execution.
71
+
72
+ Raises:
73
+ PermissionError: If authorization is denied.
74
+ """
75
+ activity_name = input.fn.__name__
76
+ activity_args = input.args
77
+
78
+ args_json = json.dumps(
79
+ [self._serialize_arg(arg) for arg in activity_args],
80
+ sort_keys=True,
81
+ default=str,
82
+ )
83
+ args_hash = hashlib.sha256(args_json.encode()).hexdigest()
84
+
85
+ request = ActionRequest(
86
+ principal=PrincipalRef(
87
+ principal_id=self._principal,
88
+ tenant_id=self._tenant_id,
89
+ session_id=self._session_id,
90
+ ),
91
+ action_spec=ActionSpec(
92
+ action=activity_name,
93
+ resource="temporal:activity",
94
+ intent=f"execute:{activity_name}",
95
+ ),
96
+ state_evidence=StateEvidence(
97
+ source="temporal-worker",
98
+ state_hash=args_hash,
99
+ schema_version="v1",
100
+ ),
101
+ verification_evidence=VerificationEvidence(signals=()),
102
+ )
103
+
104
+ decision = self._authority_client.authorize(request)
105
+
106
+ if not decision.allowed:
107
+ raise PermissionError(
108
+ f"Predicate Zero-Trust Denial: Activity '{activity_name}' not authorized. "
109
+ f"Reason: {decision.reason.value}"
110
+ + (f", violated rule: {decision.violated_rule}" if decision.violated_rule else "")
111
+ )
112
+
113
+ return await super().execute_activity(input)
114
+
115
+ @staticmethod
116
+ def _serialize_arg(arg: Any) -> Any:
117
+ """Serialize an argument for hashing.
118
+
119
+ Args:
120
+ arg: The argument to serialize.
121
+
122
+ Returns:
123
+ A JSON-serializable representation of the argument.
124
+ """
125
+ if hasattr(arg, "__dict__"):
126
+ return {k: v for k, v in arg.__dict__.items() if not k.startswith("_")}
127
+ return arg
128
+
129
+
130
+ class PredicateInterceptor(Interceptor):
131
+ """Top-level Temporal interceptor that injects Predicate Authority authorization.
132
+
133
+ Use this interceptor when creating a Temporal Worker to enforce Zero-Trust
134
+ authorization for all activities.
135
+
136
+ Example:
137
+ ```python
138
+ from temporalio.worker import Worker
139
+ from predicate_temporal import PredicateInterceptor
140
+ from predicate_authority import AuthorityClient
141
+
142
+ ctx = AuthorityClient.from_env()
143
+ interceptor = PredicateInterceptor(
144
+ authority_client=ctx.client,
145
+ principal="temporal-worker",
146
+ )
147
+
148
+ worker = Worker(
149
+ client=temporal_client,
150
+ task_queue="my-task-queue",
151
+ workflows=[MyWorkflow],
152
+ activities=[my_activity],
153
+ interceptors=[interceptor],
154
+ )
155
+ ```
156
+ """
157
+
158
+ def __init__(
159
+ self,
160
+ authority_client: AuthorityClient,
161
+ principal: str = "temporal-worker",
162
+ tenant_id: str | None = None,
163
+ session_id: str | None = None,
164
+ ) -> None:
165
+ """Initialize the Predicate interceptor.
166
+
167
+ Args:
168
+ authority_client: The Predicate Authority client for authorization.
169
+ principal: Principal ID used for authorization requests (default: "temporal-worker").
170
+ tenant_id: Optional tenant ID for multi-tenant setups.
171
+ session_id: Optional session ID for request correlation.
172
+ """
173
+ self._authority_client = authority_client
174
+ self._principal = principal
175
+ self._tenant_id = tenant_id
176
+ self._session_id = session_id
177
+
178
+ def intercept_activity(
179
+ self,
180
+ next_interceptor: ActivityInboundInterceptor,
181
+ ) -> ActivityInboundInterceptor:
182
+ """Inject the Predicate activity interceptor into the pipeline.
183
+
184
+ Args:
185
+ next_interceptor: The next interceptor in the chain.
186
+
187
+ Returns:
188
+ The PredicateActivityInterceptor wrapping the next interceptor.
189
+ """
190
+ return PredicateActivityInterceptor(
191
+ next_interceptor=next_interceptor,
192
+ authority_client=self._authority_client,
193
+ principal=self._principal,
194
+ tenant_id=self._tenant_id,
195
+ session_id=self._session_id,
196
+ )
File without changes
@@ -0,0 +1,251 @@
1
+ """Tests for the Predicate Temporal interceptor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+ from unittest.mock import AsyncMock, MagicMock
8
+
9
+ import pytest
10
+
11
+ from predicate_temporal.interceptor import (
12
+ PredicateActivityInterceptor,
13
+ PredicateInterceptor,
14
+ )
15
+
16
+
17
+ @dataclass
18
+ class MockActivityInput:
19
+ """Mock for ExecuteActivityInput."""
20
+
21
+ fn: Any
22
+ args: tuple[Any, ...]
23
+
24
+
25
+ def mock_activity_function(x: int, y: str) -> str:
26
+ """Mock activity function for testing."""
27
+ return f"{x}-{y}"
28
+
29
+
30
+ class MockAuthorizationDecision:
31
+ """Mock authorization decision."""
32
+
33
+ def __init__(self, allowed: bool, reason: str = "allowed", violated_rule: str | None = None):
34
+ self.allowed = allowed
35
+ self.reason = MagicMock(value=reason)
36
+ self.violated_rule = violated_rule
37
+ self.mandate = MagicMock() if allowed else None
38
+
39
+
40
+ class TestPredicateActivityInterceptor:
41
+ """Tests for PredicateActivityInterceptor."""
42
+
43
+ @pytest.fixture
44
+ def mock_authority_client(self) -> MagicMock:
45
+ """Create a mock authority client."""
46
+ return MagicMock()
47
+
48
+ @pytest.fixture
49
+ def mock_next_interceptor(self) -> MagicMock:
50
+ """Create a mock next interceptor."""
51
+ interceptor = MagicMock()
52
+ interceptor.execute_activity = AsyncMock(return_value="activity_result")
53
+ return interceptor
54
+
55
+ @pytest.fixture
56
+ def interceptor(
57
+ self, mock_next_interceptor: MagicMock, mock_authority_client: MagicMock
58
+ ) -> PredicateActivityInterceptor:
59
+ """Create the interceptor under test."""
60
+ return PredicateActivityInterceptor(
61
+ next_interceptor=mock_next_interceptor,
62
+ authority_client=mock_authority_client,
63
+ principal="test-worker",
64
+ tenant_id="test-tenant",
65
+ session_id="test-session",
66
+ )
67
+
68
+ @pytest.mark.asyncio
69
+ async def test_execute_activity_allowed(
70
+ self,
71
+ interceptor: PredicateActivityInterceptor,
72
+ mock_authority_client: MagicMock,
73
+ mock_next_interceptor: MagicMock,
74
+ ) -> None:
75
+ """Test that allowed activities proceed to execution."""
76
+ mock_authority_client.authorize.return_value = MockAuthorizationDecision(allowed=True)
77
+
78
+ input_data = MockActivityInput(fn=mock_activity_function, args=(42, "hello"))
79
+
80
+ result = await interceptor.execute_activity(input_data) # type: ignore[arg-type]
81
+
82
+ assert result == "activity_result"
83
+ mock_authority_client.authorize.assert_called_once()
84
+ mock_next_interceptor.execute_activity.assert_called_once_with(input_data)
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_execute_activity_denied(
88
+ self,
89
+ interceptor: PredicateActivityInterceptor,
90
+ mock_authority_client: MagicMock,
91
+ mock_next_interceptor: MagicMock,
92
+ ) -> None:
93
+ """Test that denied activities raise PermissionError."""
94
+ mock_authority_client.authorize.return_value = MockAuthorizationDecision(
95
+ allowed=False,
96
+ reason="explicit_deny",
97
+ violated_rule="deny-dangerous",
98
+ )
99
+
100
+ input_data = MockActivityInput(fn=mock_activity_function, args=(42, "hello"))
101
+
102
+ with pytest.raises(PermissionError) as exc_info:
103
+ await interceptor.execute_activity(input_data) # type: ignore[arg-type]
104
+
105
+ assert "Predicate Zero-Trust Denial" in str(exc_info.value)
106
+ assert "mock_activity_function" in str(exc_info.value)
107
+ assert "explicit_deny" in str(exc_info.value)
108
+ assert "deny-dangerous" in str(exc_info.value)
109
+
110
+ mock_next_interceptor.execute_activity.assert_not_called()
111
+
112
+ @pytest.mark.asyncio
113
+ async def test_execute_activity_denied_no_violated_rule(
114
+ self,
115
+ interceptor: PredicateActivityInterceptor,
116
+ mock_authority_client: MagicMock,
117
+ ) -> None:
118
+ """Test denial message without violated rule."""
119
+ mock_authority_client.authorize.return_value = MockAuthorizationDecision(
120
+ allowed=False,
121
+ reason="no_matching_policy",
122
+ violated_rule=None,
123
+ )
124
+
125
+ input_data = MockActivityInput(fn=mock_activity_function, args=(42, "hello"))
126
+
127
+ with pytest.raises(PermissionError) as exc_info:
128
+ await interceptor.execute_activity(input_data) # type: ignore[arg-type]
129
+
130
+ assert "no_matching_policy" in str(exc_info.value)
131
+ assert "violated rule" not in str(exc_info.value)
132
+
133
+ @pytest.mark.asyncio
134
+ async def test_authorization_request_structure(
135
+ self,
136
+ interceptor: PredicateActivityInterceptor,
137
+ mock_authority_client: MagicMock,
138
+ ) -> None:
139
+ """Test that the authorization request has correct structure."""
140
+ mock_authority_client.authorize.return_value = MockAuthorizationDecision(allowed=True)
141
+
142
+ input_data = MockActivityInput(fn=mock_activity_function, args=(42, "hello"))
143
+
144
+ await interceptor.execute_activity(input_data) # type: ignore[arg-type]
145
+
146
+ call_args = mock_authority_client.authorize.call_args
147
+ request = call_args[0][0]
148
+
149
+ assert request.principal.principal_id == "test-worker"
150
+ assert request.principal.tenant_id == "test-tenant"
151
+ assert request.principal.session_id == "test-session"
152
+ assert request.action_spec.action == "mock_activity_function"
153
+ assert request.action_spec.resource == "temporal:activity"
154
+ assert "execute:mock_activity_function" in request.action_spec.intent
155
+ assert request.state_evidence.source == "temporal-worker"
156
+ assert request.state_evidence.state_hash # Non-empty hash
157
+
158
+ def test_serialize_arg_primitive(self) -> None:
159
+ """Test serialization of primitive types."""
160
+ assert PredicateActivityInterceptor._serialize_arg(42) == 42
161
+ assert PredicateActivityInterceptor._serialize_arg("hello") == "hello"
162
+ assert PredicateActivityInterceptor._serialize_arg(True) is True
163
+ assert PredicateActivityInterceptor._serialize_arg(None) is None
164
+
165
+ def test_serialize_arg_object(self) -> None:
166
+ """Test serialization of objects with __dict__."""
167
+
168
+ @dataclass
169
+ class TestObject:
170
+ name: str
171
+ value: int
172
+ _private: str = "hidden"
173
+
174
+ obj = TestObject(name="test", value=123, _private="secret")
175
+ serialized = PredicateActivityInterceptor._serialize_arg(obj)
176
+
177
+ assert serialized == {"name": "test", "value": 123}
178
+ assert "_private" not in serialized
179
+
180
+
181
+ class TestPredicateInterceptor:
182
+ """Tests for PredicateInterceptor."""
183
+
184
+ @pytest.fixture
185
+ def mock_authority_client(self) -> MagicMock:
186
+ """Create a mock authority client."""
187
+ return MagicMock()
188
+
189
+ def test_interceptor_creation(self, mock_authority_client: MagicMock) -> None:
190
+ """Test interceptor creation with all parameters."""
191
+ interceptor = PredicateInterceptor(
192
+ authority_client=mock_authority_client,
193
+ principal="custom-worker",
194
+ tenant_id="tenant-123",
195
+ session_id="session-456",
196
+ )
197
+
198
+ assert interceptor._authority_client == mock_authority_client
199
+ assert interceptor._principal == "custom-worker"
200
+ assert interceptor._tenant_id == "tenant-123"
201
+ assert interceptor._session_id == "session-456"
202
+
203
+ def test_interceptor_defaults(self, mock_authority_client: MagicMock) -> None:
204
+ """Test interceptor creation with default parameters."""
205
+ interceptor = PredicateInterceptor(authority_client=mock_authority_client)
206
+
207
+ assert interceptor._principal == "temporal-worker"
208
+ assert interceptor._tenant_id is None
209
+ assert interceptor._session_id is None
210
+
211
+ def test_intercept_activity(self, mock_authority_client: MagicMock) -> None:
212
+ """Test that intercept_activity returns PredicateActivityInterceptor."""
213
+ interceptor = PredicateInterceptor(
214
+ authority_client=mock_authority_client,
215
+ principal="test-worker",
216
+ tenant_id="tenant-123",
217
+ )
218
+
219
+ mock_next = MagicMock()
220
+ result = interceptor.intercept_activity(mock_next)
221
+
222
+ assert isinstance(result, PredicateActivityInterceptor)
223
+ assert result._authority_client == mock_authority_client
224
+ assert result._principal == "test-worker"
225
+ assert result._tenant_id == "tenant-123"
226
+
227
+
228
+ class TestIntegration:
229
+ """Integration-style tests."""
230
+
231
+ @pytest.mark.asyncio
232
+ async def test_full_interceptor_chain(self) -> None:
233
+ """Test the full interceptor chain from top-level to activity execution."""
234
+ mock_client = MagicMock()
235
+ mock_client.authorize.return_value = MockAuthorizationDecision(allowed=True)
236
+
237
+ interceptor = PredicateInterceptor(
238
+ authority_client=mock_client,
239
+ principal="integration-worker",
240
+ )
241
+
242
+ mock_next = MagicMock()
243
+ mock_next.execute_activity = AsyncMock(return_value="success")
244
+
245
+ activity_interceptor = interceptor.intercept_activity(mock_next)
246
+
247
+ input_data = MockActivityInput(fn=mock_activity_function, args=("arg1",))
248
+ result = await activity_interceptor.execute_activity(input_data) # type: ignore[arg-type]
249
+
250
+ assert result == "success"
251
+ mock_client.authorize.assert_called_once()