openbox-sdk-python 1.3.0__tar.gz → 1.4.0__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 (159) hide show
  1. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/CHANGELOG.md +17 -0
  2. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/PKG-INFO +18 -2
  3. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/README.md +16 -0
  4. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/__init__.py +1 -1
  5. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/contracts/events.py +11 -4
  6. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/http.py +134 -36
  7. openbox_sdk_python-1.4.0/openbox_core/instrumentation/http_stream.py +185 -0
  8. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/manager.py +3 -0
  9. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/pyproject.toml +3 -2
  10. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/contracts/test_event_classify.py +1 -1
  11. openbox_sdk_python-1.4.0/tests/contracts/test_lifecycle_errors.py +47 -0
  12. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_http_preflight_block.py +141 -27
  13. openbox_sdk_python-1.4.0/tests/instrumentation/test_http_stream_capture.py +245 -0
  14. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_manager_and_otel_lifecycle.py +63 -4
  15. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/test_import_safety.py +1 -0
  16. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/test_sdk_version.py +11 -0
  17. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/uv.lock +101 -50
  18. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/.github/instructions/openbox-sdk-python.instructions.md +0 -0
  19. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/.github/workflows/ci.yml +0 -0
  20. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/.github/workflows/publish.yml +0 -0
  21. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/.gitignore +0 -0
  22. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/.python-version +0 -0
  23. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/LICENSE +0 -0
  24. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/adapters/__init__.py +0 -0
  25. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/adapters/base.py +0 -0
  26. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/approvals.py +0 -0
  27. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/bootstrap.py +0 -0
  28. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/client.py +0 -0
  29. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/config.py +0 -0
  30. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/conformance/__init__.py +0 -0
  31. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/conformance/fake_core.py +0 -0
  32. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/conformance/hook_preflight.py +0 -0
  33. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/conformance/instrumentation.py +0 -0
  34. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/context.py +0 -0
  35. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/contracts/__init__.py +0 -0
  36. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/contracts/context.py +0 -0
  37. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/contracts/otel_spans.py +0 -0
  38. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/contracts/results.py +0 -0
  39. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/errors.py +0 -0
  40. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/gate.py +0 -0
  41. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/hooks/__init__.py +0 -0
  42. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/hooks/events.py +0 -0
  43. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/hooks/preflight.py +0 -0
  44. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/hooks/wrappers.py +0 -0
  45. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/identity.py +0 -0
  46. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/identity_okta.py +0 -0
  47. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/identity_transitions.py +0 -0
  48. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/identity_types.py +0 -0
  49. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/__init__.py +0 -0
  50. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/db.py +0 -0
  51. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/file.py +0 -0
  52. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/function.py +0 -0
  53. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/llm.py +0 -0
  54. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/shared.py +0 -0
  55. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/jwk_thumbprint.py +0 -0
  56. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/__init__.py +0 -0
  57. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/propagation.py +0 -0
  58. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/provider.py +0 -0
  59. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/setup.py +0 -0
  60. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/span_processor.py +0 -0
  61. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/trace_context.py +0 -0
  62. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/py.typed +0 -0
  63. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/runtime.py +0 -0
  64. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/sdk_version.py +0 -0
  65. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/serialization.py +0 -0
  66. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/validation/__init__.py +0 -0
  67. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/validation/diagnostics.py +0 -0
  68. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/validation/event_rules.py +0 -0
  69. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/validation/registry.py +0 -0
  70. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/validation/span_normalization.py +0 -0
  71. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/wire/__init__.py +0 -0
  72. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/wire/core_span.py +0 -0
  73. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/wire/evaluate_payload.py +0 -0
  74. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/workload_identity.py +0 -0
  75. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/identity_fixtures.py +0 -0
  76. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_approval_poll.py +0 -0
  77. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_auth_failure_classification.py +0 -0
  78. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_bootstrap.py +0 -0
  79. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_fail_modes.py +0 -0
  80. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_keycloak_workload_identity.py +0 -0
  81. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_transition_preflight.py +0 -0
  82. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_v2_endpoint_selection.py +0 -0
  83. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/config/test_bootstrap_mode.py +0 -0
  84. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/config/test_identity_verification_method.py +0 -0
  85. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/config/test_resolution_order.py +0 -0
  86. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/conformance/test_required_cases.py +0 -0
  87. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/context/test_bind_reset.py +0 -0
  88. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/context/test_trace_key.py +0 -0
  89. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/contracts/test_approval_parsing.py +0 -0
  90. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/contracts/test_patch.py +0 -0
  91. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/contracts/test_result_parsing.py +0 -0
  92. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/gate/test_diagnostics.py +0 -0
  93. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/gate/test_strict_failures.py +0 -0
  94. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/conftest.py +0 -0
  95. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/instrumented_env.py +0 -0
  96. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_db_block.py +0 -0
  97. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_db_redis_mongo.py +0 -0
  98. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_file_function_block.py +0 -0
  99. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_hook_failclosed_and_sync_approval.py +0 -0
  100. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_hook_runtime.py +0 -0
  101. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_http_urllib.py +0 -0
  102. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/runtime/test_runtime_delegation.py +0 -0
  103. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/generate_golden_fixture_from_temporal_signer.py +0 -0
  104. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/golden_temporal_signed_request.json +0 -0
  105. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/MANIFEST.sha256 +0 -0
  106. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/approval.json +0 -0
  107. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/auth-validate.json +0 -0
  108. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/evaluate.json +0 -0
  109. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/handoff.json +0 -0
  110. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/keypair.json +0 -0
  111. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/README.md +0 -0
  112. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/alg-none.json +0 -0
  113. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/alg-symmetric.json +0 -0
  114. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/body-tampered.json +0 -0
  115. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/embedded-jku.json +0 -0
  116. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/embedded-jwk.json +0 -0
  117. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/embedded-x5u.json +0 -0
  118. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/empty-jti.json +0 -0
  119. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/excessive-lifetime.json +0 -0
  120. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/expired.json +0 -0
  121. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/future-iat.json +0 -0
  122. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/htu-with-host.json +0 -0
  123. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/htu-with-query.json +0 -0
  124. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/malformed-jwt-truncated.json +0 -0
  125. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/malformed-jwt.json +0 -0
  126. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/missing-assertion.json +0 -0
  127. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/missing-exp.json +0 -0
  128. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/normal-assertion-on-transition-proof-route.json +0 -0
  129. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/paired-with-different-api-key.json +0 -0
  130. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/signature-by-other-key.json +0 -0
  131. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/transition-proof-on-validate-route.json +0 -0
  132. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/transition-proof-signed-by-other-credential.json +0 -0
  133. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/undersized-rsa-key.json +0 -0
  134. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/unknown-kid.json +0 -0
  135. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/v1-path.json +0 -0
  136. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-audience.json +0 -0
  137. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-deployment-id.json +0 -0
  138. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-http-method.json +0 -0
  139. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-iss.json +0 -0
  140. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-path.json +0 -0
  141. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-signed-agent-id.json +0 -0
  142. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-signed-organization-id.json +0 -0
  143. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-sub.json +0 -0
  144. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-transition-challenge.json +0 -0
  145. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-transition-purpose.json +0 -0
  146. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-typ.json +0 -0
  147. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/transition-proof.json +0 -0
  148. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/test_golden_signing.py +0 -0
  149. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/test_identity_okta_unit.py +0 -0
  150. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/test_identity_v2_fixtures.py +0 -0
  151. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/test_identity_v2_signing.py +0 -0
  152. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/test_jwk_thumbprint.py +0 -0
  153. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/go_spandata_compat/go.mod +0 -0
  154. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/go_spandata_compat/main.go +0 -0
  155. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/span_fixtures.py +0 -0
  156. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/test_backend_compat.py +0 -0
  157. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/test_core_span_projection.py +0 -0
  158. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/test_flat_hook_contract.py +0 -0
  159. {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/test_hex_ids.py +0 -0
@@ -7,7 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Fixed
11
+ - Capture streamed text response bodies for sync/async `httpx` and `httpx2`
12
+ requests without consuming the stream ahead of the caller. Emit completed
13
+ telemetry once on EOF, close, or read failure, with bounded body capture and
14
+ the original span/activity identity preserved.
15
+
16
+ ## [1.4.0] - 2026-10-02
17
+
10
18
  ### Added
19
+ - HTTP span capture for `httpx2.Client` and `httpx2.AsyncClient` when `httpx2`
20
+ is installed, alongside `httpx`, using the existing HTTP toggle and OpenBox
21
+ started/completed payload contract.
11
22
  - **Okta identity bootstrap** — an `okta_ai_agent` runtime now needs only
12
23
  `OPENBOX_API_URL`, `OPENBOX_API_KEY`, and `OPENBOX_OKTA_AGENT_PRIVATE_KEY`. The SDK
13
24
  fetches the remaining identity metadata (agent id, organization id, deployment id,
@@ -41,6 +52,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
41
52
  - No failure path downgrades to v1, to an unsigned request, or to a different identity
42
53
  method. A Core that predates the endpoint (`404`) produces actionable upgrade guidance.
43
54
 
55
+ ## [1.3.1] - 2026-09-17
56
+
57
+ ### Security
58
+ - Require `cryptography>=50,<51` to address CVE-2026-69247. No OpenBox SDK API or
59
+ runtime behavior changes are included in this patch release.
60
+
44
61
  ## [1.3.0] - 2026-07-30
45
62
 
46
63
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: openbox-sdk-python
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: OpenBox base SDK - governance contracts, strict gate, identity/signing, evaluate client, context runtime, OTel span wire serialization, and generic instrumentation shared by every OpenBox framework SDK
5
5
  Author-email: OpenBox Team <tino@openbox.ai>
6
6
  License-Expression: MIT
@@ -15,7 +15,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
15
  Classifier: Topic :: System :: Monitoring
16
16
  Classifier: Typing :: Typed
17
17
  Requires-Python: >=3.11
18
- Requires-Dist: cryptography<50,>=48.0.1
18
+ Requires-Dist: cryptography<51,>=50
19
19
  Requires-Dist: httpx<1,>=0.28.0
20
20
  Requires-Dist: opentelemetry-api<1.40.0,>=1.38.0
21
21
  Requires-Dist: opentelemetry-sdk<1.40.0,>=1.38.0
@@ -82,6 +82,22 @@ uv sync --extra http # + HTTP instrumentation targets
82
82
  uv sync --extra db # + DB instrumentation targets
83
83
  ```
84
84
 
85
+ HTTP instrumentation also captures `httpx2.Client` and `httpx2.AsyncClient`
86
+ requests when `httpx2` is installed by the host application. It uses the same
87
+ HTTP toggle and started/completed span payloads as `httpx`, including body
88
+ capture and credential-header redaction.
89
+
90
+ For streamed `httpx`/`httpx2` responses, completed telemetry is emitted when the
91
+ caller finishes reading the decoded body or closes the response. Text chunks are
92
+ copied as they pass through; the SDK does not drain or buffer the entire stream.
93
+ SSE responses retain their raw event text in `response_body`. Capture respects
94
+ `privacy.max_body_size` (characters); when that limit is disabled, streaming
95
+ capture still uses a 65,536-character bound. Binary bodies are not captured.
96
+ A response closed before transport EOF records the consumed prefix and sets
97
+ `attributes["openbox.http.response_body.partial"]` to `true`. This includes clients
98
+ that stop reading after an SSE end marker and then close the HTTP response.
99
+ Stream read failures retain their original exception and report partial evidence.
100
+
85
101
  ## Okta AI Agent identity (`okta_ai_agent`)
86
102
 
87
103
  An agent whose OpenBox identity is verified against an Okta AI Agent credential
@@ -43,6 +43,22 @@ uv sync --extra http # + HTTP instrumentation targets
43
43
  uv sync --extra db # + DB instrumentation targets
44
44
  ```
45
45
 
46
+ HTTP instrumentation also captures `httpx2.Client` and `httpx2.AsyncClient`
47
+ requests when `httpx2` is installed by the host application. It uses the same
48
+ HTTP toggle and started/completed span payloads as `httpx`, including body
49
+ capture and credential-header redaction.
50
+
51
+ For streamed `httpx`/`httpx2` responses, completed telemetry is emitted when the
52
+ caller finishes reading the decoded body or closes the response. Text chunks are
53
+ copied as they pass through; the SDK does not drain or buffer the entire stream.
54
+ SSE responses retain their raw event text in `response_body`. Capture respects
55
+ `privacy.max_body_size` (characters); when that limit is disabled, streaming
56
+ capture still uses a 65,536-character bound. Binary bodies are not captured.
57
+ A response closed before transport EOF records the consumed prefix and sets
58
+ `attributes["openbox.http.response_body.partial"]` to `true`. This includes clients
59
+ that stop reading after an SSE end marker and then close the HTTP response.
60
+ Stream read failures retain their original exception and report partial evidence.
61
+
46
62
  ## Okta AI Agent identity (`okta_ai_agent`)
47
63
 
48
64
  An agent whose OpenBox identity is verified against an Okta AI Agent credential
@@ -37,7 +37,7 @@ from .errors import (
37
37
  # governance; eagerly it can deadlock package init as a circular import, and
38
38
  # lazily it can recurse unboundedly when a per-request header builder resolves
39
39
  # the version. Keep in sync with pyproject.toml on release.
40
- __version__ = "1.3.0"
40
+ __version__ = "1.4.0"
41
41
 
42
42
  __all__ = [
43
43
  "__version__",
@@ -221,12 +221,19 @@ def workflow_completed(
221
221
  )
222
222
 
223
223
 
224
+ def _error_payload(error: str | Mapping[str, Any]) -> dict[str, Any]:
225
+ """Core expects ErrorInfo for lifecycle failures, never a bare string."""
226
+ if isinstance(error, str):
227
+ return {"type": "Exception", "message": error}
228
+ return dict(error)
229
+
230
+
224
231
  def workflow_failed(
225
232
  *,
226
233
  workflow_id: str,
227
234
  run_id: str,
228
235
  workflow_type: str,
229
- error: str | None = None,
236
+ error: str | Mapping[str, Any] | None = None,
230
237
  task_queue: str | None = None,
231
238
  multi_agent_session_id: str | None = None,
232
239
  timestamp: str | None = None,
@@ -237,7 +244,7 @@ def workflow_failed(
237
244
  workflow_id, run_id, workflow_type, task_queue, multi_agent_session_id, extra
238
245
  )
239
246
  if error is not None:
240
- payload["error"] = error
247
+ payload["error"] = _error_payload(error)
241
248
  return EventEnvelope(
242
249
  event_type=EventType.WORKFLOW_FAILED, payload=payload, timestamp=timestamp
243
250
  )
@@ -283,7 +290,7 @@ def activity_completed(
283
290
  activity_type: str,
284
291
  task_queue: str | None = None,
285
292
  result: Any = None,
286
- error: str | None = None,
293
+ error: str | Mapping[str, Any] | None = None,
287
294
  attempt: int | None = None,
288
295
  multi_agent_session_id: str | None = None,
289
296
  timestamp: str | None = None,
@@ -301,7 +308,7 @@ def activity_completed(
301
308
  if result is not None:
302
309
  payload["result"] = result
303
310
  if error is not None:
304
- payload["error"] = error
311
+ payload["error"] = _error_payload(error)
305
312
  if attempt is not None:
306
313
  payload["attempt"] = attempt
307
314
  return EventEnvelope(
@@ -1,8 +1,8 @@
1
- """HTTP wrappers — requests + httpx (sync/async) via OTel instrumentor hooks.
1
+ """HTTP wrappers — requests + httpx/httpx2 (sync/async).
2
2
 
3
- The instrumentor creates the OTel span and invokes our request hook BEFORE
4
- the real request is sent; raising from the hook (via hook runtime -> adapter)
5
- prevents the request. The wrapper itself never interprets verdicts.
3
+ Each wrapper creates or receives an OTel span and invokes the hook runtime
4
+ BEFORE the real request is sent; raising via the adapter prevents the request.
5
+ The wrapper itself never interprets verdicts.
6
6
 
7
7
  Self-instrumentation guard: URLs under any ignored prefix (always including
8
8
  the OpenBox ``api_url``) are skipped so evaluate calls never govern
@@ -13,11 +13,15 @@ from __future__ import annotations
13
13
 
14
14
  import contextlib
15
15
  import contextvars
16
+ import importlib
16
17
  import logging
17
18
  import time
18
19
  from typing import Any
19
20
 
21
+ from ..context import activity_scope
20
22
  from ..contracts.otel_spans import HookType
23
+ from ..hooks.events import resolve_context
24
+ from .http_stream import capture_async_response_stream, capture_response_stream
21
25
  from .shared import get_hook_runtime
22
26
 
23
27
  logger = logging.getLogger(__name__)
@@ -33,6 +37,8 @@ __all__ = [
33
37
  "uninstall_httpx",
34
38
  "install_httpx_body_capture",
35
39
  "uninstall_httpx_body_capture",
40
+ "install_httpx2_body_capture",
41
+ "uninstall_httpx2_body_capture",
36
42
  "install_urllib3",
37
43
  "uninstall_urllib3",
38
44
  "install_urllib",
@@ -386,14 +392,17 @@ def uninstall_httpx() -> None:
386
392
  logger.debug("httpx uninstrument skipped", exc_info=True)
387
393
 
388
394
 
389
- # ── httpx body capture (Client.send / AsyncClient.send patch) ────────────────
395
+ # ── httpx/httpx2 body capture (Client.send / AsyncClient.send patch) ──────────
390
396
  #
391
397
  # Separate from OTel instrumentation: OTel hooks receive streams that cannot be
392
398
  # consumed. Patching send lets us read the request body (buffered before send)
393
- # and the response body (cached by httpx after a non-streaming send) safely.
399
+ # and the response body safely. Unread streams are observed as the caller reads
400
+ # them; completed telemetry waits for stream consumption or explicit close.
394
401
 
395
- _original_httpx_send: Any = None
396
- _original_httpx_async_send: Any = None
402
+ # Each library has distinct client classes. Keep its originals independent,
403
+ # including in the wrapper closures, so installing/restoring one cannot affect
404
+ # requests through the other.
405
+ _original_httpx_sends: dict[str, tuple[Any, Any, Any]] = {}
397
406
 
398
407
 
399
408
  def _pop_httpx_span() -> Any:
@@ -486,31 +495,59 @@ def _httpx_completed_fields(
486
495
  }
487
496
 
488
497
 
498
+ def _stream_completed_fields(
499
+ request: Any, response: Any, span: Any, start: float,
500
+ request_body: str | None, request_headers: dict | None,
501
+ body: str | None, error: str | None, partial: bool,
502
+ ) -> dict:
503
+ with contextlib.suppress(Exception):
504
+ span.set_attribute("openbox.http.response_body.partial", partial)
505
+ fields = _httpx_completed_fields(
506
+ request, response, int((time.perf_counter() - start) * 1e9),
507
+ request_body, request_headers, body, sanitize_headers(response.headers),
508
+ )
509
+ if error is not None:
510
+ fields["error"] = error
511
+ return fields
512
+
513
+
489
514
  def install_httpx_body_capture() -> bool:
490
- """Patch httpx ``Client.send``/``AsyncClient.send`` for completed body
491
- capture. Idempotent; must be installed AFTER ``install_httpx`` so the
492
- captured original send already carries the OTel request hook."""
493
- global _original_httpx_send, _original_httpx_async_send
494
- if _original_httpx_send is not None:
515
+ """Capture both stages for httpx; install AFTER ``install_httpx``."""
516
+ return _install_httpx_body_capture("httpx")
517
+
518
+
519
+ def install_httpx2_body_capture() -> bool:
520
+ """Capture both stages for httpx2 when installed, using the same wire shape.
521
+
522
+ The send wrapper creates its own OTel span, so this does not require a
523
+ newer OTel HTTPX instrumentor with native httpx2 support.
524
+ """
525
+ return _install_httpx_body_capture("httpx2")
526
+
527
+
528
+ def _install_httpx_body_capture(module_name: str) -> bool:
529
+ if module_name in _original_httpx_sends:
495
530
  return True
496
531
  try:
497
- import httpx
532
+ library = importlib.import_module(module_name)
498
533
  except ImportError:
499
- logger.info("httpx not available for body capture — deferred")
534
+ logger.info("%s not available for body capture — deferred", module_name)
500
535
  return False
501
536
 
502
- _original_httpx_send = httpx.Client.send
503
- _original_httpx_async_send = httpx.AsyncClient.send
537
+ original_send = library.Client.send
538
+ original_async_send = library.AsyncClient.send
504
539
 
505
540
  def _patched_send(self: Any, request: Any, *args: Any, **kwargs: Any) -> Any:
506
541
  runtime = get_hook_runtime()
507
542
  url = str(getattr(request, "url", "") or "")
508
543
  if runtime is None or should_ignore_url(url):
509
- return _original_httpx_send(self, request, *args, **kwargs)
544
+ return original_send(self, request, *args, **kwargs)
510
545
  request_body, request_headers = _capture_httpx_request(request)
511
546
  span = _start_httpx_span(request)
547
+ context = resolve_context(runtime._store, span)
512
548
  patch_token = _httpx_patch_span_var.set(span)
513
549
  start = time.perf_counter()
550
+ deferred = False
514
551
  try:
515
552
  runtime.preflight(
516
553
  span,
@@ -523,7 +560,32 @@ def install_httpx_body_capture() -> bool:
523
560
  "request_body": request_body,
524
561
  },
525
562
  )
526
- response = _original_httpx_send(self, request, *args, **kwargs)
563
+ response = original_send(self, request, *args, **kwargs)
564
+ if not response.is_stream_consumed:
565
+ # An unbound request must not acquire another activity's
566
+ # context just because its body is consumed later.
567
+ if context is None or not context.activity_id or not context.activity_type:
568
+ return response
569
+
570
+ def complete(body: str | None, error: str | None, partial: bool) -> None:
571
+ try:
572
+ fields = _stream_completed_fields(
573
+ request, response, span, start, request_body, request_headers,
574
+ body, error, partial,
575
+ )
576
+ with activity_scope(context, store=runtime._store):
577
+ runtime.completed(span, hook_type=HookType.HTTP_REQUEST, fields=fields)
578
+ finally:
579
+ span.end()
580
+
581
+ capture_response_stream(
582
+ response,
583
+ max_chars=runtime._runtime.config.privacy.max_body_size,
584
+ capture_text=_is_text_content_type(response.headers.get("content-type")),
585
+ complete=complete,
586
+ )
587
+ deferred = True
588
+ return response
527
589
  duration_ns = int((time.perf_counter() - start) * 1e9)
528
590
  response_body, response_headers = _capture_httpx_response(response)
529
591
  runtime.completed(
@@ -542,18 +604,21 @@ def install_httpx_body_capture() -> bool:
542
604
  return response
543
605
  finally:
544
606
  _httpx_patch_span_var.reset(patch_token)
545
- with contextlib.suppress(Exception):
546
- span.end()
607
+ if not deferred:
608
+ with contextlib.suppress(Exception):
609
+ span.end()
547
610
 
548
611
  async def _patched_async_send(self: Any, request: Any, *args: Any, **kwargs: Any) -> Any:
549
612
  runtime = get_hook_runtime()
550
613
  url = str(getattr(request, "url", "") or "")
551
614
  if runtime is None or should_ignore_url(url):
552
- return await _original_httpx_async_send(self, request, *args, **kwargs)
615
+ return await original_async_send(self, request, *args, **kwargs)
553
616
  request_body, request_headers = _capture_httpx_request(request)
554
617
  span = _start_httpx_span(request)
618
+ context = resolve_context(runtime._store, span)
555
619
  patch_token = _httpx_patch_span_var.set(span)
556
620
  start = time.perf_counter()
621
+ deferred = False
557
622
  try:
558
623
  await runtime.apreflight(
559
624
  span,
@@ -566,7 +631,32 @@ def install_httpx_body_capture() -> bool:
566
631
  "request_body": request_body,
567
632
  },
568
633
  )
569
- response = await _original_httpx_async_send(self, request, *args, **kwargs)
634
+ response = await original_async_send(self, request, *args, **kwargs)
635
+ if not response.is_stream_consumed:
636
+ if context is None or not context.activity_id or not context.activity_type:
637
+ return response
638
+
639
+ async def complete(body: str | None, error: str | None, partial: bool) -> None:
640
+ try:
641
+ fields = _stream_completed_fields(
642
+ request, response, span, start, request_body, request_headers,
643
+ body, error, partial,
644
+ )
645
+ with activity_scope(context, store=runtime._store):
646
+ await runtime.acompleted(
647
+ span, hook_type=HookType.HTTP_REQUEST, fields=fields
648
+ )
649
+ finally:
650
+ span.end()
651
+
652
+ capture_async_response_stream(
653
+ response,
654
+ max_chars=runtime._runtime.config.privacy.max_body_size,
655
+ capture_text=_is_text_content_type(response.headers.get("content-type")),
656
+ complete=complete,
657
+ )
658
+ deferred = True
659
+ return response
570
660
  duration_ns = int((time.perf_counter() - start) * 1e9)
571
661
  response_body, response_headers = _capture_httpx_response(response)
572
662
  await runtime.acompleted(
@@ -585,29 +675,37 @@ def install_httpx_body_capture() -> bool:
585
675
  return response
586
676
  finally:
587
677
  _httpx_patch_span_var.reset(patch_token)
588
- with contextlib.suppress(Exception):
589
- span.end()
678
+ if not deferred:
679
+ with contextlib.suppress(Exception):
680
+ span.end()
590
681
 
591
- httpx.Client.send = _patched_send
592
- httpx.AsyncClient.send = _patched_async_send
682
+ library.Client.send = _patched_send
683
+ library.AsyncClient.send = _patched_async_send
684
+ _original_httpx_sends[module_name] = (library, original_send, original_async_send)
593
685
  return True
594
686
 
595
687
 
596
688
  def uninstall_httpx_body_capture() -> None:
597
689
  """Restore httpx send (idempotent). Call BEFORE ``uninstall_httpx`` so the
598
690
  original chain is unwound in reverse install order."""
599
- global _original_httpx_send, _original_httpx_async_send
600
- if _original_httpx_send is None:
691
+ _uninstall_httpx_body_capture("httpx")
692
+
693
+
694
+ def uninstall_httpx2_body_capture() -> None:
695
+ """Restore httpx2 send independently of httpx (idempotent)."""
696
+ _uninstall_httpx_body_capture("httpx2")
697
+
698
+
699
+ def _uninstall_httpx_body_capture(module_name: str) -> None:
700
+ originals = _original_httpx_sends.pop(module_name, None)
701
+ if originals is None:
601
702
  return
703
+ library, original_send, original_async_send = originals
602
704
  try:
603
- import httpx
604
-
605
- httpx.Client.send = _original_httpx_send
606
- httpx.AsyncClient.send = _original_httpx_async_send
705
+ library.Client.send = original_send
706
+ library.AsyncClient.send = original_async_send
607
707
  except Exception:
608
- logger.debug("httpx body-capture restore skipped", exc_info=True)
609
- _original_httpx_send = None
610
- _original_httpx_async_send = None
708
+ logger.debug("%s body-capture restore skipped", module_name, exc_info=True)
611
709
 
612
710
 
613
711
  # ── urllib3 (OTel URLLib3Instrumentor hooks) ─────────────────────────────────
@@ -0,0 +1,185 @@
1
+ """Bounded response capture while HTTPX callers consume decoded stream bytes.
2
+
3
+ Wrap response iterators instead of buffering the transport: HTTPX keeps ownership
4
+ of decompression, backpressure, and connection cleanup. Its raw iterator closes
5
+ the response before the decoded iterator flushes, so defer that internal close's
6
+ completion until the outer iterator finishes. An explicit close between chunks
7
+ completes immediately with the captured prefix.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import codecs
13
+ import logging
14
+ from collections.abc import Awaitable, Callable
15
+ from typing import Any
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class _Capture:
21
+ def __init__(self, response: Any, max_chars: int, capture_text: bool):
22
+ # Streaming capture must remain bounded even when ordinary body
23
+ # truncation is disabled. Keep one extra character for the wire gate's
24
+ # existing truncation diagnostic when a positive limit is configured.
25
+ self.limit = max_chars if max_chars and max_chars > 0 else 65536
26
+ self.remaining = self.limit + (1 if max_chars and max_chars > 0 else 0)
27
+ self.parts: list[str] = []
28
+ self.in_next = False
29
+ self.eof = False
30
+ self.finished = False
31
+ self.error: str | None = None
32
+ self.decoder = None
33
+ if capture_text:
34
+ try:
35
+ decoder = codecs.getincrementaldecoder(response.encoding or "utf-8")
36
+ except LookupError:
37
+ decoder = codecs.getincrementaldecoder("utf-8")
38
+ self.decoder = decoder(errors="replace")
39
+
40
+ def _append(self, text: str) -> None:
41
+ part = text[: self.remaining]
42
+ if part:
43
+ self.parts.append(part)
44
+ self.remaining -= len(part)
45
+
46
+ def feed(self, chunk: bytes) -> None:
47
+ if self.decoder is None or self.finished:
48
+ return
49
+ # Decode in bounded slices, including when a transport yields a large
50
+ # chunk. Incremental decoding preserves characters split across chunks.
51
+ for offset in range(0, len(chunk), 4096):
52
+ if self.remaining <= 0:
53
+ break
54
+ self._append(self.decoder.decode(chunk[offset : offset + 4096]))
55
+
56
+ def failed(self, error: BaseException) -> None:
57
+ if not isinstance(error, GeneratorExit) and self.error is None:
58
+ self.error = f"{type(error).__name__}: {error}"
59
+
60
+ def body(self) -> str | None:
61
+ if self.decoder is None:
62
+ return None
63
+ if self.remaining > 0:
64
+ self._append(self.decoder.decode(b"", final=True))
65
+ return "".join(self.parts)
66
+
67
+
68
+ def capture_response_stream(
69
+ response: Any,
70
+ *,
71
+ max_chars: int,
72
+ capture_text: bool,
73
+ complete: Callable[[str | None, str | None, bool], None],
74
+ ) -> None:
75
+ """Capture sync decoded bytes and complete once at EOF, close, or error."""
76
+ capture = _Capture(response, max_chars, capture_text)
77
+ original_iter = response.iter_bytes
78
+ original_close = response.close
79
+
80
+ def finish() -> None:
81
+ if capture.finished or capture.in_next:
82
+ return
83
+ capture.finished = True
84
+ try:
85
+ complete(capture.body(), capture.error, not capture.eof)
86
+ except Exception:
87
+ logger.warning("stream completed-hook telemetry failed", exc_info=True)
88
+
89
+ def iter_bytes(*args: Any, **kwargs: Any):
90
+ iterator = iter(original_iter(*args, **kwargs))
91
+ try:
92
+ while True:
93
+ capture.in_next = True
94
+ try:
95
+ chunk = next(iterator)
96
+ except StopIteration:
97
+ capture.eof = True
98
+ break
99
+ finally:
100
+ capture.in_next = False
101
+ capture.feed(chunk)
102
+ yield chunk
103
+ except BaseException as exc:
104
+ capture.failed(exc)
105
+ raise
106
+ finally:
107
+ try:
108
+ capture.in_next = True
109
+ if hasattr(iterator, "close"):
110
+ iterator.close()
111
+ finally:
112
+ capture.in_next = False
113
+ finish()
114
+
115
+ def close() -> None:
116
+ try:
117
+ original_close()
118
+ except BaseException as exc:
119
+ capture.failed(exc)
120
+ raise
121
+ finally:
122
+ finish()
123
+
124
+ response.iter_bytes = iter_bytes
125
+ response.close = close
126
+
127
+
128
+ def capture_async_response_stream(
129
+ response: Any,
130
+ *,
131
+ max_chars: int,
132
+ capture_text: bool,
133
+ complete: Callable[[str | None, str | None, bool], Awaitable[None]],
134
+ ) -> None:
135
+ """Async counterpart; never prefetches chunks or schedules background work."""
136
+ capture = _Capture(response, max_chars, capture_text)
137
+ original_iter = response.aiter_bytes
138
+ original_close = response.aclose
139
+
140
+ async def finish() -> None:
141
+ if capture.finished or capture.in_next:
142
+ return
143
+ capture.finished = True
144
+ try:
145
+ await complete(capture.body(), capture.error, not capture.eof)
146
+ except Exception:
147
+ logger.warning("stream completed-hook telemetry failed", exc_info=True)
148
+
149
+ async def aiter_bytes(*args: Any, **kwargs: Any):
150
+ iterator = original_iter(*args, **kwargs).__aiter__()
151
+ try:
152
+ while True:
153
+ capture.in_next = True
154
+ try:
155
+ chunk = await anext(iterator)
156
+ except StopAsyncIteration:
157
+ capture.eof = True
158
+ break
159
+ finally:
160
+ capture.in_next = False
161
+ capture.feed(chunk)
162
+ yield chunk
163
+ except BaseException as exc:
164
+ capture.failed(exc)
165
+ raise
166
+ finally:
167
+ try:
168
+ capture.in_next = True
169
+ if hasattr(iterator, "aclose"):
170
+ await iterator.aclose()
171
+ finally:
172
+ capture.in_next = False
173
+ await finish()
174
+
175
+ async def aclose() -> None:
176
+ try:
177
+ await original_close()
178
+ except BaseException as exc:
179
+ capture.failed(exc)
180
+ raise
181
+ finally:
182
+ await finish()
183
+
184
+ response.aiter_bytes = aiter_bytes
185
+ response.aclose = aclose
@@ -77,6 +77,8 @@ class InstrumentationManager:
77
77
  # the captured original send already carries the request hook.
78
78
  if http_instrumentation.install_httpx_body_capture():
79
79
  self._installed_targets.append("httpx_body_capture")
80
+ if http_instrumentation.install_httpx2_body_capture():
81
+ self._installed_targets.append("httpx2_body_capture")
80
82
  if http_instrumentation.install_urllib3():
81
83
  self._installed_targets.append("urllib3")
82
84
  if http_instrumentation.install_urllib():
@@ -122,6 +124,7 @@ class InstrumentationManager:
122
124
  # Reverse install order: unwind the send patch before OTel httpx.
123
125
  http_instrumentation.uninstall_urllib()
124
126
  http_instrumentation.uninstall_urllib3()
127
+ http_instrumentation.uninstall_httpx2_body_capture()
125
128
  http_instrumentation.uninstall_httpx_body_capture()
126
129
  http_instrumentation.uninstall_httpx()
127
130
  http_instrumentation.uninstall_requests()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openbox-sdk-python"
3
- version = "1.3.0"
3
+ version = "1.4.0"
4
4
  description = "OpenBox base SDK - governance contracts, strict gate, identity/signing, evaluate client, context runtime, OTel span wire serialization, and generic instrumentation shared by every OpenBox framework SDK"
5
5
  authors = [
6
6
  { name = "OpenBox Team", email = "tino@openbox.ai" },
@@ -26,7 +26,7 @@ dependencies = [
26
26
  "opentelemetry-api>=1.38.0,<1.40.0",
27
27
  "opentelemetry-sdk>=1.38.0,<1.40.0",
28
28
  "httpx>=0.28.0,<1",
29
- "cryptography>=48.0.1,<50",
29
+ "cryptography>=50,<51",
30
30
  ]
31
31
 
32
32
  [project.optional-dependencies]
@@ -53,6 +53,7 @@ db = [
53
53
 
54
54
  [dependency-groups]
55
55
  dev = [
56
+ "httpx2>=2.0.0,<3",
56
57
  "pytest>=8.2",
57
58
  "pytest-asyncio>=0.26.0,<0.27",
58
59
  "ruff>=0.8.0",
@@ -142,7 +142,7 @@ class TestFactoriesValidation:
142
142
 
143
143
  def test_workflow_failed_carries_error(self):
144
144
  body = workflow_failed(**WF, error="boom").to_payload_dict()
145
- assert body["error"] == "boom"
145
+ assert body["error"] == {"type": "Exception", "message": "boom"}
146
146
 
147
147
 
148
148
  class TestRfc3339: