openbox-temporal-sdk-python 1.1.1__tar.gz → 1.1.2__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.2/CHANGELOG.md +124 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/PKG-INFO +35 -8
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/README.md +34 -7
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/code-standards.md +29 -15
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/codebase-summary.md +27 -9
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/__init__.py +3 -3
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/activities.py +101 -52
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/db_governance_hooks.py +8 -3
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/errors.py +15 -1
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/hook_governance.py +15 -5
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/plugin.py +36 -11
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/worker.py +33 -20
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/workflow_interceptor.py +77 -50
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/pyproject.toml +1 -1
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_plugin.py +3 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_plugin_integration.py +4 -3
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_worker.py +68 -45
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_workflow_interceptor.py +50 -65
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/uv.lock +12 -12
- openbox_temporal_sdk_python-1.1.1/CHANGELOG.md +0 -68
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/.github/workflows/publish.yml +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/.github/workflows/sonarqube.yaml +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/.gitignore +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/.python-version +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/.repomixignore +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/LICENSE +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/changelog-hook-level-governance.md +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/configuration.md +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/project-overview-pdr.md +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/system-architecture.md +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/temporal-plugin-integration-guide.md +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/activity_interceptor.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/client.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/config.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/context_propagation.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/file_governance_hooks.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/hitl.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/http_governance_hooks.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/otel_setup.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/py.typed +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/span_processor.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/tracing.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/types.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/verdict_handler.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/release-manifest.json +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/repomix-output.xml +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/sonar-project.properties +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/__init__.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/conftest.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_activities.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_activity_interceptor.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_config.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_db_governance_hooks.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_file_governance_hooks.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_http_body_truncation.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_otel_setup.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_psycopg2_hooks_verify.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_span_processor.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_tracing.py +0 -0
- {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_types.py +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to OpenBox SDK for Temporal Workflows.
|
|
4
|
+
|
|
5
|
+
## [1.1.2] - 2026-04-22
|
|
6
|
+
|
|
7
|
+
### Security
|
|
8
|
+
|
|
9
|
+
- **API key no longer flows through workflow history.** `send_governance_event` refactored into a `GovernanceActivities` class that holds credentials on `self`; activity inputs carry only `payload`, `timeout`, and `on_api_error`. Before this change, anyone with Describe permissions on the namespace could read the API key from the recorded activity input.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- W3C trace propagation through Temporal headers, on by default via `enable_trace_propagation=True` on `OpenBoxPlugin` and `create_openbox_worker()`. Uses `temporalio.contrib.opentelemetry.TracingInterceptor` so spans started by the caller stitch to workflow/activity spans on the worker side.
|
|
14
|
+
- `ApplicationError` type constants in `errors.py` (`GOVERNANCE_HALT_ERROR_TYPE`, `GOVERNANCE_BLOCK_ERROR_TYPE`, `GOVERNANCE_API_ERROR_TYPE`, `GOVERNANCE_STOP_ERROR_TYPE`) — single source of truth for governance error routing.
|
|
15
|
+
- `openbox.activities.GovernanceActivities` class + `build_governance_activities()` factory.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Workflow exception shadowing** — `WorkflowFailed` event send is now wrapped in `try/except`, so a `GovernanceHaltError` from `fail_closed + API down` no longer replaces the real workflow error.
|
|
20
|
+
- **String-matching on exception types** — `workflow_interceptor` now inspects `ApplicationError.type` via the exception chain instead of `"GovernanceHalt" in str(e)`. Eliminates false positives when a user workflow happens to emit an error message containing a governance keyword.
|
|
21
|
+
- **Race creating governance HTTP client** — `hook_governance._get_sync_client` / `_get_async_client` now use double-checked locking. Previously two concurrent activities could each create a client, with the losing instance getting garbage-collected while its connection pool leaked.
|
|
22
|
+
- **Replayer plugin coverage** — `test_plugin_integration.py` now passes `plugins=[plugin]` to `Replayer`, so replay tests validate interceptor determinism, not just user workflow code.
|
|
23
|
+
- Version-pin mismatch — comments in `openbox/__init__.py` said `temporalio >= 1.24.0` but pyproject/README pin `1.23.0`. Corrected to `1.23.0`.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `plugin.py` / `worker.py` now use `logging.getLogger(__name__)` for initialization status messages instead of `print()`.
|
|
28
|
+
|
|
29
|
+
## [1.1.1] - 2026-04-07
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **OpenBoxPlugin** — drop-in `SimplePlugin` integration for Temporal Workers. Single-line setup: `plugins=[OpenBoxPlugin(openbox_url=..., openbox_api_key=...)]`. Auto-registers interceptors, OTel instrumentation, sandbox passthrough, and `send_governance_event` activity
|
|
34
|
+
- Plugin integration guide for Temporal AI Partner Ecosystem (`docs/temporal-plugin-integration-guide.md`)
|
|
35
|
+
- HTTP body truncation tests (`tests/test_http_body_truncation.py`)
|
|
36
|
+
- Plugin unit tests (`tests/test_plugin.py`) and integration/replay tests (`tests/test_plugin_integration.py`)
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **HTTP body truncation** — enforce `max_body_size` (default 64KB) on request/response bodies in governance spans
|
|
41
|
+
- **File I/O spans** — remove raw file content from governance payloads; only `bytes_read`/`bytes_written` metadata sent
|
|
42
|
+
- **error_type sanitization** — prevent serialized error objects from being sent as `error.cause.error_type` string in WorkflowFailed payloads
|
|
43
|
+
- Remove useless f-strings, redundant `(ImportError, Exception)` clauses, merge nested if statements
|
|
44
|
+
- Prefix unused `span` param in urllib hook
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- `temporalio>=1.23.0` (from 1.8.0) for SimplePlugin support
|
|
49
|
+
- `GovernanceConfig.max_body_size` default changed from `None` (unlimited) to `65536` (64KB)
|
|
50
|
+
- `\w` regex shorthand in API key pattern
|
|
51
|
+
|
|
52
|
+
### Refactored
|
|
53
|
+
|
|
54
|
+
- Reduce cognitive complexity across 7 modules: `activity_interceptor.py` (126→split), `workflow_interceptor.py` (40→split), `activities.py` (20→split), `db_governance_hooks.py` (34→split), `otel_setup.py` (51→split), `tracing.py` (85→split), `verdict_handler.py` (16→15)
|
|
55
|
+
- Extract shared helpers: `_run_governed_query_sync/async`, `_build_error_dict`, `_extract_dbapi_context`, `_instrument_sqlalchemy`
|
|
56
|
+
|
|
57
|
+
### Dependencies
|
|
58
|
+
|
|
59
|
+
- Bump Pygments 2.19.2 → 2.20.0 (ReDoS fix, CVSS 1.9)
|
|
60
|
+
|
|
61
|
+
## [1.1.0] - 2026-03-09
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- **Hook-level governance** — real-time, per-operation governance evaluation during activity execution
|
|
66
|
+
- Every HTTP request, database query, file operation, and traced function call is evaluated at `started` (before, can block) and `completed` (after, informational) stages
|
|
67
|
+
- Same `POST /api/v1/governance/evaluate` endpoint with new `hook_trigger` field in payload
|
|
68
|
+
- Automatically enabled when using `create_openbox_worker()`
|
|
69
|
+
- **Database query governance** — per-query started/completed evaluations for psycopg2, pymysql, mysql-connector, asyncpg, pymongo, redis, sqlalchemy
|
|
70
|
+
- **File I/O governance** — per-operation evaluations for open, read, write, readline, readlines, writelines, close (opt-in via `instrument_file_io=True`)
|
|
71
|
+
- **`@traced` decorator** (`openbox.tracing`) — function-level governance with OTel spans; zero overhead when governance not configured
|
|
72
|
+
- **`GovernanceBlockedError`** — new exception type for hook-level blocking with verdict, reason, and resource identifier
|
|
73
|
+
- **Abort propagation** — once one hook blocks, all subsequent hooks for the same activity short-circuit immediately
|
|
74
|
+
- **HALT workflow termination** from hook-level governance via `client.terminate()`
|
|
75
|
+
- **REQUIRE_APPROVAL** from hook-level governance enters the same HITL approval polling flow as activity-level approvals
|
|
76
|
+
- **`duration_ns`** computed for all hook span types (HTTP, file, function — DB already had it)
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- **`hook_trigger` simplified to boolean** — was a dict with type/stage/data, now just `true`. All data moved to span root fields
|
|
81
|
+
- **Span data consolidation** — all type-specific fields at span root (`hook_type`, `http_method`, `db_system`, `file_path`, `function`, etc.)
|
|
82
|
+
- **`attributes` is OTel-original only** — no custom `openbox.*`, `http.request.*`, `db.result.*` fields injected
|
|
83
|
+
- Hook governance payloads send only the current span per evaluation (not accumulated history)
|
|
84
|
+
- Event-level payloads (ActivityStarted/Completed, Workflow events) no longer include spans
|
|
85
|
+
- Simplified `WorkflowSpanProcessor` — removed span buffering, governed span tracking, body data merging; `on_end()` now only forwards to fallback exporters
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- HALT verdict from hooks now correctly terminates the workflow (previously only stopped the activity like BLOCK)
|
|
90
|
+
- REQUIRE_APPROVAL from hooks now enters the approval polling flow (previously raised unhandled error)
|
|
91
|
+
- Stale buffer/verdict from previous workflow run no longer carries over when workflow_id is reused
|
|
92
|
+
- Subsequent hooks no longer fire after the first hook blocks an activity
|
|
93
|
+
|
|
94
|
+
## [1.0.21] - 2026-03-04
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
|
|
98
|
+
- Human-in-the-loop approval with expiration handling
|
|
99
|
+
- Approval polling via `POST /api/v1/governance/approval`
|
|
100
|
+
- Guardrails: input/output validation and redaction
|
|
101
|
+
- `GovernanceVerdictResponse.from_dict()` with guardrails_result parsing
|
|
102
|
+
- Output redaction for activity results
|
|
103
|
+
- `_deep_update_dataclass()` for in-place dataclass field updates from redacted dicts
|
|
104
|
+
|
|
105
|
+
### Fixed
|
|
106
|
+
|
|
107
|
+
- Temporal Payload objects no longer slip through as non-serializable in governance payloads
|
|
108
|
+
- Stale buffer detection via run_id comparison
|
|
109
|
+
|
|
110
|
+
## [1.0.0] - 2026-02-15
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
|
|
114
|
+
- Initial release
|
|
115
|
+
- 6 event types: WorkflowStarted, WorkflowCompleted, WorkflowFailed, SignalReceived, ActivityStarted, ActivityCompleted
|
|
116
|
+
- 5-tier verdict system: ALLOW, CONSTRAIN, REQUIRE_APPROVAL, BLOCK, HALT
|
|
117
|
+
- HTTP instrumentation via OpenTelemetry (httpx, requests, urllib3, urllib)
|
|
118
|
+
- Database instrumentation (psycopg2, pymysql, asyncpg, pymongo, redis, sqlalchemy)
|
|
119
|
+
- File I/O instrumentation (opt-in)
|
|
120
|
+
- Zero-code setup via `create_openbox_worker()` factory
|
|
121
|
+
- Workflow and activity interceptors for governance
|
|
122
|
+
- Span buffering and activity context tracking
|
|
123
|
+
- `fail_open` / `fail_closed` error policies
|
|
124
|
+
- v1.0 backward compatibility for legacy verdict strings
|
|
@@ -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.2
|
|
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
|
|
@@ -100,7 +100,15 @@ await worker.run()
|
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
The plugin automatically configures governance interceptors, OTel instrumentation,
|
|
103
|
-
sandbox passthrough,
|
|
103
|
+
sandbox passthrough, W3C trace propagation through Temporal headers (via
|
|
104
|
+
`temporalio.contrib.opentelemetry.TracingInterceptor`), and the
|
|
105
|
+
`send_governance_event` activity.
|
|
106
|
+
|
|
107
|
+
**Credentials never leave the plugin.** `openbox_api_key` is captured on the
|
|
108
|
+
governance activity instance itself — it does **not** flow through activity
|
|
109
|
+
inputs, so it is never written to workflow history. To opt out of trace
|
|
110
|
+
propagation (e.g., if you already wire `OpenTelemetryPlugin`), pass
|
|
111
|
+
`enable_trace_propagation=False`.
|
|
104
112
|
|
|
105
113
|
### Composing with Other Plugins
|
|
106
114
|
|
|
@@ -148,8 +156,10 @@ The factory automatically:
|
|
|
148
156
|
1. Validates the API key
|
|
149
157
|
2. Creates span processor
|
|
150
158
|
3. Sets up OpenTelemetry instrumentation
|
|
151
|
-
4. Creates governance interceptors
|
|
152
|
-
5.
|
|
159
|
+
4. Creates governance interceptors (incl. W3C trace propagation)
|
|
160
|
+
5. Builds the `GovernanceActivities` instance with credentials captured on
|
|
161
|
+
`self` and registers its `send_governance_event` method — the API key is
|
|
162
|
+
never passed through activity inputs / workflow history
|
|
153
163
|
6. Returns fully configured Worker
|
|
154
164
|
|
|
155
165
|
---
|
|
@@ -195,6 +205,11 @@ worker = create_openbox_worker(
|
|
|
195
205
|
# File I/O instrumentation
|
|
196
206
|
instrument_file_io=False, # disabled by default
|
|
197
207
|
|
|
208
|
+
# Header-based W3C trace propagation (client → workflow → activities).
|
|
209
|
+
# Default True. Set False if you already wire OpenTelemetryPlugin or a
|
|
210
|
+
# custom propagator.
|
|
211
|
+
enable_trace_propagation=True,
|
|
212
|
+
|
|
198
213
|
# Standard Worker options (all supported)
|
|
199
214
|
activity_executor=my_executor,
|
|
200
215
|
max_concurrent_activities=10,
|
|
@@ -471,7 +486,7 @@ from openbox import (
|
|
|
471
486
|
)
|
|
472
487
|
from openbox.otel_setup import setup_opentelemetry_for_governance
|
|
473
488
|
from openbox.activity_interceptor import ActivityGovernanceInterceptor
|
|
474
|
-
from openbox.activities import
|
|
489
|
+
from openbox.activities import build_governance_activities
|
|
475
490
|
from openbox.hook_governance import FAIL_OPEN, FAIL_CLOSED # error policy constants
|
|
476
491
|
|
|
477
492
|
# 1. Initialize SDK
|
|
@@ -511,14 +526,26 @@ activity_interceptor = ActivityGovernanceInterceptor(
|
|
|
511
526
|
config=config,
|
|
512
527
|
)
|
|
513
528
|
|
|
514
|
-
# 6.
|
|
529
|
+
# 6. Build the governance activity instance (credentials captured on `self`
|
|
530
|
+
# so they never flow through workflow history).
|
|
531
|
+
governance_activities = build_governance_activities(
|
|
532
|
+
api_url="http://localhost:8086",
|
|
533
|
+
api_key="obx_test_key_1",
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# 7. Create worker
|
|
515
537
|
from temporalio.worker import Worker
|
|
538
|
+
from temporalio.contrib.opentelemetry import TracingInterceptor
|
|
516
539
|
worker = Worker(
|
|
517
540
|
client=client,
|
|
518
541
|
task_queue="my-task-queue",
|
|
519
542
|
workflows=[MyWorkflow],
|
|
520
|
-
activities=[my_activity, send_governance_event],
|
|
521
|
-
interceptors=[
|
|
543
|
+
activities=[my_activity, governance_activities.send_governance_event],
|
|
544
|
+
interceptors=[
|
|
545
|
+
workflow_interceptor,
|
|
546
|
+
activity_interceptor,
|
|
547
|
+
TracingInterceptor(), # W3C header propagation for OTel spans
|
|
548
|
+
],
|
|
522
549
|
)
|
|
523
550
|
```
|
|
524
551
|
|
|
@@ -52,7 +52,15 @@ await worker.run()
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
The plugin automatically configures governance interceptors, OTel instrumentation,
|
|
55
|
-
sandbox passthrough,
|
|
55
|
+
sandbox passthrough, W3C trace propagation through Temporal headers (via
|
|
56
|
+
`temporalio.contrib.opentelemetry.TracingInterceptor`), and the
|
|
57
|
+
`send_governance_event` activity.
|
|
58
|
+
|
|
59
|
+
**Credentials never leave the plugin.** `openbox_api_key` is captured on the
|
|
60
|
+
governance activity instance itself — it does **not** flow through activity
|
|
61
|
+
inputs, so it is never written to workflow history. To opt out of trace
|
|
62
|
+
propagation (e.g., if you already wire `OpenTelemetryPlugin`), pass
|
|
63
|
+
`enable_trace_propagation=False`.
|
|
56
64
|
|
|
57
65
|
### Composing with Other Plugins
|
|
58
66
|
|
|
@@ -100,8 +108,10 @@ The factory automatically:
|
|
|
100
108
|
1. Validates the API key
|
|
101
109
|
2. Creates span processor
|
|
102
110
|
3. Sets up OpenTelemetry instrumentation
|
|
103
|
-
4. Creates governance interceptors
|
|
104
|
-
5.
|
|
111
|
+
4. Creates governance interceptors (incl. W3C trace propagation)
|
|
112
|
+
5. Builds the `GovernanceActivities` instance with credentials captured on
|
|
113
|
+
`self` and registers its `send_governance_event` method — the API key is
|
|
114
|
+
never passed through activity inputs / workflow history
|
|
105
115
|
6. Returns fully configured Worker
|
|
106
116
|
|
|
107
117
|
---
|
|
@@ -147,6 +157,11 @@ worker = create_openbox_worker(
|
|
|
147
157
|
# File I/O instrumentation
|
|
148
158
|
instrument_file_io=False, # disabled by default
|
|
149
159
|
|
|
160
|
+
# Header-based W3C trace propagation (client → workflow → activities).
|
|
161
|
+
# Default True. Set False if you already wire OpenTelemetryPlugin or a
|
|
162
|
+
# custom propagator.
|
|
163
|
+
enable_trace_propagation=True,
|
|
164
|
+
|
|
150
165
|
# Standard Worker options (all supported)
|
|
151
166
|
activity_executor=my_executor,
|
|
152
167
|
max_concurrent_activities=10,
|
|
@@ -423,7 +438,7 @@ from openbox import (
|
|
|
423
438
|
)
|
|
424
439
|
from openbox.otel_setup import setup_opentelemetry_for_governance
|
|
425
440
|
from openbox.activity_interceptor import ActivityGovernanceInterceptor
|
|
426
|
-
from openbox.activities import
|
|
441
|
+
from openbox.activities import build_governance_activities
|
|
427
442
|
from openbox.hook_governance import FAIL_OPEN, FAIL_CLOSED # error policy constants
|
|
428
443
|
|
|
429
444
|
# 1. Initialize SDK
|
|
@@ -463,14 +478,26 @@ activity_interceptor = ActivityGovernanceInterceptor(
|
|
|
463
478
|
config=config,
|
|
464
479
|
)
|
|
465
480
|
|
|
466
|
-
# 6.
|
|
481
|
+
# 6. Build the governance activity instance (credentials captured on `self`
|
|
482
|
+
# so they never flow through workflow history).
|
|
483
|
+
governance_activities = build_governance_activities(
|
|
484
|
+
api_url="http://localhost:8086",
|
|
485
|
+
api_key="obx_test_key_1",
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# 7. Create worker
|
|
467
489
|
from temporalio.worker import Worker
|
|
490
|
+
from temporalio.contrib.opentelemetry import TracingInterceptor
|
|
468
491
|
worker = Worker(
|
|
469
492
|
client=client,
|
|
470
493
|
task_queue="my-task-queue",
|
|
471
494
|
workflows=[MyWorkflow],
|
|
472
|
-
activities=[my_activity, send_governance_event],
|
|
473
|
-
interceptors=[
|
|
495
|
+
activities=[my_activity, governance_activities.send_governance_event],
|
|
496
|
+
interceptors=[
|
|
497
|
+
workflow_interceptor,
|
|
498
|
+
activity_interceptor,
|
|
499
|
+
TracingInterceptor(), # W3C header propagation for OTel spans
|
|
500
|
+
],
|
|
474
501
|
)
|
|
475
502
|
```
|
|
476
503
|
|
{openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/code-standards.md
RENAMED
|
@@ -83,10 +83,13 @@ This document defines coding standards, architectural patterns, and best practic
|
|
|
83
83
|
async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any:
|
|
84
84
|
info = workflow.info()
|
|
85
85
|
|
|
86
|
-
# Send event via activity (deterministic)
|
|
86
|
+
# Send event via activity (deterministic). NEVER pass credentials
|
|
87
|
+
# (api_key) in activity inputs — workflow history is readable by anyone
|
|
88
|
+
# with Describe permissions on the namespace. The activity holds the
|
|
89
|
+
# credentials on its instance (see rule 3).
|
|
87
90
|
await workflow.execute_activity(
|
|
88
91
|
"send_governance_event",
|
|
89
|
-
args=[{"
|
|
92
|
+
args=[{"payload": {...}, "timeout": 30.0, "on_api_error": "fail_open"}],
|
|
90
93
|
start_to_close_timeout=timedelta(seconds=30),
|
|
91
94
|
)
|
|
92
95
|
|
|
@@ -122,25 +125,36 @@ async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any:
|
|
|
122
125
|
#### Solution: Lazy Imports
|
|
123
126
|
|
|
124
127
|
```python
|
|
125
|
-
# ✅ CORRECT -
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
# ✅ CORRECT - Class-based activity with credentials captured on self
|
|
129
|
+
class GovernanceActivities:
|
|
130
|
+
def __init__(self, api_url: str, api_key: str):
|
|
131
|
+
self._api_url = api_url
|
|
132
|
+
self._api_key = api_key
|
|
133
|
+
|
|
134
|
+
@activity.defn(name="send_governance_event")
|
|
135
|
+
async def send_governance_event(self, input: Dict[str, Any]):
|
|
136
|
+
import httpx # ✅ Loaded only when activity executes
|
|
137
|
+
async with httpx.AsyncClient() as client:
|
|
138
|
+
response = await client.post(
|
|
139
|
+
f"{self._api_url}/api/v1/governance/evaluate",
|
|
140
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
141
|
+
json=input["payload"],
|
|
142
|
+
)
|
|
143
|
+
return response.json()
|
|
134
144
|
```
|
|
135
145
|
|
|
136
146
|
```python
|
|
137
|
-
# ❌ WRONG -
|
|
138
|
-
import httpx # ❌ Loaded when module imports, triggers sandbox
|
|
139
|
-
|
|
147
|
+
# ❌ WRONG - Credentials in activity input (leaks to workflow history)
|
|
140
148
|
@activity.defn(name="send_governance_event")
|
|
141
149
|
async def send_governance_event(input: Dict[str, Any]):
|
|
150
|
+
# ❌ input["api_key"] is persisted in workflow history — readable by
|
|
151
|
+
# anyone with Describe access to the namespace.
|
|
142
152
|
async with httpx.AsyncClient() as client:
|
|
143
|
-
response = await client.post(
|
|
153
|
+
response = await client.post(
|
|
154
|
+
input["api_url"],
|
|
155
|
+
headers={"Authorization": f"Bearer {input['api_key']}"},
|
|
156
|
+
json=input["payload"],
|
|
157
|
+
)
|
|
144
158
|
return response.json()
|
|
145
159
|
```
|
|
146
160
|
|
{openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/codebase-summary.md
RENAMED
|
@@ -328,30 +328,48 @@ def create_openbox_worker(
|
|
|
328
328
|
### 11. Governance Event Activity (`activities.py`)
|
|
329
329
|
|
|
330
330
|
**Purpose:** Execute HTTP calls to OpenBox Core from workflow context
|
|
331
|
-
**Activity:** `send_governance_event`
|
|
331
|
+
**Activity:** `send_governance_event` (method of `GovernanceActivities` class)
|
|
332
332
|
|
|
333
|
-
**
|
|
333
|
+
**Class Signature:**
|
|
334
334
|
```python
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
class GovernanceActivities:
|
|
336
|
+
def __init__(self, api_url: str, api_key: str): ...
|
|
337
|
+
|
|
338
|
+
@activity.defn(name="send_governance_event")
|
|
339
|
+
async def send_governance_event(
|
|
340
|
+
self, input: Dict[str, Any]
|
|
341
|
+
) -> Optional[Dict[str, Any]]: ...
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def build_governance_activities(api_url: str, api_key: str) -> GovernanceActivities
|
|
337
345
|
```
|
|
338
346
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
347
|
+
The plugin / `create_openbox_worker()` factory build one instance and register
|
|
348
|
+
its bound `send_governance_event` method with the Temporal Worker.
|
|
349
|
+
Credentials live on `self` — they do **not** flow through activity inputs,
|
|
350
|
+
so the API key is never written to workflow history.
|
|
351
|
+
|
|
352
|
+
**Input Fields (activity input dict):**
|
|
342
353
|
- `payload` - Event data (without timestamp)
|
|
343
354
|
- `timeout` - Request timeout
|
|
344
355
|
- `on_api_error` - "fail_open" or "fail_closed"
|
|
345
356
|
|
|
357
|
+
> `api_url` / `api_key` are deliberately absent from the input dict to avoid
|
|
358
|
+
> leaking credentials into workflow history. They are held on the activity
|
|
359
|
+
> instance itself.
|
|
360
|
+
|
|
346
361
|
**Behavior:**
|
|
347
362
|
- Adds RFC3339 timestamp to payload
|
|
348
|
-
- POSTs to
|
|
363
|
+
- POSTs to `{self._api_url}/api/v1/governance/evaluate`
|
|
349
364
|
- Parses verdict from response
|
|
350
365
|
- For SignalReceived: Returns verdict dict (workflow interceptor stores it)
|
|
351
366
|
- For other events with BLOCK/HALT: Raises non-retryable `ApplicationError`
|
|
367
|
+
with `type="GovernanceBlock"` / `"GovernanceHalt"`
|
|
352
368
|
- On API failure with fail_closed: Raises `GovernanceAPIError`
|
|
353
369
|
|
|
354
|
-
|
|
370
|
+
A module-level `send_governance_event(input)` helper remains for direct
|
|
371
|
+
callers (tests, scripts) that already hold credentials; it instantiates
|
|
372
|
+
`GovernanceActivities` internally and delegates.
|
|
355
373
|
|
|
356
374
|
---
|
|
357
375
|
|
|
@@ -68,7 +68,7 @@ from .span_processor import WorkflowSpanProcessor
|
|
|
68
68
|
from .workflow_interceptor import GovernanceInterceptor
|
|
69
69
|
|
|
70
70
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
71
|
-
# Plugin (requires temporalio >= 1.
|
|
71
|
+
# Plugin (requires temporalio >= 1.23.0)
|
|
72
72
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
73
73
|
|
|
74
74
|
try:
|
|
@@ -76,7 +76,7 @@ try:
|
|
|
76
76
|
|
|
77
77
|
from .plugin import OpenBoxPlugin
|
|
78
78
|
except ImportError:
|
|
79
|
-
pass # temporalio < 1.
|
|
79
|
+
pass # temporalio < 1.23.0, plugin not available
|
|
80
80
|
|
|
81
81
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
82
82
|
# Verdict Handler
|
|
@@ -139,7 +139,7 @@ from .client import GovernanceClient
|
|
|
139
139
|
__all__ = [
|
|
140
140
|
# Simple Worker Factory (recommended)
|
|
141
141
|
"create_openbox_worker",
|
|
142
|
-
# Plugin (recommended for temporalio >= 1.
|
|
142
|
+
# Plugin (recommended for temporalio >= 1.23.0)
|
|
143
143
|
"OpenBoxPlugin",
|
|
144
144
|
# Configuration
|
|
145
145
|
"initialize",
|
{openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/activities.py
RENAMED
|
@@ -142,60 +142,109 @@ def _handle_api_error(event_type: str, error_msg: str, on_api_error: str) -> dic
|
|
|
142
142
|
return {"success": False, "error": error_msg}
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"""
|
|
148
|
-
Activity that sends governance events to OpenBox Core.
|
|
145
|
+
class GovernanceActivities:
|
|
146
|
+
"""Container for Temporal activities that need OpenBox credentials.
|
|
149
147
|
|
|
150
|
-
|
|
151
|
-
to
|
|
148
|
+
Credentials live on the instance (never in activity inputs → never in
|
|
149
|
+
workflow history → not visible to anyone with namespace read access).
|
|
150
|
+
The worker registers bound methods of a single instance, created at
|
|
151
|
+
worker-init time by the plugin / create_openbox_worker factory.
|
|
152
152
|
"""
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
153
|
+
|
|
154
|
+
def __init__(self, api_url: str, api_key: str):
|
|
155
|
+
self._api_url = api_url.rstrip("/")
|
|
156
|
+
self._api_key = api_key
|
|
157
|
+
|
|
158
|
+
@activity.defn(name="send_governance_event")
|
|
159
|
+
async def send_governance_event(
|
|
160
|
+
self, input: Dict[str, Any]
|
|
161
|
+
) -> Optional[Dict[str, Any]]:
|
|
162
|
+
"""Send a governance event to OpenBox Core.
|
|
163
|
+
|
|
164
|
+
Called from WorkflowInboundInterceptor via workflow.execute_activity()
|
|
165
|
+
to maintain workflow determinism. The `input` dict carries only the
|
|
166
|
+
event payload and per-call policy — never credentials.
|
|
167
|
+
"""
|
|
168
|
+
event_payload = input.get("payload", {})
|
|
169
|
+
timeout = input.get("timeout", 30.0)
|
|
170
|
+
on_api_error = input.get("on_api_error", "fail_open")
|
|
171
|
+
|
|
172
|
+
# Add timestamp in activity context (non-deterministic code allowed)
|
|
173
|
+
payload = {**event_payload, "timestamp": _rfc3339_now()}
|
|
174
|
+
event_type = event_payload.get("event_type", "unknown")
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
178
|
+
response = await client.post(
|
|
179
|
+
f"{self._api_url}/api/v1/governance/evaluate",
|
|
180
|
+
json=payload,
|
|
181
|
+
headers=build_auth_headers(self._api_key),
|
|
176
182
|
)
|
|
177
183
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
verdict, reason, policy_id, risk_score, event_type, event_payload
|
|
184
|
+
if response.status_code != 200:
|
|
185
|
+
return _handle_api_error(
|
|
186
|
+
event_type,
|
|
187
|
+
f"HTTP {response.status_code}: {response.text}",
|
|
188
|
+
on_api_error,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
data = response.json()
|
|
192
|
+
verdict = Verdict.from_string(
|
|
193
|
+
data.get("verdict") or data.get("action", "continue")
|
|
189
194
|
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
reason = data.get("reason")
|
|
196
|
+
policy_id = data.get("policy_id")
|
|
197
|
+
risk_score = data.get("risk_score", 0.0)
|
|
198
|
+
|
|
199
|
+
if verdict.should_stop():
|
|
200
|
+
result = await _handle_stop_verdict(
|
|
201
|
+
verdict,
|
|
202
|
+
reason,
|
|
203
|
+
policy_id,
|
|
204
|
+
risk_score,
|
|
205
|
+
event_type,
|
|
206
|
+
event_payload,
|
|
207
|
+
)
|
|
208
|
+
if result:
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
return _build_verdict_result(verdict, reason, policy_id, risk_score)
|
|
212
|
+
|
|
213
|
+
except (GovernanceAPIError, ApplicationError):
|
|
214
|
+
raise
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.warning(f"Failed to send {event_type} event: {e}")
|
|
217
|
+
if on_api_error == "fail_closed":
|
|
218
|
+
raise GovernanceAPIError(str(e))
|
|
219
|
+
return {"success": False, "error": str(e)}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def build_governance_activities(
|
|
223
|
+
api_url: str, api_key: str
|
|
224
|
+
) -> GovernanceActivities:
|
|
225
|
+
"""Factory used by plugin.py and worker.py to build the activities instance."""
|
|
226
|
+
return GovernanceActivities(api_url=api_url, api_key=api_key)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
230
|
+
# Backward-compat module-level helper.
|
|
231
|
+
#
|
|
232
|
+
# Not decorated with @activity.defn — worker/plugin register the class-based
|
|
233
|
+
# version above so credentials never flow through activity inputs. This shim
|
|
234
|
+
# exists for direct callers (tests, scripts) who already hold credentials and
|
|
235
|
+
# want to invoke the HTTP logic without constructing the class themselves.
|
|
236
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
237
|
+
async def send_governance_event(input: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
238
|
+
"""Backward-compat wrapper — delegates to GovernanceActivities.
|
|
239
|
+
|
|
240
|
+
Tests and direct callers can keep passing api_url/api_key in the input
|
|
241
|
+
dict. The class-based activity registered with the worker does NOT see
|
|
242
|
+
these fields (it reads credentials from self), so nothing written to
|
|
243
|
+
workflow history ever carries the API key.
|
|
244
|
+
"""
|
|
245
|
+
instance = GovernanceActivities(
|
|
246
|
+
api_url=input.get("api_url", ""),
|
|
247
|
+
api_key=input.get("api_key", ""),
|
|
248
|
+
)
|
|
249
|
+
forwarded = {k: v for k, v in input.items() if k not in ("api_url", "api_key")}
|
|
250
|
+
return await instance.send_governance_event(forwarded)
|
|
@@ -68,9 +68,14 @@ def _classify_sql(query: Any) -> str:
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def _generate_span_id() -> str:
|
|
71
|
-
"""Generate a random 16-hex-char span ID for pymongo governance spans.
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
"""Generate a random 16-hex-char span ID for pymongo governance spans.
|
|
72
|
+
|
|
73
|
+
Uses secrets (CSPRNG) rather than random — span IDs are not secrets, but
|
|
74
|
+
the stronger generator silences security scanners without runtime cost
|
|
75
|
+
and removes any chance of cross-trace ID collision under heavy load.
|
|
76
|
+
"""
|
|
77
|
+
import secrets
|
|
78
|
+
return secrets.token_hex(8)
|
|
74
79
|
|
|
75
80
|
|
|
76
81
|
def _build_db_span_data(
|
|
@@ -21,12 +21,26 @@ Hierarchy:
|
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
-
from typing import TYPE_CHECKING, Union
|
|
24
|
+
from typing import TYPE_CHECKING, Final, Union
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
27
27
|
from .types import Verdict
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
31
|
+
# ApplicationError.type values raised by governance activities.
|
|
32
|
+
# Use these constants — never string-match on error messages or
|
|
33
|
+
# exception class names (brittle against locale / reformatting /
|
|
34
|
+
# Temporal's ActivityError wrapping).
|
|
35
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
36
|
+
|
|
37
|
+
GOVERNANCE_HALT_ERROR_TYPE: Final[str] = "GovernanceHalt"
|
|
38
|
+
GOVERNANCE_BLOCK_ERROR_TYPE: Final[str] = "GovernanceBlock"
|
|
39
|
+
GOVERNANCE_API_ERROR_TYPE: Final[str] = "GovernanceAPIError"
|
|
40
|
+
# Legacy alias; kept for histories predating the rename.
|
|
41
|
+
GOVERNANCE_STOP_ERROR_TYPE: Final[str] = "GovernanceStop"
|
|
42
|
+
|
|
43
|
+
|
|
30
44
|
# ═══════════════════════════════════════════════════════════════════
|
|
31
45
|
# Base
|
|
32
46
|
# ═══════════════════════════════════════════════════════════════════
|