openbox-temporal-sdk-python 1.1.0__tar.gz → 1.1.1__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.
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/PKG-INFO +58 -11
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/README.md +55 -6
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/codebase-summary.md +1 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/system-architecture.md +33 -0
- openbox_temporal_sdk_python-1.1.1/docs/temporal-plugin-integration-guide.md +147 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/__init__.py +13 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/activities.py +78 -75
- openbox_temporal_sdk_python-1.1.1/openbox/activity_interceptor.py +676 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/client.py +11 -4
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/config.py +13 -7
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/context_propagation.py +4 -1
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/db_governance_hooks.py +239 -342
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/errors.py +18 -3
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/file_governance_hooks.py +67 -25
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/hitl.py +5 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/hook_governance.py +68 -17
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/http_governance_hooks.py +193 -83
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/otel_setup.py +104 -117
- openbox_temporal_sdk_python-1.1.1/openbox/plugin.py +185 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/span_processor.py +40 -12
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/tracing.py +83 -106
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/types.py +17 -5
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/verdict_handler.py +17 -7
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/worker.py +13 -4
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/workflow_interceptor.py +146 -83
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/pyproject.toml +5 -7
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/conftest.py +9 -4
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_activities.py +41 -15
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_activity_interceptor.py +410 -195
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_config.py +16 -7
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_db_governance_hooks.py +149 -71
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_file_governance_hooks.py +111 -43
- openbox_temporal_sdk_python-1.1.1/tests/test_http_body_truncation.py +75 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_otel_setup.py +372 -175
- openbox_temporal_sdk_python-1.1.1/tests/test_plugin.py +263 -0
- openbox_temporal_sdk_python-1.1.1/tests/test_plugin_integration.py +280 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_psycopg2_hooks_verify.py +10 -3
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_span_processor.py +22 -9
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_tracing.py +54 -17
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_worker.py +13 -5
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_workflow_interceptor.py +92 -42
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/uv.lock +96 -608
- openbox_temporal_sdk_python-1.1.0/openbox/activity_interceptor.py +0 -626
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/.github/workflows/publish.yml +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/.github/workflows/sonarqube.yaml +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/.gitignore +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/.python-version +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/.repomixignore +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/CHANGELOG.md +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/LICENSE +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/changelog-hook-level-governance.md +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/code-standards.md +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/configuration.md +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/project-overview-pdr.md +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/py.typed +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/release-manifest.json +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/repomix-output.xml +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/sonar-project.properties +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/__init__.py +0 -0
- {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbox-temporal-sdk-python
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: OpenBox SDK - Governance and observability for Temporal workflows
|
|
5
5
|
Project-URL: Homepage, https://github.com/OpenBox-AI/temporal-sdk-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/OpenBox-AI/temporal-sdk-python#readme
|
|
@@ -15,14 +15,12 @@ Classifier: Development Status :: 3 - Alpha
|
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
21
|
Classifier: Topic :: System :: Monitoring
|
|
24
22
|
Classifier: Typing :: Typed
|
|
25
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.11
|
|
26
24
|
Requires-Dist: asyncpg>=0.29.0
|
|
27
25
|
Requires-Dist: httpx<1,>=0.28.0
|
|
28
26
|
Requires-Dist: mysql-connector-python>=8.0.0
|
|
@@ -45,7 +43,7 @@ Requires-Dist: pymongo>=4.0.0
|
|
|
45
43
|
Requires-Dist: pymysql>=1.0.0
|
|
46
44
|
Requires-Dist: redis>=5.0.0
|
|
47
45
|
Requires-Dist: sqlalchemy>=2.0.0
|
|
48
|
-
Requires-Dist: temporalio<2,>=1.
|
|
46
|
+
Requires-Dist: temporalio<2,>=1.23.0
|
|
49
47
|
Description-Content-Type: text/markdown
|
|
50
48
|
|
|
51
49
|
# OpenBox SDK for Temporal Workflows
|
|
@@ -70,13 +68,62 @@ pip install openbox-temporal-sdk-python
|
|
|
70
68
|
```
|
|
71
69
|
|
|
72
70
|
**Requirements:**
|
|
73
|
-
- Python 3.
|
|
74
|
-
- Temporal SDK 1.8+
|
|
71
|
+
- Python 3.11+
|
|
72
|
+
- Temporal SDK 1.23+ (1.8+ for factory-only usage)
|
|
75
73
|
- OpenTelemetry API/SDK 1.38.0+
|
|
76
74
|
|
|
77
75
|
---
|
|
78
76
|
|
|
79
|
-
##
|
|
77
|
+
## Plugin Integration (Recommended)
|
|
78
|
+
|
|
79
|
+
Use `OpenBoxPlugin` for drop-in integration with Temporal Workers:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import os
|
|
83
|
+
from temporalio.worker import Worker
|
|
84
|
+
from openbox.plugin import OpenBoxPlugin
|
|
85
|
+
|
|
86
|
+
worker = Worker(
|
|
87
|
+
client,
|
|
88
|
+
task_queue="my-task-queue",
|
|
89
|
+
workflows=[MyWorkflow],
|
|
90
|
+
activities=[my_activity],
|
|
91
|
+
plugins=[
|
|
92
|
+
OpenBoxPlugin(
|
|
93
|
+
openbox_url=os.getenv("OPENBOX_URL"),
|
|
94
|
+
openbox_api_key=os.getenv("OPENBOX_API_KEY"),
|
|
95
|
+
)
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
await worker.run()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The plugin automatically configures governance interceptors, OTel instrumentation,
|
|
103
|
+
sandbox passthrough, and the `send_governance_event` activity.
|
|
104
|
+
|
|
105
|
+
### Composing with Other Plugins
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from temporalio.contrib.opentelemetry import OpenTelemetryPlugin
|
|
109
|
+
|
|
110
|
+
worker = Worker(
|
|
111
|
+
client,
|
|
112
|
+
task_queue="my-task-queue",
|
|
113
|
+
workflows=[MyWorkflow],
|
|
114
|
+
activities=[my_activity],
|
|
115
|
+
plugins=[
|
|
116
|
+
OpenTelemetryPlugin(),
|
|
117
|
+
OpenBoxPlugin(openbox_url=..., openbox_api_key=...),
|
|
118
|
+
],
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
> **Requires** `temporalio >= 1.23.0`. For older versions, use `create_openbox_worker()` below.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Quick Start (Factory)
|
|
80
127
|
|
|
81
128
|
Use the `create_openbox_worker()` factory for simple integration:
|
|
82
129
|
|
|
@@ -489,13 +536,13 @@ worker = Worker(
|
|
|
489
536
|
|
|
490
537
|
## Testing
|
|
491
538
|
|
|
492
|
-
The SDK includes comprehensive test coverage with
|
|
539
|
+
The SDK includes comprehensive test coverage with 15 test files:
|
|
493
540
|
|
|
494
541
|
```bash
|
|
495
542
|
pytest tests/
|
|
496
543
|
```
|
|
497
544
|
|
|
498
|
-
Test files: `test_activities.py`, `test_activity_interceptor.py`, `test_config.py`, `test_db_governance_hooks.py`, `test_file_governance_hooks.py`, `test_otel_hook_pause.py`, `test_otel_hook_pause_db.py`, `test_otel_setup.py`, `test_span_processor.py`, `test_tracing.py`, `test_types.py`, `test_worker.py`, `test_workflow_interceptor.py`
|
|
545
|
+
Test files: `test_activities.py`, `test_activity_interceptor.py`, `test_config.py`, `test_db_governance_hooks.py`, `test_file_governance_hooks.py`, `test_otel_hook_pause.py`, `test_otel_hook_pause_db.py`, `test_otel_setup.py`, `test_plugin.py`, `test_plugin_integration.py`, `test_span_processor.py`, `test_tracing.py`, `test_types.py`, `test_worker.py`, `test_workflow_interceptor.py`
|
|
499
546
|
|
|
500
547
|
---
|
|
501
548
|
|
|
@@ -512,4 +559,4 @@ MIT License - See LICENSE file for details
|
|
|
512
559
|
|
|
513
560
|
---
|
|
514
561
|
|
|
515
|
-
**Version:** 1.
|
|
562
|
+
**Version:** 1.2.0 | **Last Updated:** 2026-04-05
|
|
@@ -20,13 +20,62 @@ pip install openbox-temporal-sdk-python
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
**Requirements:**
|
|
23
|
-
- Python 3.
|
|
24
|
-
- Temporal SDK 1.8+
|
|
23
|
+
- Python 3.11+
|
|
24
|
+
- Temporal SDK 1.23+ (1.8+ for factory-only usage)
|
|
25
25
|
- OpenTelemetry API/SDK 1.38.0+
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Plugin Integration (Recommended)
|
|
30
|
+
|
|
31
|
+
Use `OpenBoxPlugin` for drop-in integration with Temporal Workers:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import os
|
|
35
|
+
from temporalio.worker import Worker
|
|
36
|
+
from openbox.plugin import OpenBoxPlugin
|
|
37
|
+
|
|
38
|
+
worker = Worker(
|
|
39
|
+
client,
|
|
40
|
+
task_queue="my-task-queue",
|
|
41
|
+
workflows=[MyWorkflow],
|
|
42
|
+
activities=[my_activity],
|
|
43
|
+
plugins=[
|
|
44
|
+
OpenBoxPlugin(
|
|
45
|
+
openbox_url=os.getenv("OPENBOX_URL"),
|
|
46
|
+
openbox_api_key=os.getenv("OPENBOX_API_KEY"),
|
|
47
|
+
)
|
|
48
|
+
],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
await worker.run()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The plugin automatically configures governance interceptors, OTel instrumentation,
|
|
55
|
+
sandbox passthrough, and the `send_governance_event` activity.
|
|
56
|
+
|
|
57
|
+
### Composing with Other Plugins
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from temporalio.contrib.opentelemetry import OpenTelemetryPlugin
|
|
61
|
+
|
|
62
|
+
worker = Worker(
|
|
63
|
+
client,
|
|
64
|
+
task_queue="my-task-queue",
|
|
65
|
+
workflows=[MyWorkflow],
|
|
66
|
+
activities=[my_activity],
|
|
67
|
+
plugins=[
|
|
68
|
+
OpenTelemetryPlugin(),
|
|
69
|
+
OpenBoxPlugin(openbox_url=..., openbox_api_key=...),
|
|
70
|
+
],
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> **Requires** `temporalio >= 1.23.0`. For older versions, use `create_openbox_worker()` below.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Quick Start (Factory)
|
|
30
79
|
|
|
31
80
|
Use the `create_openbox_worker()` factory for simple integration:
|
|
32
81
|
|
|
@@ -439,13 +488,13 @@ worker = Worker(
|
|
|
439
488
|
|
|
440
489
|
## Testing
|
|
441
490
|
|
|
442
|
-
The SDK includes comprehensive test coverage with
|
|
491
|
+
The SDK includes comprehensive test coverage with 15 test files:
|
|
443
492
|
|
|
444
493
|
```bash
|
|
445
494
|
pytest tests/
|
|
446
495
|
```
|
|
447
496
|
|
|
448
|
-
Test files: `test_activities.py`, `test_activity_interceptor.py`, `test_config.py`, `test_db_governance_hooks.py`, `test_file_governance_hooks.py`, `test_otel_hook_pause.py`, `test_otel_hook_pause_db.py`, `test_otel_setup.py`, `test_span_processor.py`, `test_tracing.py`, `test_types.py`, `test_worker.py`, `test_workflow_interceptor.py`
|
|
497
|
+
Test files: `test_activities.py`, `test_activity_interceptor.py`, `test_config.py`, `test_db_governance_hooks.py`, `test_file_governance_hooks.py`, `test_otel_hook_pause.py`, `test_otel_hook_pause_db.py`, `test_otel_setup.py`, `test_plugin.py`, `test_plugin_integration.py`, `test_span_processor.py`, `test_tracing.py`, `test_types.py`, `test_worker.py`, `test_workflow_interceptor.py`
|
|
449
498
|
|
|
450
499
|
---
|
|
451
500
|
|
|
@@ -462,4 +511,4 @@ MIT License - See LICENSE file for details
|
|
|
462
511
|
|
|
463
512
|
---
|
|
464
513
|
|
|
465
|
-
**Version:** 1.
|
|
514
|
+
**Version:** 1.2.0 | **Last Updated:** 2026-04-05
|
{openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/codebase-summary.md
RENAMED
|
@@ -22,6 +22,7 @@ openbox-temporal-sdk-python/
|
|
|
22
22
|
│ ├── types.py # Type definitions (workflow-safe)
|
|
23
23
|
│ ├── config.py # Configuration and initialization
|
|
24
24
|
│ ├── worker.py # Worker factory function
|
|
25
|
+
│ ├── plugin.py # OpenBoxPlugin(SimplePlugin) for Temporal AI Partner Ecosystem
|
|
25
26
|
│ ├── errors.py # Unified exception hierarchy
|
|
26
27
|
│ ├── client.py # GovernanceClient (centralized HTTP client)
|
|
27
28
|
│ ├── hitl.py # HITL approval handling
|
{openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/system-architecture.md
RENAMED
|
@@ -89,6 +89,39 @@ OpenBox SDK for Temporal Workflows is a governance and observability layer that
|
|
|
89
89
|
|
|
90
90
|
---
|
|
91
91
|
|
|
92
|
+
## Plugin Integration
|
|
93
|
+
|
|
94
|
+
### OpenBoxPlugin (SimplePlugin)
|
|
95
|
+
|
|
96
|
+
**File:** `openbox/plugin.py`
|
|
97
|
+
|
|
98
|
+
`OpenBoxPlugin` extends Temporal's `SimplePlugin` base class, providing a drop-in integration for the AI Partner Ecosystem. It composes all existing SDK components (interceptors, OTel setup, governance activities) without modifying them.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
Worker(client, task_queue, workflows, activities,
|
|
102
|
+
plugins=[OpenBoxPlugin(openbox_url=..., openbox_api_key=...)])
|
|
103
|
+
|
|
104
|
+
OpenBoxPlugin.__init__():
|
|
105
|
+
→ validate_api_key() # config.py
|
|
106
|
+
→ WorkflowSpanProcessor() # span_processor.py
|
|
107
|
+
→ setup_opentelemetry_for_governance() # otel_setup.py
|
|
108
|
+
→ GovernanceInterceptor() # workflow_interceptor.py
|
|
109
|
+
→ ActivityGovernanceInterceptor() # activity_interceptor.py
|
|
110
|
+
→ SimplePlugin.__init__(interceptors, activities, workflow_runner)
|
|
111
|
+
|
|
112
|
+
OpenBoxPlugin.configure_worker(config):
|
|
113
|
+
→ set_temporal_client(config["client"]) # activities.py
|
|
114
|
+
→ super().configure_worker(config) # appends interceptors, activities
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Key design choices:**
|
|
118
|
+
- Composition-only — no changes to existing modules
|
|
119
|
+
- Sandbox passthrough for `opentelemetry` via `workflow_runner` callback
|
|
120
|
+
- `configure_worker()` captures Temporal client ref for HALT terminate calls
|
|
121
|
+
- Plugin name: `"openbox.OpenBoxPlugin"` per Temporal's naming standard
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
92
125
|
## Component Architecture
|
|
93
126
|
|
|
94
127
|
### 1. Interceptor Layer
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# OpenBox Plugin Integration Guide
|
|
2
|
+
|
|
3
|
+
**For:** Temporal AI Partner Ecosystem
|
|
4
|
+
**Version:** 1.2.0
|
|
5
|
+
**Requires:** `temporalio >= 1.23.0`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What is OpenBox?
|
|
10
|
+
|
|
11
|
+
OpenBox provides governance and observability for Temporal workflows. It captures workflow/activity lifecycle events, HTTP requests, database queries, and file operations, then sends them to OpenBox Core for real-time policy evaluation. Verdicts (ALLOW, BLOCK, HALT, REQUIRE_APPROVAL, CONSTRAIN) control whether operations proceed.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install openbox-temporal-sdk-python
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import os
|
|
27
|
+
from temporalio.worker import Worker
|
|
28
|
+
from openbox.plugin import OpenBoxPlugin
|
|
29
|
+
|
|
30
|
+
worker = Worker(
|
|
31
|
+
client,
|
|
32
|
+
task_queue="my-task-queue",
|
|
33
|
+
workflows=[MyWorkflow],
|
|
34
|
+
activities=[my_activity],
|
|
35
|
+
plugins=[
|
|
36
|
+
OpenBoxPlugin(
|
|
37
|
+
openbox_url=os.getenv("OPENBOX_URL"),
|
|
38
|
+
openbox_api_key=os.getenv("OPENBOX_API_KEY"),
|
|
39
|
+
)
|
|
40
|
+
],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
await worker.run()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
| Parameter | Type | Default | Description |
|
|
51
|
+
|-----------|------|---------|-------------|
|
|
52
|
+
| `openbox_url` | str | required | OpenBox Core API URL |
|
|
53
|
+
| `openbox_api_key` | str | required | API key (`obx_live_*` or `obx_test_*`) |
|
|
54
|
+
| `governance_timeout` | float | 30.0 | Timeout for governance API calls (seconds) |
|
|
55
|
+
| `governance_policy` | str | `"fail_open"` | `"fail_open"` or `"fail_closed"` |
|
|
56
|
+
| `send_start_event` | bool | True | Send WorkflowStarted events |
|
|
57
|
+
| `send_activity_start_event` | bool | True | Send ActivityStarted events |
|
|
58
|
+
| `skip_workflow_types` | Set[str] | None | Workflow types to skip governance |
|
|
59
|
+
| `skip_activity_types` | Set[str] | None | Activity types to skip (default: `send_governance_event`) |
|
|
60
|
+
| `skip_signals` | Set[str] | None | Signal names to skip governance |
|
|
61
|
+
| `hitl_enabled` | bool | True | Enable human-in-the-loop approval polling |
|
|
62
|
+
| `instrument_databases` | bool | True | Instrument database libraries |
|
|
63
|
+
| `db_libraries` | set | None | Specific DB libraries to instrument (None = all) |
|
|
64
|
+
| `sqlalchemy_engine` | Any | None | Pre-existing SQLAlchemy engine to instrument |
|
|
65
|
+
| `instrument_file_io` | bool | True | Instrument file I/O operations |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## How It Works
|
|
70
|
+
|
|
71
|
+
The plugin composes existing OpenBox SDK components via Temporal's `SimplePlugin` base class:
|
|
72
|
+
|
|
73
|
+
1. **Constructor** — validates API key, sets up OTel instrumentation, creates governance interceptors
|
|
74
|
+
2. **`configure_worker()`** — stores Temporal client reference (for HALT terminate calls), delegates to `SimplePlugin` to append interceptors and activities
|
|
75
|
+
3. **`workflow_runner`** — adds sandbox passthrough for `opentelemetry` module
|
|
76
|
+
|
|
77
|
+
### Interceptor Ordering
|
|
78
|
+
|
|
79
|
+
Plugin interceptors are appended (innermost). This means user-provided interceptors run first, then OpenBox governance. This is correct for governance — it observes the final state of operations.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Governance Verdicts
|
|
84
|
+
|
|
85
|
+
| Verdict | Behavior |
|
|
86
|
+
|---------|----------|
|
|
87
|
+
| `ALLOW` | Continue normally |
|
|
88
|
+
| `CONSTRAIN` | Log constraints, continue |
|
|
89
|
+
| `REQUIRE_APPROVAL` | Pause, poll for human approval |
|
|
90
|
+
| `BLOCK` | Raise error, stop activity |
|
|
91
|
+
| `HALT` | Raise error, terminate workflow |
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Hook-Level Governance
|
|
96
|
+
|
|
97
|
+
Every HTTP request, database query, and file operation during an activity is evaluated in real-time:
|
|
98
|
+
|
|
99
|
+
- **HTTP** — request/response hooks via OTel instrumentation (httpx, requests, urllib3)
|
|
100
|
+
- **Database** — per-query hooks (psycopg2, asyncpg, pymysql, pymongo, redis, sqlalchemy)
|
|
101
|
+
- **File I/O** — per-operation hooks (open, read, write, close)
|
|
102
|
+
- **Function tracing** — `@traced` decorator for governed function calls
|
|
103
|
+
|
|
104
|
+
Each operation is evaluated at `started` (pre-execution, can block) and `completed` (post-execution).
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Human-in-the-Loop
|
|
109
|
+
|
|
110
|
+
When a `REQUIRE_APPROVAL` verdict is returned:
|
|
111
|
+
|
|
112
|
+
1. Activity pauses and polls for approval
|
|
113
|
+
2. Approval status checked via OpenBox Core API
|
|
114
|
+
3. On approval → activity continues
|
|
115
|
+
4. On rejection/expiry → activity fails with `ApprovalRejectedError`/`ApprovalExpiredError`
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Error Handling
|
|
120
|
+
|
|
121
|
+
| Policy | Behavior |
|
|
122
|
+
|--------|----------|
|
|
123
|
+
| `fail_open` (default) | If governance API fails, allow workflow to continue |
|
|
124
|
+
| `fail_closed` | If governance API fails, terminate workflow |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Composability
|
|
129
|
+
|
|
130
|
+
OpenBoxPlugin works alongside other Temporal plugins:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from temporalio.contrib.opentelemetry import OpenTelemetryPlugin
|
|
134
|
+
|
|
135
|
+
worker = Worker(
|
|
136
|
+
client,
|
|
137
|
+
task_queue="my-task-queue",
|
|
138
|
+
workflows=[MyWorkflow],
|
|
139
|
+
activities=[my_activity],
|
|
140
|
+
plugins=[
|
|
141
|
+
OpenTelemetryPlugin(),
|
|
142
|
+
OpenBoxPlugin(openbox_url=..., openbox_api_key=...),
|
|
143
|
+
],
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
It also works with user-provided interceptors — both are active simultaneously.
|
|
@@ -67,6 +67,17 @@ from .span_processor import WorkflowSpanProcessor
|
|
|
67
67
|
|
|
68
68
|
from .workflow_interceptor import GovernanceInterceptor
|
|
69
69
|
|
|
70
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
71
|
+
# Plugin (requires temporalio >= 1.24.0)
|
|
72
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
from temporalio.plugin import SimplePlugin # noqa: F401 — probe only
|
|
76
|
+
|
|
77
|
+
from .plugin import OpenBoxPlugin
|
|
78
|
+
except ImportError:
|
|
79
|
+
pass # temporalio < 1.24.0, plugin not available
|
|
80
|
+
|
|
70
81
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
71
82
|
# Verdict Handler
|
|
72
83
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -128,6 +139,8 @@ from .client import GovernanceClient
|
|
|
128
139
|
__all__ = [
|
|
129
140
|
# Simple Worker Factory (recommended)
|
|
130
141
|
"create_openbox_worker",
|
|
142
|
+
# Plugin (recommended for temporalio >= 1.24.0)
|
|
143
|
+
"OpenBoxPlugin",
|
|
131
144
|
# Configuration
|
|
132
145
|
"initialize",
|
|
133
146
|
"get_global_config",
|
{openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/activities.py
RENAMED
|
@@ -70,7 +70,9 @@ async def _terminate_workflow_for_halt(workflow_id: str, reason: str) -> None:
|
|
|
70
70
|
except Exception as e:
|
|
71
71
|
logger.warning(f"HALT: failed to terminate workflow {workflow_id}: {e}")
|
|
72
72
|
else:
|
|
73
|
-
logger.warning(
|
|
73
|
+
logger.warning(
|
|
74
|
+
f"HALT: _temporal_client is None, cannot terminate workflow {workflow_id}"
|
|
75
|
+
)
|
|
74
76
|
|
|
75
77
|
# Always raise to stop the current activity execution.
|
|
76
78
|
# Even after successful terminate(), the activity code keeps running
|
|
@@ -82,7 +84,9 @@ async def _terminate_workflow_for_halt(workflow_id: str, reason: str) -> None:
|
|
|
82
84
|
)
|
|
83
85
|
|
|
84
86
|
|
|
85
|
-
def raise_governance_block(
|
|
87
|
+
def raise_governance_block(
|
|
88
|
+
reason: str, policy_id: str = None, risk_score: float = None
|
|
89
|
+
):
|
|
86
90
|
"""Raise non-retryable ApplicationError for BLOCK verdict — blocks activity only."""
|
|
87
91
|
details = {"policy_id": policy_id, "risk_score": risk_score}
|
|
88
92
|
raise ApplicationError(
|
|
@@ -93,35 +97,66 @@ def raise_governance_block(reason: str, policy_id: str = None, risk_score: float
|
|
|
93
97
|
)
|
|
94
98
|
|
|
95
99
|
|
|
100
|
+
def _build_verdict_result(verdict: Verdict, reason, policy_id, risk_score) -> dict:
|
|
101
|
+
"""Build a success result dict from a governance verdict."""
|
|
102
|
+
return {
|
|
103
|
+
"success": True,
|
|
104
|
+
"verdict": verdict.value,
|
|
105
|
+
"action": verdict.value, # backward compat
|
|
106
|
+
"reason": reason,
|
|
107
|
+
"policy_id": policy_id,
|
|
108
|
+
"risk_score": risk_score,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def _handle_stop_verdict(
|
|
113
|
+
verdict: Verdict, reason, policy_id, risk_score, event_type, event_payload
|
|
114
|
+
) -> Optional[dict]:
|
|
115
|
+
"""Handle BLOCK/HALT verdicts. Returns result for signals, raises for others."""
|
|
116
|
+
logger.info(
|
|
117
|
+
f"Governance {verdict.value} {event_type}: {reason} (policy: {policy_id})"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# SignalReceived: return result instead of raising
|
|
121
|
+
if event_type == "SignalReceived":
|
|
122
|
+
return _build_verdict_result(verdict, reason, policy_id, risk_score)
|
|
123
|
+
|
|
124
|
+
# HALT: terminate workflow + raise
|
|
125
|
+
if verdict == Verdict.HALT:
|
|
126
|
+
workflow_id = event_payload.get("workflow_id", "")
|
|
127
|
+
await _terminate_workflow_for_halt(workflow_id, reason or "No reason provided")
|
|
128
|
+
|
|
129
|
+
# BLOCK: fail this activity only
|
|
130
|
+
raise_governance_block(
|
|
131
|
+
reason=reason or "No reason provided",
|
|
132
|
+
policy_id=policy_id,
|
|
133
|
+
risk_score=risk_score,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _handle_api_error(event_type: str, error_msg: str, on_api_error: str) -> dict:
|
|
138
|
+
"""Handle non-200 responses or exceptions based on error policy."""
|
|
139
|
+
logger.warning(f"Governance API error for {event_type}: {error_msg}")
|
|
140
|
+
if on_api_error == "fail_closed":
|
|
141
|
+
raise GovernanceAPIError(error_msg)
|
|
142
|
+
return {"success": False, "error": error_msg}
|
|
143
|
+
|
|
144
|
+
|
|
96
145
|
@activity.defn(name="send_governance_event")
|
|
97
146
|
async def send_governance_event(input: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
98
147
|
"""
|
|
99
148
|
Activity that sends governance events to OpenBox Core.
|
|
100
149
|
|
|
101
|
-
|
|
102
|
-
to maintain workflow determinism.
|
|
103
|
-
|
|
104
|
-
Args (in input dict):
|
|
105
|
-
api_url: OpenBox Core API URL
|
|
106
|
-
api_key: API key for authentication
|
|
107
|
-
payload: Event payload (without timestamp)
|
|
108
|
-
timeout: Request timeout in seconds
|
|
109
|
-
on_api_error: "fail_open" (default) or "fail_closed"
|
|
110
|
-
|
|
111
|
-
When on_api_error == "fail_closed" and API fails, raises GovernanceAPIError.
|
|
112
|
-
This is caught by the workflow interceptor and re-raised as GovernanceHaltError.
|
|
113
|
-
|
|
114
|
-
Logging is safe here because activities run outside the workflow sandbox.
|
|
150
|
+
Called from WorkflowInboundInterceptor via workflow.execute_activity()
|
|
151
|
+
to maintain workflow determinism.
|
|
115
152
|
"""
|
|
116
|
-
# Extract input fields
|
|
117
153
|
api_url = input.get("api_url", "")
|
|
118
154
|
api_key = input.get("api_key", "")
|
|
119
155
|
event_payload = input.get("payload", {})
|
|
120
156
|
timeout = input.get("timeout", 30.0)
|
|
121
157
|
on_api_error = input.get("on_api_error", "fail_open")
|
|
122
158
|
|
|
123
|
-
# Add timestamp
|
|
124
|
-
# Use RFC3339 format: 2024-01-15T10:30:45.123Z
|
|
159
|
+
# Add timestamp in activity context (non-deterministic code allowed)
|
|
125
160
|
payload = {**event_payload, "timestamp": _rfc3339_now()}
|
|
126
161
|
event_type = event_payload.get("event_type", "unknown")
|
|
127
162
|
|
|
@@ -133,66 +168,34 @@ async def send_governance_event(input: Dict[str, Any]) -> Optional[Dict[str, Any
|
|
|
133
168
|
headers=build_auth_headers(api_key),
|
|
134
169
|
)
|
|
135
170
|
|
|
136
|
-
if response.status_code
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
# HALT → terminate workflow + raise to stop activity
|
|
161
|
-
if verdict == Verdict.HALT:
|
|
162
|
-
workflow_id = event_payload.get("workflow_id", "")
|
|
163
|
-
# Always raises ApplicationError(type="GovernanceHalt")
|
|
164
|
-
await _terminate_workflow_for_halt(
|
|
165
|
-
workflow_id, reason or "No reason provided"
|
|
166
|
-
)
|
|
167
|
-
else:
|
|
168
|
-
# BLOCK → fail this activity only, workflow can continue
|
|
169
|
-
raise_governance_block(
|
|
170
|
-
reason=reason or "No reason provided",
|
|
171
|
-
policy_id=policy_id,
|
|
172
|
-
risk_score=risk_score,
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
"success": True,
|
|
177
|
-
"verdict": verdict.value,
|
|
178
|
-
"action": verdict.value, # backward compat
|
|
179
|
-
"reason": reason,
|
|
180
|
-
"policy_id": policy_id,
|
|
181
|
-
"risk_score": risk_score,
|
|
182
|
-
}
|
|
183
|
-
else:
|
|
184
|
-
error_msg = f"HTTP {response.status_code}: {response.text}"
|
|
185
|
-
logger.warning(f"Governance API error for {event_type}: {error_msg}")
|
|
186
|
-
if on_api_error == "fail_closed":
|
|
187
|
-
raise GovernanceAPIError(error_msg)
|
|
188
|
-
return {"success": False, "error": error_msg}
|
|
171
|
+
if response.status_code != 200:
|
|
172
|
+
return _handle_api_error(
|
|
173
|
+
event_type,
|
|
174
|
+
f"HTTP {response.status_code}: {response.text}",
|
|
175
|
+
on_api_error,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
data = response.json()
|
|
179
|
+
verdict = Verdict.from_string(
|
|
180
|
+
data.get("verdict") or data.get("action", "continue")
|
|
181
|
+
)
|
|
182
|
+
reason = data.get("reason")
|
|
183
|
+
policy_id = data.get("policy_id")
|
|
184
|
+
risk_score = data.get("risk_score", 0.0)
|
|
185
|
+
|
|
186
|
+
if verdict.should_stop():
|
|
187
|
+
result = await _handle_stop_verdict(
|
|
188
|
+
verdict, reason, policy_id, risk_score, event_type, event_payload
|
|
189
|
+
)
|
|
190
|
+
if result:
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
return _build_verdict_result(verdict, reason, policy_id, risk_score)
|
|
189
194
|
|
|
190
195
|
except (GovernanceAPIError, ApplicationError):
|
|
191
|
-
raise
|
|
196
|
+
raise
|
|
192
197
|
except Exception as e:
|
|
193
198
|
logger.warning(f"Failed to send {event_type} event: {e}")
|
|
194
199
|
if on_api_error == "fail_closed":
|
|
195
200
|
raise GovernanceAPIError(str(e))
|
|
196
201
|
return {"success": False, "error": str(e)}
|
|
197
|
-
|
|
198
|
-
|