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.
Files changed (60) hide show
  1. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/PKG-INFO +58 -11
  2. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/README.md +55 -6
  3. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/codebase-summary.md +1 -0
  4. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/system-architecture.md +33 -0
  5. openbox_temporal_sdk_python-1.1.1/docs/temporal-plugin-integration-guide.md +147 -0
  6. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/__init__.py +13 -0
  7. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/activities.py +78 -75
  8. openbox_temporal_sdk_python-1.1.1/openbox/activity_interceptor.py +676 -0
  9. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/client.py +11 -4
  10. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/config.py +13 -7
  11. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/context_propagation.py +4 -1
  12. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/db_governance_hooks.py +239 -342
  13. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/errors.py +18 -3
  14. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/file_governance_hooks.py +67 -25
  15. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/hitl.py +5 -0
  16. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/hook_governance.py +68 -17
  17. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/http_governance_hooks.py +193 -83
  18. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/otel_setup.py +104 -117
  19. openbox_temporal_sdk_python-1.1.1/openbox/plugin.py +185 -0
  20. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/span_processor.py +40 -12
  21. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/tracing.py +83 -106
  22. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/types.py +17 -5
  23. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/verdict_handler.py +17 -7
  24. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/worker.py +13 -4
  25. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/workflow_interceptor.py +146 -83
  26. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/pyproject.toml +5 -7
  27. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/conftest.py +9 -4
  28. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_activities.py +41 -15
  29. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_activity_interceptor.py +410 -195
  30. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_config.py +16 -7
  31. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_db_governance_hooks.py +149 -71
  32. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_file_governance_hooks.py +111 -43
  33. openbox_temporal_sdk_python-1.1.1/tests/test_http_body_truncation.py +75 -0
  34. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_otel_setup.py +372 -175
  35. openbox_temporal_sdk_python-1.1.1/tests/test_plugin.py +263 -0
  36. openbox_temporal_sdk_python-1.1.1/tests/test_plugin_integration.py +280 -0
  37. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_psycopg2_hooks_verify.py +10 -3
  38. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_span_processor.py +22 -9
  39. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_tracing.py +54 -17
  40. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_worker.py +13 -5
  41. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/test_workflow_interceptor.py +92 -42
  42. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/uv.lock +96 -608
  43. openbox_temporal_sdk_python-1.1.0/openbox/activity_interceptor.py +0 -626
  44. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/.github/workflows/publish.yml +0 -0
  45. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/.github/workflows/sonarqube.yaml +0 -0
  46. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/.gitignore +0 -0
  47. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/.python-version +0 -0
  48. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/.repomixignore +0 -0
  49. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/CHANGELOG.md +0 -0
  50. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/LICENSE +0 -0
  51. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/changelog-hook-level-governance.md +0 -0
  52. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/code-standards.md +0 -0
  53. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/configuration.md +0 -0
  54. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/docs/project-overview-pdr.md +0 -0
  55. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/openbox/py.typed +0 -0
  56. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/release-manifest.json +0 -0
  57. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/repomix-output.xml +0 -0
  58. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/sonar-project.properties +0 -0
  59. {openbox_temporal_sdk_python-1.1.0 → openbox_temporal_sdk_python-1.1.1}/tests/__init__.py +0 -0
  60. {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.0
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.9
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.8.0
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.9+
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
- ## Quick Start
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 13 test files:
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.1.0 | **Last Updated:** 2026-03-09
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.9+
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
- ## Quick Start
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 13 test files:
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.1.0 | **Last Updated:** 2026-03-09
514
+ **Version:** 1.2.0 | **Last Updated:** 2026-04-05
@@ -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
@@ -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",
@@ -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(f"HALT: _temporal_client is None, cannot terminate workflow {workflow_id}")
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(reason: str, policy_id: str = None, risk_score: float = None):
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
- This activity is called from WorkflowInboundInterceptor via workflow.execute_activity()
102
- to maintain workflow determinism. HTTP calls cannot be made directly in workflow context.
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 here in activity context (non-deterministic code allowed)
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 == 200:
137
- data = response.json()
138
- # Parse verdict (v1.1) or action (v1.0)
139
- verdict = Verdict.from_string(data.get("verdict") or data.get("action", "continue"))
140
- reason = data.get("reason")
141
- policy_id = data.get("policy_id")
142
- risk_score = data.get("risk_score", 0.0)
143
-
144
- # Check if governance wants to stop (BLOCK or HALT)
145
- if verdict.should_stop():
146
- logger.info(f"Governance {verdict.value} {event_type}: {reason} (policy: {policy_id})")
147
-
148
- # For SignalReceived events, return result instead of raising/terminating
149
- # The workflow interceptor will store verdict for activity interceptor to check
150
- if event_type == "SignalReceived":
151
- return {
152
- "success": True,
153
- "verdict": verdict.value,
154
- "action": verdict.value, # backward compat
155
- "reason": reason,
156
- "policy_id": policy_id,
157
- "risk_score": risk_score,
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 # Re-raise to workflow (ApplicationError is non-retryable)
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
-