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.
Files changed (60) hide show
  1. openbox_temporal_sdk_python-1.1.2/CHANGELOG.md +124 -0
  2. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/PKG-INFO +35 -8
  3. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/README.md +34 -7
  4. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/code-standards.md +29 -15
  5. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/codebase-summary.md +27 -9
  6. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/__init__.py +3 -3
  7. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/activities.py +101 -52
  8. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/db_governance_hooks.py +8 -3
  9. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/errors.py +15 -1
  10. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/hook_governance.py +15 -5
  11. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/plugin.py +36 -11
  12. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/worker.py +33 -20
  13. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/workflow_interceptor.py +77 -50
  14. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/pyproject.toml +1 -1
  15. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_plugin.py +3 -0
  16. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_plugin_integration.py +4 -3
  17. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_worker.py +68 -45
  18. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_workflow_interceptor.py +50 -65
  19. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/uv.lock +12 -12
  20. openbox_temporal_sdk_python-1.1.1/CHANGELOG.md +0 -68
  21. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/.github/workflows/publish.yml +0 -0
  22. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/.github/workflows/sonarqube.yaml +0 -0
  23. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/.gitignore +0 -0
  24. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/.python-version +0 -0
  25. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/.repomixignore +0 -0
  26. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/LICENSE +0 -0
  27. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/changelog-hook-level-governance.md +0 -0
  28. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/configuration.md +0 -0
  29. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/project-overview-pdr.md +0 -0
  30. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/system-architecture.md +0 -0
  31. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/docs/temporal-plugin-integration-guide.md +0 -0
  32. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/activity_interceptor.py +0 -0
  33. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/client.py +0 -0
  34. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/config.py +0 -0
  35. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/context_propagation.py +0 -0
  36. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/file_governance_hooks.py +0 -0
  37. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/hitl.py +0 -0
  38. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/http_governance_hooks.py +0 -0
  39. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/otel_setup.py +0 -0
  40. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/py.typed +0 -0
  41. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/span_processor.py +0 -0
  42. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/tracing.py +0 -0
  43. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/types.py +0 -0
  44. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/openbox/verdict_handler.py +0 -0
  45. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/release-manifest.json +0 -0
  46. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/repomix-output.xml +0 -0
  47. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/sonar-project.properties +0 -0
  48. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/__init__.py +0 -0
  49. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/conftest.py +0 -0
  50. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_activities.py +0 -0
  51. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_activity_interceptor.py +0 -0
  52. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_config.py +0 -0
  53. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_db_governance_hooks.py +0 -0
  54. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_file_governance_hooks.py +0 -0
  55. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_http_body_truncation.py +0 -0
  56. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_otel_setup.py +0 -0
  57. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_psycopg2_hooks_verify.py +0 -0
  58. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_span_processor.py +0 -0
  59. {openbox_temporal_sdk_python-1.1.1 → openbox_temporal_sdk_python-1.1.2}/tests/test_tracing.py +0 -0
  60. {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.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, and the `send_governance_event` activity.
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. Adds `send_governance_event` activity
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 send_governance_event
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. Create worker
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=[workflow_interceptor, activity_interceptor],
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, and the `send_governance_event` activity.
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. Adds `send_governance_event` activity
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 send_governance_event
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. Create worker
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=[workflow_interceptor, activity_interceptor],
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
 
@@ -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=[{"api_url": api_url, "payload": {...}}],
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 - Lazy import in activity function
126
- @activity.defn(name="send_governance_event")
127
- async def send_governance_event(input: Dict[str, Any]):
128
- import httpx # ✅ Loaded only when activity executes
129
- from datetime import datetime, timezone # ✅ Lazy import
130
-
131
- async with httpx.AsyncClient() as client:
132
- response = await client.post(...)
133
- return response.json()
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 - Module-level import
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
 
@@ -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
- **Function Signature:**
333
+ **Class Signature:**
334
334
  ```python
335
- @activity.defn(name="send_governance_event")
336
- async def send_governance_event(input: Dict[str, Any]) -> Optional[Dict[str, Any]]
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
- **Input Fields:**
340
- - `api_url` - OpenBox Core URL
341
- - `api_key` - Bearer token
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 `/api/v1/governance/evaluate`
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
- **Lines of Code:** 163
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.24.0)
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.24.0, plugin not available
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.24.0)
142
+ # Plugin (recommended for temporalio >= 1.23.0)
143
143
  "OpenBoxPlugin",
144
144
  # Configuration
145
145
  "initialize",
@@ -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
- @activity.defn(name="send_governance_event")
146
- async def send_governance_event(input: Dict[str, Any]) -> Optional[Dict[str, Any]]:
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
- Called from WorkflowInboundInterceptor via workflow.execute_activity()
151
- to maintain workflow determinism.
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
- api_url = input.get("api_url", "")
154
- api_key = input.get("api_key", "")
155
- event_payload = input.get("payload", {})
156
- timeout = input.get("timeout", 30.0)
157
- on_api_error = input.get("on_api_error", "fail_open")
158
-
159
- # Add timestamp in activity context (non-deterministic code allowed)
160
- payload = {**event_payload, "timestamp": _rfc3339_now()}
161
- event_type = event_payload.get("event_type", "unknown")
162
-
163
- try:
164
- async with httpx.AsyncClient(timeout=timeout) as client:
165
- response = await client.post(
166
- f"{api_url}/api/v1/governance/evaluate",
167
- json=payload,
168
- headers=build_auth_headers(api_key),
169
- )
170
-
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,
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
- 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
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
- if result:
191
- return result
192
-
193
- return _build_verdict_result(verdict, reason, policy_id, risk_score)
194
-
195
- except (GovernanceAPIError, ApplicationError):
196
- raise
197
- except Exception as e:
198
- logger.warning(f"Failed to send {event_type} event: {e}")
199
- if on_api_error == "fail_closed":
200
- raise GovernanceAPIError(str(e))
201
- return {"success": False, "error": str(e)}
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
- import random
73
- return format(random.getrandbits(64), "016x")
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
  # ═══════════════════════════════════════════════════════════════════