openbox-temporal-sdk-python 1.1.1__py3-none-any.whl → 1.2.0__py3-none-any.whl
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/__init__.py +22 -78
- openbox/activities.py +137 -60
- openbox/activity_interceptor.py +123 -199
- openbox/client.py +32 -8
- openbox/config.py +166 -49
- openbox/core_adapter.py +279 -0
- openbox/errors.py +74 -30
- openbox/governance_state.py +101 -0
- openbox/hitl.py +8 -12
- openbox/multi_agent.py +136 -0
- openbox/plugin.py +57 -34
- openbox/request_signing.py +89 -0
- openbox/tracing.py +29 -267
- openbox/types.py +63 -172
- openbox/verdict_handler.py +0 -1
- openbox/worker.py +60 -48
- openbox/workflow_interceptor.py +126 -92
- {openbox_temporal_sdk_python-1.1.1.dist-info → openbox_temporal_sdk_python-1.2.0.dist-info}/METADATA +140 -101
- openbox_temporal_sdk_python-1.2.0.dist-info/RECORD +22 -0
- {openbox_temporal_sdk_python-1.1.1.dist-info → openbox_temporal_sdk_python-1.2.0.dist-info}/WHEEL +1 -1
- openbox/context_propagation.py +0 -56
- openbox/db_governance_hooks.py +0 -771
- openbox/file_governance_hooks.py +0 -396
- openbox/hook_governance.py +0 -427
- openbox/http_governance_hooks.py +0 -859
- openbox/otel_setup.py +0 -474
- openbox/span_processor.py +0 -240
- openbox_temporal_sdk_python-1.1.1.dist-info/RECORD +0 -25
- {openbox_temporal_sdk_python-1.1.1.dist-info → openbox_temporal_sdk_python-1.2.0.dist-info}/licenses/LICENSE +0 -0
openbox/__init__.py
CHANGED
|
@@ -1,38 +1,28 @@
|
|
|
1
1
|
"""OpenBox SDK - Workflow-Boundary Governance with OpenTelemetry"""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
11
|
-
# Simple Factories (recommended)
|
|
12
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
3
|
+
# STATIC on purpose — never read via importlib.metadata. A metadata lookup
|
|
4
|
+
# OPENS A FILE; with file instrumentation active (traced_open patches
|
|
5
|
+
# builtins.open + io.open) that read re-enters governance: eagerly at import
|
|
6
|
+
# it crashes the workflow sandbox as a circular import, lazily it recurses
|
|
7
|
+
# unboundedly from build_auth_headers on every evaluate. Keep in sync with
|
|
8
|
+
# pyproject.toml on release.
|
|
9
|
+
__version__ = "1.2.0"
|
|
13
10
|
|
|
14
11
|
from .worker import create_openbox_worker
|
|
15
12
|
|
|
16
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
17
|
-
# Core Configuration
|
|
18
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
19
|
-
|
|
20
13
|
from .config import (
|
|
21
14
|
initialize,
|
|
22
15
|
get_global_config,
|
|
23
16
|
GovernanceConfig,
|
|
24
17
|
)
|
|
25
18
|
|
|
26
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
27
|
-
# Errors (unified hierarchy in errors.py)
|
|
28
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
29
|
-
|
|
30
19
|
from .errors import (
|
|
31
20
|
OpenBoxError,
|
|
32
21
|
OpenBoxConfigError,
|
|
33
22
|
OpenBoxAuthError,
|
|
34
23
|
OpenBoxNetworkError,
|
|
35
24
|
OpenBoxInsecureURLError,
|
|
25
|
+
OpenBoxSigningError,
|
|
36
26
|
GovernanceBlockedError,
|
|
37
27
|
GovernanceHaltError,
|
|
38
28
|
GovernanceAPIError,
|
|
@@ -41,68 +31,40 @@ from .errors import (
|
|
|
41
31
|
ApprovalRejectedError,
|
|
42
32
|
ApprovalTimeoutError,
|
|
43
33
|
extract_governance_error,
|
|
34
|
+
map_signing_error,
|
|
44
35
|
)
|
|
45
36
|
|
|
46
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
47
|
-
# Types (sandbox-safe - can be imported in workflow code)
|
|
48
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
49
|
-
|
|
50
37
|
from .types import (
|
|
51
38
|
Verdict,
|
|
52
39
|
WorkflowEventType,
|
|
53
|
-
WorkflowSpanBuffer,
|
|
54
40
|
GovernanceVerdictResponse,
|
|
55
41
|
GuardrailsCheckResult,
|
|
56
42
|
)
|
|
57
43
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
from .span_processor import WorkflowSpanProcessor
|
|
63
|
-
|
|
64
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
65
|
-
# Interceptors
|
|
66
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
44
|
+
# Multi-agent primitives (sandbox-safe — only imports temporalio.workflow eagerly;
|
|
45
|
+
# signing/HTTP routed lazily through the governance activity).
|
|
46
|
+
from .multi_agent import emit_handoff
|
|
67
47
|
|
|
68
48
|
from .workflow_interceptor import GovernanceInterceptor
|
|
69
49
|
|
|
70
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
71
|
-
# Plugin (requires temporalio >= 1.24.0)
|
|
72
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
73
|
-
|
|
74
50
|
try:
|
|
75
51
|
from temporalio.plugin import SimplePlugin # noqa: F401 — probe only
|
|
76
52
|
|
|
77
53
|
from .plugin import OpenBoxPlugin
|
|
78
54
|
except ImportError:
|
|
79
|
-
pass # temporalio < 1.
|
|
80
|
-
|
|
81
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
82
|
-
# Verdict Handler
|
|
83
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
55
|
+
pass # temporalio < 1.23.0, plugin not available
|
|
84
56
|
|
|
85
57
|
from .verdict_handler import enforce_verdict, VerdictEnforcementResult
|
|
86
58
|
|
|
87
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
88
|
-
# HITL — Human-in-the-Loop approval helpers
|
|
89
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
90
|
-
|
|
91
59
|
from .hitl import handle_approval_response, raise_approval_pending, should_skip_hitl
|
|
92
60
|
|
|
93
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
94
|
-
# Governance HTTP Client (sandbox-safe — httpx imported lazily inside methods)
|
|
95
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
96
|
-
|
|
97
61
|
from .client import GovernanceClient
|
|
98
62
|
|
|
99
63
|
# NOTE: ActivityGovernanceInterceptor is NOT imported here because it imports
|
|
100
64
|
# OpenTelemetry which uses importlib_metadata -> os.stat, causing sandbox issues.
|
|
101
65
|
# Users must import directly: from openbox.activity_interceptor import ActivityGovernanceInterceptor
|
|
102
66
|
|
|
103
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
104
67
|
# Activities - DO NOT import here!
|
|
105
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
106
68
|
#
|
|
107
69
|
# IMPORTANT: Do NOT import activities from openbox/__init__.py!
|
|
108
70
|
# activities.py imports httpx which uses os.stat internally. If we re-export them
|
|
@@ -111,25 +73,16 @@ from .client import GovernanceClient
|
|
|
111
73
|
# Users must import directly:
|
|
112
74
|
# from openbox.activities import send_governance_event
|
|
113
75
|
|
|
114
|
-
#
|
|
115
|
-
# OTel Setup - NOT imported here to avoid sandbox issues!
|
|
116
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
76
|
+
# Instrumentation — owned by the openbox_core base runtime
|
|
117
77
|
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
78
|
+
# HTTP/DB/file/function hook instrumentation is installed by the base runtime
|
|
79
|
+
# (create_openbox_worker / OpenBoxPlugin call runtime.install_instrumentation()).
|
|
80
|
+
# There is no Temporal-local OpenTelemetry setup to import.
|
|
121
81
|
#
|
|
122
|
-
# Users must import directly: from openbox.otel_setup import setup_opentelemetry_for_governance
|
|
123
|
-
|
|
124
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
125
82
|
# Tracing Decorators - NOT imported here to avoid sandbox issues!
|
|
126
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
127
|
-
#
|
|
128
|
-
# Use the @traced decorator to capture internal function calls as spans.
|
|
129
|
-
# Import directly: from openbox.tracing import traced, create_span
|
|
130
83
|
#
|
|
131
|
-
#
|
|
132
|
-
# from openbox.tracing import traced
|
|
84
|
+
# @traced wraps the base SDK's governed() decorator. Import directly:
|
|
85
|
+
# from openbox.tracing import traced, create_span
|
|
133
86
|
#
|
|
134
87
|
# @traced
|
|
135
88
|
# def my_function(data):
|
|
@@ -137,20 +90,17 @@ from .client import GovernanceClient
|
|
|
137
90
|
|
|
138
91
|
|
|
139
92
|
__all__ = [
|
|
140
|
-
# Simple Worker Factory (recommended)
|
|
141
93
|
"create_openbox_worker",
|
|
142
|
-
# Plugin (recommended for temporalio >= 1.24.0)
|
|
143
94
|
"OpenBoxPlugin",
|
|
144
|
-
# Configuration
|
|
145
95
|
"initialize",
|
|
146
96
|
"get_global_config",
|
|
147
97
|
"GovernanceConfig",
|
|
148
|
-
# Errors (unified hierarchy)
|
|
149
98
|
"OpenBoxError",
|
|
150
99
|
"OpenBoxConfigError",
|
|
151
100
|
"OpenBoxAuthError",
|
|
152
101
|
"OpenBoxNetworkError",
|
|
153
102
|
"OpenBoxInsecureURLError",
|
|
103
|
+
"OpenBoxSigningError",
|
|
154
104
|
"GovernanceBlockedError",
|
|
155
105
|
"GovernanceHaltError",
|
|
156
106
|
"GovernanceAPIError",
|
|
@@ -159,23 +109,17 @@ __all__ = [
|
|
|
159
109
|
"ApprovalRejectedError",
|
|
160
110
|
"ApprovalTimeoutError",
|
|
161
111
|
"extract_governance_error",
|
|
162
|
-
|
|
112
|
+
"map_signing_error",
|
|
163
113
|
"Verdict",
|
|
164
114
|
"WorkflowEventType",
|
|
165
|
-
"WorkflowSpanBuffer",
|
|
166
115
|
"GovernanceVerdictResponse",
|
|
167
116
|
"GuardrailsCheckResult",
|
|
168
|
-
|
|
169
|
-
"WorkflowSpanProcessor",
|
|
170
|
-
# Interceptors
|
|
117
|
+
"emit_handoff",
|
|
171
118
|
"GovernanceInterceptor",
|
|
172
|
-
# Verdict handler
|
|
173
119
|
"enforce_verdict",
|
|
174
120
|
"VerdictEnforcementResult",
|
|
175
|
-
# HITL helpers
|
|
176
121
|
"handle_approval_response",
|
|
177
122
|
"raise_approval_pending",
|
|
178
123
|
"should_skip_hitl",
|
|
179
|
-
# Governance HTTP client
|
|
180
124
|
"GovernanceClient",
|
|
181
125
|
]
|
openbox/activities.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
# openbox/activities.py
|
|
2
|
-
#
|
|
3
|
-
# IMPORTANT: This module imports httpx which uses os.stat internally.
|
|
4
|
-
# Do NOT import this module from workflow code (workflow_interceptor.py)!
|
|
5
|
-
# The workflow interceptor references this activity by string name "send_governance_event".
|
|
6
1
|
"""
|
|
7
2
|
Governance event activity for workflow-level HTTP calls.
|
|
8
3
|
|
|
4
|
+
NOT sandbox-safe: this module imports httpx. Workflow code references the
|
|
5
|
+
activity by string name instead of importing it.
|
|
6
|
+
|
|
9
7
|
CRITICAL: Temporal workflows must be deterministic. HTTP calls are NOT allowed directly
|
|
10
8
|
in workflow code (including interceptors). WorkflowInboundInterceptor sends events via
|
|
11
9
|
workflow.execute_activity() using this activity.
|
|
@@ -29,13 +27,12 @@ from datetime import datetime, timezone
|
|
|
29
27
|
from typing import Dict, Any, Optional
|
|
30
28
|
|
|
31
29
|
|
|
32
|
-
from .types import rfc3339_now as _rfc3339_now
|
|
30
|
+
from .types import rfc3339_now as _rfc3339_now
|
|
33
31
|
|
|
34
32
|
from temporalio import activity
|
|
35
33
|
from temporalio.exceptions import ApplicationError
|
|
36
34
|
|
|
37
35
|
from .types import Verdict
|
|
38
|
-
from .hook_governance import build_auth_headers
|
|
39
36
|
|
|
40
37
|
logger = logging.getLogger(__name__)
|
|
41
38
|
|
|
@@ -50,7 +47,6 @@ def set_temporal_client(client) -> None:
|
|
|
50
47
|
_temporal_client = client
|
|
51
48
|
|
|
52
49
|
|
|
53
|
-
# Re-export from errors.py for backward compatibility
|
|
54
50
|
from .errors import GovernanceAPIError # noqa: F401
|
|
55
51
|
|
|
56
52
|
|
|
@@ -142,60 +138,141 @@ def _handle_api_error(event_type: str, error_msg: str, on_api_error: str) -> dic
|
|
|
142
138
|
return {"success": False, "error": error_msg}
|
|
143
139
|
|
|
144
140
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"""
|
|
148
|
-
Activity that sends governance events to OpenBox Core.
|
|
141
|
+
class GovernanceActivities:
|
|
142
|
+
"""Container for Temporal activities that need OpenBox credentials.
|
|
149
143
|
|
|
150
|
-
|
|
151
|
-
to
|
|
144
|
+
Credentials live on the instance (never in activity inputs → never in
|
|
145
|
+
workflow history → not visible to anyone with namespace read access).
|
|
146
|
+
The worker registers bound methods of a single instance, created at
|
|
147
|
+
worker-init time by the plugin / create_openbox_worker factory.
|
|
152
148
|
"""
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
149
|
+
|
|
150
|
+
def __init__(self, api_url: str, api_key: str, *, agent_did=None, signer=None):
|
|
151
|
+
self._api_url = api_url.rstrip("/")
|
|
152
|
+
self._api_key = api_key
|
|
153
|
+
# AIP signing material — held on the instance so it never flows through
|
|
154
|
+
# activity inputs / workflow history.
|
|
155
|
+
self._agent_did = agent_did
|
|
156
|
+
self._signer = signer
|
|
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
|
+
# Sign once over the exact bytes we transmit (timestamp included).
|
|
177
|
+
from .request_signing import prepare_signed_request
|
|
178
|
+
|
|
179
|
+
headers, body = prepare_signed_request(
|
|
180
|
+
"POST",
|
|
181
|
+
"/api/v1/governance/evaluate",
|
|
182
|
+
payload,
|
|
183
|
+
api_key=self._api_key,
|
|
184
|
+
agent_did=self._agent_did,
|
|
185
|
+
signer=self._signer,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
190
|
+
response = await client.post(
|
|
191
|
+
f"{self._api_url}/api/v1/governance/evaluate",
|
|
192
|
+
content=body,
|
|
193
|
+
headers=headers,
|
|
176
194
|
)
|
|
177
195
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
verdict, reason, policy_id, risk_score, event_type, event_payload
|
|
196
|
+
if response.status_code != 200:
|
|
197
|
+
return _handle_api_error(
|
|
198
|
+
event_type,
|
|
199
|
+
f"HTTP {response.status_code}: {response.text}",
|
|
200
|
+
on_api_error,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
data = response.json()
|
|
204
|
+
verdict = Verdict.from_string(
|
|
205
|
+
data.get("verdict") or data.get("action", "continue")
|
|
189
206
|
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
reason = data.get("reason")
|
|
208
|
+
policy_id = data.get("policy_id")
|
|
209
|
+
risk_score = data.get("risk_score", 0.0)
|
|
210
|
+
|
|
211
|
+
if verdict.should_stop():
|
|
212
|
+
result = await _handle_stop_verdict(
|
|
213
|
+
verdict,
|
|
214
|
+
reason,
|
|
215
|
+
policy_id,
|
|
216
|
+
risk_score,
|
|
217
|
+
event_type,
|
|
218
|
+
event_payload,
|
|
219
|
+
)
|
|
220
|
+
if result:
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
return _build_verdict_result(verdict, reason, policy_id, risk_score)
|
|
224
|
+
|
|
225
|
+
except (GovernanceAPIError, ApplicationError):
|
|
226
|
+
raise
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.warning(f"Failed to send {event_type} event: {e}")
|
|
229
|
+
if on_api_error == "fail_closed":
|
|
230
|
+
raise GovernanceAPIError(str(e))
|
|
231
|
+
return {"success": False, "error": str(e)}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def build_governance_activities(
|
|
235
|
+
api_url: str,
|
|
236
|
+
api_key: str,
|
|
237
|
+
*,
|
|
238
|
+
agent_did=None,
|
|
239
|
+
signer=None,
|
|
240
|
+
) -> GovernanceActivities:
|
|
241
|
+
"""Factory used by plugin.py and worker.py to build the activities instance.
|
|
242
|
+
|
|
243
|
+
agent_did + signer enable AIP signed requests; both stay on the instance
|
|
244
|
+
(never in inputs).
|
|
245
|
+
"""
|
|
246
|
+
# Fall back to the globally-configured signer/DID when omitted (manual setups),
|
|
247
|
+
# so workflow/signal events routed through this activity are signed too.
|
|
248
|
+
from .config import resolve_signing_defaults
|
|
249
|
+
|
|
250
|
+
agent_did, signer = resolve_signing_defaults(agent_did, signer)
|
|
251
|
+
return GovernanceActivities(
|
|
252
|
+
api_url=api_url,
|
|
253
|
+
api_key=api_key,
|
|
254
|
+
agent_did=agent_did,
|
|
255
|
+
signer=signer,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# Backward-compat module-level helper.
|
|
260
|
+
#
|
|
261
|
+
# Not decorated with @activity.defn — worker/plugin register the class-based
|
|
262
|
+
# version above so credentials never flow through activity inputs. This shim
|
|
263
|
+
# exists for direct callers (tests, scripts) who already hold credentials and
|
|
264
|
+
# want to invoke the HTTP logic without constructing the class themselves.
|
|
265
|
+
async def send_governance_event(input: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
266
|
+
"""Backward-compat wrapper — delegates to GovernanceActivities.
|
|
267
|
+
|
|
268
|
+
Tests and direct callers can keep passing api_url/api_key in the input
|
|
269
|
+
dict. The class-based activity registered with the worker does NOT see
|
|
270
|
+
these fields (it reads credentials from self), so nothing written to
|
|
271
|
+
workflow history ever carries the API key.
|
|
272
|
+
"""
|
|
273
|
+
instance = GovernanceActivities(
|
|
274
|
+
api_url=input.get("api_url", ""),
|
|
275
|
+
api_key=input.get("api_key", ""),
|
|
276
|
+
)
|
|
277
|
+
forwarded = {k: v for k, v in input.items() if k not in ("api_url", "api_key")}
|
|
278
|
+
return await instance.send_governance_event(forwarded)
|