imperal-sdk 4.2.1__tar.gz → 4.2.3__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 (206) hide show
  1. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/CHANGELOG.md +88 -0
  2. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/PKG-INFO +1 -1
  3. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/__init__.py +7 -1
  4. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/extension.py +92 -0
  5. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/manifest.py +7 -0
  6. imperal_sdk-4.2.3/src/imperal_sdk/secrets/__init__.py +29 -0
  7. imperal_sdk-4.2.3/src/imperal_sdk/secrets/client.py +238 -0
  8. imperal_sdk-4.2.3/src/imperal_sdk/secrets/exceptions.py +34 -0
  9. imperal_sdk-4.2.3/src/imperal_sdk/secrets/panel_handler.py +133 -0
  10. imperal_sdk-4.2.3/src/imperal_sdk/secrets/spec.py +66 -0
  11. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/testing/__init__.py +2 -0
  12. imperal_sdk-4.2.3/src/imperal_sdk/testing/mock_secrets.py +76 -0
  13. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/.github/workflows/identity-contract.yml +0 -0
  14. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/.github/workflows/publish.yml +0 -0
  15. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/.github/workflows/test.yml +0 -0
  16. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/.gitignore +0 -0
  17. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/LICENSE +0 -0
  18. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/README.md +0 -0
  19. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/api_surface.json +0 -0
  20. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/pyproject.toml +0 -0
  21. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/.codebase-index-cache.pkl +0 -0
  22. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ai/__init__.py +0 -0
  23. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ai/client.py +0 -0
  24. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/auth/__init__.py +0 -0
  25. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/auth/client.py +0 -0
  26. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/auth/middleware.py +0 -0
  27. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/billing/__init__.py +0 -0
  28. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/billing/client.py +0 -0
  29. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/cache/__init__.py +0 -0
  30. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/cache/client.py +0 -0
  31. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/cache/protocol.py +0 -0
  32. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/__init__.py +0 -0
  33. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/action_result.py +0 -0
  34. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/error_codes.py +0 -0
  35. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/extension.py +0 -0
  36. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/filters.py +0 -0
  37. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/guards.py +0 -0
  38. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/handler.py +0 -0
  39. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/kernel_primitives.py +0 -0
  40. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/narration.py +0 -0
  41. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/narration_guard.py +0 -0
  42. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/prompt.py +0 -0
  43. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/refusal.py +0 -0
  44. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/chat/retry.py +0 -0
  45. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/cli/__init__.py +0 -0
  46. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/cli/main.py +0 -0
  47. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/config/__init__.py +0 -0
  48. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/config/client.py +0 -0
  49. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/context.py +0 -0
  50. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/db/__init__.py +0 -0
  51. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/db/client.py +0 -0
  52. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/errors.py +0 -0
  53. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/extensions/__init__.py +0 -0
  54. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/extensions/client.py +0 -0
  55. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/http/__init__.py +0 -0
  56. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/http/client.py +0 -0
  57. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/manifest_schema.py +0 -0
  58. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/notify/__init__.py +0 -0
  59. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/notify/client.py +0 -0
  60. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/prompts/__init__.py +0 -0
  61. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/prompts/icnli_integrity_rules.txt +0 -0
  62. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/prompts/kernel_formatting_rule.txt +0 -0
  63. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/prompts/kernel_proactivity_rule.txt +0 -0
  64. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/protocols.py +0 -0
  65. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/rpc/__init__.py +0 -0
  66. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/rpc/codec.py +0 -0
  67. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/rpc/contract.py +0 -0
  68. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/runtime/__init__.py +0 -0
  69. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/runtime/executor.py +0 -0
  70. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/runtime/llm_provider.py +0 -0
  71. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/runtime/message_adapter.py +0 -0
  72. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/action_result.schema.json +0 -0
  73. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/balance_info.schema.json +0 -0
  74. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/chat_result.schema.json +0 -0
  75. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/completion_result.schema.json +0 -0
  76. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/document.schema.json +0 -0
  77. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/event.schema.json +0 -0
  78. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/file_info.schema.json +0 -0
  79. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/function_call.schema.json +0 -0
  80. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/http_response.schema.json +0 -0
  81. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/imperal.schema.json +0 -0
  82. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/limits_result.schema.json +0 -0
  83. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/schemas/subscription_info.schema.json +0 -0
  84. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/security/__init__.py +0 -0
  85. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/security/call_token.py +0 -0
  86. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/skeleton/__init__.py +0 -0
  87. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/skeleton/client.py +0 -0
  88. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/storage/__init__.py +0 -0
  89. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/storage/client.py +0 -0
  90. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/store/__init__.py +0 -0
  91. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/store/client.py +0 -0
  92. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/store/exceptions.py +0 -0
  93. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/testing/mock_context.py +0 -0
  94. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/tools/__init__.py +0 -0
  95. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/tools/client.py +0 -0
  96. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/tools/generate_api_surface.py +0 -0
  97. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/tools/validate_identity_contract.py +0 -0
  98. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/__init__.py +0 -0
  99. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/action_result.py +0 -0
  100. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/chat_result.py +0 -0
  101. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/client_contracts.py +0 -0
  102. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/contracts.py +0 -0
  103. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/contributions.py +0 -0
  104. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/events.py +0 -0
  105. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/health.py +0 -0
  106. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/identity.py +0 -0
  107. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/models.py +0 -0
  108. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/pagination.py +0 -0
  109. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/types/store_contracts.py +0 -0
  110. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ui/__init__.py +0 -0
  111. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ui/actions.py +0 -0
  112. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ui/base.py +0 -0
  113. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ui/data.py +0 -0
  114. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ui/display.py +0 -0
  115. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ui/feedback.py +0 -0
  116. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ui/graph.py +0 -0
  117. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ui/input_components.py +0 -0
  118. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ui/interactive.py +0 -0
  119. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ui/layout.py +0 -0
  120. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/ui/theme.py +0 -0
  121. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/validator.py +0 -0
  122. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/src/imperal_sdk/validator_v1_6_0.py +0 -0
  123. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/conftest.py +0 -0
  124. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/contracts/__init__.py +0 -0
  125. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/contracts/test_store_contracts.py +0 -0
  126. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/fixtures/openapi/auth-gateway.json +0 -0
  127. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/fixtures/openapi/registry.json +0 -0
  128. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/fixtures/openapi/sharelock-cases.json +0 -0
  129. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/rpc/__init__.py +0 -0
  130. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/rpc/test_codec.py +0 -0
  131. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/rpc/test_contract.py +0 -0
  132. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/runtime/__init__.py +0 -0
  133. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/runtime/test_llm_provider_config_store.py +0 -0
  134. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/runtime/test_llm_provider_ctx_injection.py +0 -0
  135. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/store/__init__.py +0 -0
  136. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/store/test_list_users_client.py +0 -0
  137. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/store/test_query_all_client.py +0 -0
  138. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_action_result_typed.py +0 -0
  139. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_as_user.py +0 -0
  140. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_auth.py +0 -0
  141. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_billing.py +0 -0
  142. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_cache_client.py +0 -0
  143. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_cache_model.py +0 -0
  144. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_call_token.py +0 -0
  145. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_chat_extension_deprecation.py +0 -0
  146. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_chat_filters.py +0 -0
  147. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_chat_guards.py +0 -0
  148. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_chat_guards_bleed.py +0 -0
  149. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_chat_prompt.py +0 -0
  150. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_chat_pydantic_retry.py +0 -0
  151. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_chat_result.py +0 -0
  152. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_cli.py +0 -0
  153. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_client_contracts.py +0 -0
  154. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_config_client.py +0 -0
  155. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_context.py +0 -0
  156. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_context_guards.py +0 -0
  157. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_contracts.py +0 -0
  158. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_contracts_live.py +0 -0
  159. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_contributions.py +0 -0
  160. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_document_contract.py +0 -0
  161. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_emits_decorator.py +0 -0
  162. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_error_codes.py +0 -0
  163. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_errors.py +0 -0
  164. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_event_schema_v2.py +0 -0
  165. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_events_health.py +0 -0
  166. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_extension.py +0 -0
  167. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_extension_v2.py +0 -0
  168. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_extensions_emit.py +0 -0
  169. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_handler_p2.py +0 -0
  170. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_id_shape_guard.py +0 -0
  171. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_identity_contract.py +0 -0
  172. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_imperal_schema_v2.py +0 -0
  173. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_kernel_primitives.py +0 -0
  174. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_manifest.py +0 -0
  175. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_manifest_roundtrip_gate.py +0 -0
  176. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_manifest_schema.py +0 -0
  177. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_manifest_v2_events.py +0 -0
  178. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_manifest_v2_other_sections.py +0 -0
  179. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_manifest_v2_webhooks.py +0 -0
  180. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_manifest_validator_v2.py +0 -0
  181. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_mock_context.py +0 -0
  182. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_models.py +0 -0
  183. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_narration_emission.py +0 -0
  184. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_narration_guard.py +0 -0
  185. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_openai_max_completion_tokens.py +0 -0
  186. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_pagination.py +0 -0
  187. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_panel_rendering_contract.py +0 -0
  188. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_panels.py +0 -0
  189. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_skeleton_decorator.py +0 -0
  190. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_spec_validation.py +0 -0
  191. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_tools_client.py +0 -0
  192. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_ui.py +0 -0
  193. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_ui_fileupload_enhanced.py +0 -0
  194. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_ui_html.py +0 -0
  195. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_ui_image_enhanced.py +0 -0
  196. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_ui_open.py +0 -0
  197. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_ui_theme.py +0 -0
  198. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_user.py +0 -0
  199. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_v7_emit_refusal.py +0 -0
  200. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_validator.py +0 -0
  201. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_validator_drift.py +0 -0
  202. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_validator_pep563.py +0 -0
  203. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_validator_v1_6_0_rules.py +0 -0
  204. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/test_write_arg_bleed.py +0 -0
  205. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/tools/__init__.py +0 -0
  206. {imperal_sdk-4.2.1 → imperal_sdk-4.2.3}/tests/tools/test_generate_api_surface.py +0 -0
@@ -2,6 +2,94 @@
2
2
 
3
3
  All notable changes to `imperal-sdk` are documented here.
4
4
 
5
+ ## 4.2.3 — 2026-05-13
6
+
7
+ **EXT-SECRETS-V1 UX polish — synthetic `secrets` panel auto-injected**
8
+
9
+ When an extension declares one or more `@ext.secret(...)` entries, the SDK
10
+ now auto-registers a synthetic `secrets` panel (slot=`right`, title=`Secrets`,
11
+ icon=`KeyRound`) so the user-facing Secrets manager appears alongside the
12
+ extension's own tabs without the author writing any panel code.
13
+
14
+ ### Added
15
+
16
+ - **Auto-injected `secrets` panel**: `Extension.secret(...)` on first call
17
+ registers a built-in handler from `imperal_sdk.secrets.panel_handler`. The
18
+ handler reads declared secrets + live is_set state from `ctx.secrets.list()`
19
+ and renders `ui.Card` rows with status badges + a `Manage` button that
20
+ routes to the dedicated `/ext/{ext_id}/secrets` page.
21
+ - The synthetic panel uses **slot=`right`** defensively — most extensions use
22
+ `left` (sidebar nav) and `center` (main content); `right` is rarely-used so
23
+ the panel-sync logic in `imperal-ext-developer` won't overwrite it. If your
24
+ extension already declares a `right`-slot panel, your panel wins; users
25
+ still reach the Secrets UI via the chat-top ribbon and the direct
26
+ `/ext/{ext_id}/secrets` route.
27
+ - Panel idempotent — multiple `@ext.secret(...)` calls register the panel
28
+ only once.
29
+
30
+ ### Migration notes
31
+
32
+ - **No code changes required** to receive the synthetic panel. Bump your
33
+ ext's SDK pin to `>= 4.2.3` and redeploy via Dev Portal.
34
+ - Once your extension is on v4.2.3+, the chat-top ribbon (added in Panel for
35
+ v4.2.2 discoverability) becomes redundant for that ext — synthetic panel
36
+ appears in the `right` slot as a proper tab.
37
+
38
+ ## 4.2.2 — 2026-05-13
39
+
40
+ ### Added — EXT-SECRETS-V1 (closes ARCH-D1 in compliance-posture.md)
41
+
42
+ - **`@ext.secret(name, description, ...)`** declarative decorator. Extensions
43
+ declare what user-supplied credentials they need (API keys, OAuth tokens,
44
+ webhook signing secrets). Each declaration carries `required`,
45
+ `write_mode` (`user` / `extension` / `both`), `max_bytes`, optional
46
+ `rotation_hint_days`. Manifest emits `secrets[]` as an additive optional
47
+ field (manifest schema v3 stays — back-compat).
48
+
49
+ - **`ctx.secrets`** accessor on `KernelContext` (resolved kernel-side; SDK
50
+ ships `SecretClient` HTTP proxy to auth-gw `/v1/secrets/*`). Methods:
51
+ `get(name)` → plaintext or None; `set(name, value)` → raises
52
+ `SecretWriteForbidden` for `write_mode='user'`; `delete(name)`;
53
+ `is_set(name)` (cheap metadata, no audit); `list()` (descriptions +
54
+ is_set, never values).
55
+
56
+ - **Dev mode** (`IMPERAL_DEV_MODE=true`): `get(name)` reads
57
+ `IMPERAL_SECRET_<UPPER_NAME>` env var; set/delete are no-ops with WARN
58
+ log. Manifest contract still enforced (I-SECRETS-CONTRACT-DECLARED —
59
+ undeclared names raise even in dev).
60
+
61
+ - **`imperal_sdk.testing.MockSecretStore`** for pytest fixtures. Optional
62
+ `declared` set to mirror SecretNotDeclaredError semantics.
63
+
64
+ - **Federal invariants enforced SDK-side**:
65
+ - `I-SECRETS-HANDLER-SCOPE-MEMORY` — no module/class-level plaintext
66
+ cache in `SecretClient`; source-inspection-friendly
67
+ - `I-SECRETS-CONTRACT-DECLARED` — runtime read/write of undeclared
68
+ name raises `SecretNotDeclaredError`; manifest is single source of truth
69
+ - `I-SECRETS-VAULT-DEPENDENCY` — auth-gw 503 → `SecretVaultUnavailable`
70
+
71
+ - **Federal invariants enforced auth-gw-side** (live in production
72
+ whm-gateway since 2026-05-13):
73
+ - `I-SECRETS-USER-SCOPED` — cross-user 403
74
+ - `I-SECRETS-NEVER-LOGGED` — `action_ledger` row stores length +
75
+ sha256-prefix-8 only, never the value
76
+ - `I-SECRETS-EXT-SCOPED` — extension token's `ext_id` claim must
77
+ match URL `{ext_id}`
78
+ - `I-SECRETS-AUDIT-FOREVER` — every op writes
79
+ `retention_class='security_forever'`
80
+
81
+ - **New JWT claims**: `actor_kind` (`'user'` or `'extension'`) and `ext_id`
82
+ (extension tokens only). `build_session_claims` and
83
+ `build_extension_claims` helpers in `app.auth.claims` on the auth-gw side.
84
+
85
+ ### Notes
86
+
87
+ - Migration of existing plaintext-stored credentials (BYOLLM keys, OAuth
88
+ refresh tokens, etc.) is at extension-author pace; no automated migration.
89
+ The V32 publish-time validator blocks *new* extensions that read
90
+ credential-like fields without an `@ext.secret` declaration
91
+ (validator implementation deferred).
92
+
5
93
  ## 4.2.1 — 2026-05-11
6
94
 
7
95
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imperal-sdk
3
- Version: 4.2.1
3
+ Version: 4.2.3
4
4
  Summary: SDK for building Imperal Cloud extensions
5
5
  Author: Valentin Scerbacov, Imperal, Inc.
6
6
  License-Expression: AGPL-3.0-or-later
@@ -33,7 +33,13 @@ from imperal_sdk.validator_v1_6_0 import (
33
33
  validate_manifest_v1_6_0,
34
34
  )
35
35
 
36
- __version__ = "4.2.1"
36
+ from imperal_sdk.secrets import (
37
+ SecretSpec, SecretClient, SecretStatus,
38
+ SecretNotDeclaredError, SecretWriteForbidden, SecretVaultUnavailable,
39
+ SecretValueTooLarge, SecretDeclarationConflict,
40
+ )
41
+
42
+ __version__ = "4.2.3"
37
43
 
38
44
  __all__ = [
39
45
  # Core
@@ -139,6 +139,98 @@ class Extension:
139
139
  self._panels: dict[str, dict] = {}
140
140
  self._tray: dict[str, "TrayDef"] = {}
141
141
  self._cache_models: dict[str, type] = {}
142
+ # EXT-SECRETS-V1 (v4.2.2) — declared secrets emitted into manifest.secrets[]
143
+ self._secrets: dict[str, "SecretSpec"] = {}
144
+
145
+ def secret(
146
+ self,
147
+ name: str,
148
+ description: str,
149
+ *,
150
+ required: bool = False,
151
+ write_mode: str = "user",
152
+ max_bytes: int = 4096,
153
+ rotation_hint_days: int | None = None,
154
+ ):
155
+ """Declare a secret the extension needs.
156
+
157
+ Federal EXT-SECRETS-V1 contract — manifest.secrets[] is the single
158
+ source of truth for what an extension may touch via ``ctx.secrets``.
159
+
160
+ Usage::
161
+
162
+ ext.secret(
163
+ name="spotify_api_key",
164
+ description="Your Spotify API key (from developer.spotify.com)",
165
+ required=True,
166
+ write_mode="user", # user pastes in Panel UI
167
+ max_bytes=200,
168
+ )(lambda: None)
169
+
170
+ # OAuth refresh tokens are written by the ext after authorize
171
+ ext.secret(
172
+ name="spotify_refresh_token",
173
+ description="OAuth refresh token written by ext after authorize",
174
+ write_mode="extension",
175
+ rotation_hint_days=30,
176
+ )(lambda: None)
177
+
178
+ Returns an identity decorator — the wrapped target is unchanged.
179
+ The call itself registers the SecretSpec on the Extension.
180
+ """
181
+ from imperal_sdk.secrets.spec import SecretSpec
182
+ from imperal_sdk.secrets.exceptions import SecretDeclarationConflict
183
+
184
+ spec = SecretSpec(
185
+ name=name,
186
+ description=description,
187
+ required=required,
188
+ write_mode=write_mode,
189
+ max_bytes=max_bytes,
190
+ rotation_hint_days=rotation_hint_days,
191
+ )
192
+ if name in self._secrets:
193
+ raise SecretDeclarationConflict(
194
+ f"@ext.secret name={name!r} declared twice on app_id={self.app_id!r}"
195
+ )
196
+ self._secrets[name] = spec
197
+
198
+ # EXT-SECRETS-V1 (v4.2.3): auto-register synthetic 'secrets' panel
199
+ # on first @ext.secret call so the Secrets tab appears alongside
200
+ # the extension's own tabs without the author writing panel code.
201
+ # The handler is shipped in imperal_sdk.secrets.panel_handler.
202
+ # Subsequent @ext.secret calls are no-ops here — registration is
203
+ # idempotent.
204
+ #
205
+ # Slot choice: ``right`` is defensively chosen. Most extensions
206
+ # use ``left`` (sidebar nav) and ``center`` (main content); ``right``
207
+ # is rarely-used so the synthetic Secrets tab is least likely to be
208
+ # overwritten by the deploy-sync logic in imperal-ext-developer
209
+ # which currently keeps only one panel per slot. If your extension
210
+ # already uses ``right``, your panel wins; users can still reach
211
+ # the Secrets UI via the chat-top ribbon and at /ext/{ext_id}/secrets.
212
+ if "secrets" not in self._panels:
213
+ from imperal_sdk.secrets.panel_handler import (
214
+ builtin_secrets_panel_handler,
215
+ )
216
+ # Call self.panel(...) the same way an extension author would.
217
+ self.panel(
218
+ "secrets",
219
+ slot="right",
220
+ title="Secrets",
221
+ icon="KeyRound",
222
+ refresh="manual",
223
+ )(builtin_secrets_panel_handler)
224
+
225
+ def _decorator(target):
226
+ return target # syntactic anchor only — decorator is a no-op wrapper
227
+
228
+ return _decorator
229
+
230
+ @property
231
+ def secrets(self) -> dict[str, "SecretSpec"]:
232
+ """Read-only view of declared secrets keyed by name."""
233
+ return dict(self._secrets)
142
234
 
143
235
  def tool(self, name: str, scopes: list[str] | None = None, description: str = ""):
144
236
  """Register a tool that the AI assistant can call."""
@@ -176,6 +176,13 @@ def generate_manifest(ext: Extension) -> dict:
176
176
  if ext.config_defaults:
177
177
  manifest["config_defaults"] = ext.config_defaults
178
178
 
179
+ # EXT-SECRETS-V1 (v4.2.2) — emit declared secrets[]. Optional field;
180
+ # backwards-compatible (extensions without @ext.secret omit it).
181
+ if getattr(ext, "_secrets", None):
182
+ manifest["secrets"] = [
183
+ s.to_manifest_dict() for s in ext._secrets.values()
184
+ ]
185
+
179
186
  return manifest
180
187
 
181
188
 
@@ -0,0 +1,29 @@
1
+ """EXT-SECRETS-V1 — federal per-user per-extension encrypted secrets.
2
+
3
+ User-facing surfaces:
4
+ - ``@ext.secret(name, description, ...)`` declares a secret in the manifest.
5
+ - ``ctx.secrets.get(name)`` reads plaintext (only inside handler scope).
6
+ - ``ctx.secrets.set(name, value)`` writes (only when write_mode allows).
7
+
8
+ See: superpowers/specs/2026-05-12-ext-secrets-v1-design.md
9
+ """
10
+ from imperal_sdk.secrets.spec import SecretSpec
11
+ from imperal_sdk.secrets.client import SecretClient, SecretStatus
12
+ from imperal_sdk.secrets.exceptions import (
13
+ SecretNotDeclaredError,
14
+ SecretWriteForbidden,
15
+ SecretVaultUnavailable,
16
+ SecretValueTooLarge,
17
+ SecretDeclarationConflict,
18
+ )
19
+
20
+ __all__ = [
21
+ "SecretSpec",
22
+ "SecretClient",
23
+ "SecretStatus",
24
+ "SecretNotDeclaredError",
25
+ "SecretWriteForbidden",
26
+ "SecretVaultUnavailable",
27
+ "SecretValueTooLarge",
28
+ "SecretDeclarationConflict",
29
+ ]
@@ -0,0 +1,238 @@
1
+ """SecretClient — thin HTTP proxy from SDK to auth-gw /v1/secrets/*.
2
+
3
+ Federal contract:
4
+ - NEVER caches plaintext between calls (I-SECRETS-HANDLER-SCOPE-MEMORY).
5
+ - Validates name against manifest declarations (I-SECRETS-CONTRACT-DECLARED).
6
+ - Validates write_mode before PUT/DELETE/rotate.
7
+ - Translates auth-gw 503 → SecretVaultUnavailable.
8
+ - Returns None for 404 SECRET_NOT_SET (declared but no value yet).
9
+
10
+ Dev mode (when ``IMPERAL_DEV_MODE=true``):
11
+ - get(name) reads ``IMPERAL_SECRET_<UPPER_NAME>`` env var
12
+ - set/delete/rotate are no-ops with a WARN log (manifest contract still enforced)
13
+ - list() reflects env-var presence
14
+
15
+ Pytest: inject a MockSecretStore via fixture; see imperal_sdk.testing.MockSecretStore.
16
+ """
17
+ import logging
18
+ import os
19
+ from dataclasses import dataclass
20
+ from typing import Optional
21
+
22
+ import httpx
23
+
24
+ from imperal_sdk.secrets.exceptions import (
25
+ SecretNotDeclaredError,
26
+ SecretWriteForbidden,
27
+ SecretVaultUnavailable,
28
+ SecretValueTooLarge,
29
+ )
30
+ from imperal_sdk.secrets.spec import SecretSpec
31
+
32
+ log = logging.getLogger(__name__)
33
+
34
+ SDK_HTTP_TIMEOUT_S = 5.0
35
+
36
+
37
+ @dataclass
38
+ class SecretStatus:
39
+ """Returned by ``ctx.secrets.list()``. NEVER carries the value itself."""
40
+ name: str
41
+ description: str
42
+ is_set: bool
43
+ last_accessed_at: Optional[int]
44
+
45
+
46
+ def _dev_mode_active() -> bool:
47
+ return os.getenv("IMPERAL_DEV_MODE", "").lower() in {"1", "true", "yes", "on"}
48
+
49
+
50
+ def _dev_env_key(name: str) -> str:
51
+ return f"IMPERAL_SECRET_{name.upper()}"
52
+
53
+
54
+ class SecretClient:
55
+ """Source-inspection contract: NO module-level cache, NO @lru_cache,
56
+ NO instance attribute holding plaintext between calls. Plaintext is
57
+ only ever a local variable in get()'s return path."""
58
+
59
+ def __init__(
60
+ self,
61
+ *,
62
+ ext_id: str,
63
+ imperal_id: str,
64
+ auth_gw_base: str,
65
+ session_token: str,
66
+ declared: dict[str, SecretSpec],
67
+ ):
68
+ self._ext_id = ext_id
69
+ self._imperal_id = imperal_id
70
+ self._base = auth_gw_base.rstrip("/")
71
+ self._token = session_token
72
+ self._declared = declared
73
+
74
+ def _headers(self, *, json: bool = False) -> dict:
75
+ h = {
76
+ "Authorization": f"Bearer {self._token}",
77
+ "X-Acting-User": self._imperal_id,
78
+ "X-Ext-Id": self._ext_id,
79
+ }
80
+ if json:
81
+ h["Content-Type"] = "application/json"
82
+ return h
83
+
84
+ def _ensure_declared(self, name: str) -> SecretSpec:
85
+ if name not in self._declared:
86
+ raise SecretNotDeclaredError(
87
+ f"secret name={name!r} not in manifest for ext_id={self._ext_id!r}"
88
+ )
89
+ return self._declared[name]
90
+
91
+ async def get(self, name: str) -> Optional[str]:
92
+ self._ensure_declared(name)
93
+
94
+ if _dev_mode_active():
95
+ return os.getenv(_dev_env_key(name))
96
+
97
+ try:
98
+ async with httpx.AsyncClient(timeout=SDK_HTTP_TIMEOUT_S) as c:
99
+ r = await c.get(
100
+ f"{self._base}/v1/secrets/{self._ext_id}/{name}",
101
+ headers=self._headers(),
102
+ )
103
+ except (httpx.ConnectError, httpx.TimeoutException, httpx.HTTPError) as e:
104
+ raise SecretVaultUnavailable(
105
+ f"auth-gw unreachable on get(name={name!r}): {type(e).__name__}"
106
+ ) from None
107
+ if r.status_code == 200:
108
+ return r.json().get("value")
109
+ if r.status_code == 404:
110
+ return None
111
+ if r.status_code == 503:
112
+ raise SecretVaultUnavailable(
113
+ f"auth-gw 503 on get(name={name!r})"
114
+ )
115
+ raise RuntimeError(
116
+ f"unexpected auth-gw status {r.status_code} on get(name={name!r})"
117
+ )
118
+
119
+ async def set(self, name: str, value: str) -> None:
120
+ spec = self._ensure_declared(name)
121
+ if spec.write_mode == "user":
122
+ raise SecretWriteForbidden(
123
+ f"secret name={name!r} has write_mode='user'; only Panel UI can "
124
+ f"write. Declare write_mode='extension' or 'both' to allow."
125
+ )
126
+ if len(value.encode("utf-8")) > spec.max_bytes:
127
+ raise SecretValueTooLarge(
128
+ f"value ({len(value.encode())} bytes) exceeds "
129
+ f"max_bytes={spec.max_bytes} for name={name!r}"
130
+ )
131
+ if _dev_mode_active():
132
+ log.warning(
133
+ "secret writes ignored in dev mode (name=%s, ext_id=%s)",
134
+ name, self._ext_id,
135
+ )
136
+ return
137
+
138
+ try:
139
+ async with httpx.AsyncClient(timeout=SDK_HTTP_TIMEOUT_S) as c:
140
+ r = await c.put(
141
+ f"{self._base}/v1/secrets/{self._ext_id}/{name}",
142
+ headers=self._headers(json=True),
143
+ json={"value": value},
144
+ )
145
+ except (httpx.ConnectError, httpx.TimeoutException, httpx.HTTPError) as e:
146
+ raise SecretVaultUnavailable(
147
+ f"auth-gw unreachable on set(name={name!r}): {type(e).__name__}"
148
+ ) from None
149
+ if r.status_code == 200:
150
+ return
151
+ if r.status_code == 503:
152
+ raise SecretVaultUnavailable(f"auth-gw 503 on set(name={name!r})")
153
+ raise RuntimeError(
154
+ f"unexpected auth-gw status {r.status_code} on set(name={name!r})"
155
+ )
156
+
157
+ async def delete(self, name: str) -> bool:
158
+ spec = self._ensure_declared(name)
159
+ if spec.write_mode == "user":
160
+ raise SecretWriteForbidden(
161
+ f"secret name={name!r} write_mode='user' — only Panel can delete"
162
+ )
163
+ if _dev_mode_active():
164
+ log.warning("secret deletes ignored in dev mode (name=%s)", name)
165
+ return False
166
+
167
+ try:
168
+ async with httpx.AsyncClient(timeout=SDK_HTTP_TIMEOUT_S) as c:
169
+ r = await c.delete(
170
+ f"{self._base}/v1/secrets/{self._ext_id}/{name}",
171
+ headers=self._headers(),
172
+ )
173
+ except (httpx.ConnectError, httpx.TimeoutException, httpx.HTTPError) as e:
174
+ raise SecretVaultUnavailable(
175
+ f"auth-gw unreachable on delete(name={name!r}): {type(e).__name__}"
176
+ ) from None
177
+ if r.status_code == 200:
178
+ return bool(r.json().get("was_set", False))
179
+ raise RuntimeError(
180
+ f"unexpected auth-gw status {r.status_code} on delete(name={name!r})"
181
+ )
182
+
183
+ async def is_set(self, name: str) -> bool:
184
+ self._ensure_declared(name)
185
+ if _dev_mode_active():
186
+ return os.getenv(_dev_env_key(name)) is not None
187
+
188
+ try:
189
+ async with httpx.AsyncClient(timeout=SDK_HTTP_TIMEOUT_S) as c:
190
+ r = await c.get(
191
+ f"{self._base}/v1/secrets/{self._ext_id}/{name}/meta",
192
+ headers=self._headers(),
193
+ )
194
+ except (httpx.ConnectError, httpx.TimeoutException, httpx.HTTPError) as e:
195
+ raise SecretVaultUnavailable(
196
+ f"auth-gw unreachable on is_set(name={name!r}): {type(e).__name__}"
197
+ ) from None
198
+ if r.status_code == 200:
199
+ return bool(r.json().get("is_set", False))
200
+ if r.status_code == 404:
201
+ return False
202
+ raise RuntimeError(
203
+ f"unexpected auth-gw status {r.status_code} on is_set(name={name!r})"
204
+ )
205
+
206
+ async def list(self) -> list[SecretStatus]:
207
+ if _dev_mode_active():
208
+ return [
209
+ SecretStatus(
210
+ name=n,
211
+ description=s.description,
212
+ is_set=os.getenv(_dev_env_key(n)) is not None,
213
+ last_accessed_at=None,
214
+ )
215
+ for n, s in self._declared.items()
216
+ ]
217
+
218
+ try:
219
+ async with httpx.AsyncClient(timeout=SDK_HTTP_TIMEOUT_S) as c:
220
+ r = await c.get(
221
+ f"{self._base}/v1/secrets/{self._ext_id}",
222
+ headers=self._headers(),
223
+ )
224
+ except (httpx.ConnectError, httpx.TimeoutException, httpx.HTTPError) as e:
225
+ raise SecretVaultUnavailable(
226
+ f"auth-gw unreachable on list(): {type(e).__name__}"
227
+ ) from None
228
+ if r.status_code == 200:
229
+ return [
230
+ SecretStatus(
231
+ name=item.get("name", ""),
232
+ description=item.get("description", ""),
233
+ is_set=bool(item.get("is_set", False)),
234
+ last_accessed_at=item.get("last_accessed_at"),
235
+ )
236
+ for item in r.json()
237
+ ]
238
+ raise RuntimeError(f"unexpected auth-gw status {r.status_code} on list()")
@@ -0,0 +1,34 @@
1
+ """Secret-related SDK exceptions. Federal rule: NEVER embed plaintext in messages."""
2
+
3
+
4
+ class SecretNotDeclaredError(Exception):
5
+ """ctx.secrets.* called with a name not in the manifest's secrets[].
6
+
7
+ Manifest is the single source of truth for what an extension may touch
8
+ (I-SECRETS-CONTRACT-DECLARED).
9
+ """
10
+
11
+
12
+ class SecretWriteForbidden(Exception):
13
+ """ctx.secrets.set() called for a secret with manifest write_mode='user'.
14
+
15
+ Only the Panel UI (user-attributable session) can write 'user'-mode
16
+ secrets. Extension code can write secrets declared with
17
+ write_mode='extension' or write_mode='both'.
18
+ """
19
+
20
+
21
+ class SecretVaultUnavailable(Exception):
22
+ """auth-gw returned 503; Vault transit endpoint is down.
23
+
24
+ Per I-SECRETS-VAULT-DEPENDENCY, the SDK fails closed — no fallback
25
+ decryption, no cached plaintext.
26
+ """
27
+
28
+
29
+ class SecretValueTooLarge(Exception):
30
+ """Written value exceeds the manifest's max_bytes for this secret."""
31
+
32
+
33
+ class SecretDeclarationConflict(Exception):
34
+ """@ext.secret declared the same name twice for one Extension."""
@@ -0,0 +1,133 @@
1
+ """Built-in handler for the synthetic ``secrets`` panel.
2
+
3
+ When an Extension declares one or more ``@ext.secret(...)`` entries, the SDK
4
+ auto-registers a panel with ``panel_id='secrets'`` so the user-facing
5
+ Secrets manager appears among the extension's other tabs (Overview /
6
+ Analytics / Transactions / ...). The extension author doesn't write any
7
+ panel code — the SDK provides this canonical handler.
8
+
9
+ Federal contract: the handler renders a summary of declared secrets +
10
+ links to the full /ext/{ext_id}/secrets page where Panel UI handles
11
+ PUT/DELETE through type=password input + cleared-on-submit state. We
12
+ do NOT inline the input form here because the canonical UI lives in
13
+ Panel React (SecretManagerCard) — duplicating it in declarative ui.*
14
+ primitives would split the federal contract (I-SECRETS-NEVER-LOGGED,
15
+ no-echo, no-clipboard) across two implementations.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Any
20
+
21
+
22
+ async def builtin_secrets_panel_handler(ctx: Any, **_params: Any) -> dict:
23
+ """Render the synthetic secrets panel.
24
+
25
+ Returns a UINode dict (post-``.to_dict()``) that the kernel relays to
26
+ Imperal Panel. The panel shows one card per declared secret with
27
+ is_set status badge and a primary action that navigates to
28
+ /ext/{ext_id}/secrets — the full SecretManagerCard route.
29
+ """
30
+ # Import inside the handler to keep SDK init light and avoid
31
+ # circular imports if ui.* itself imports anything from secrets.
32
+ from imperal_sdk import ui
33
+
34
+ ext_id = getattr(ctx, "ext_id", "") or getattr(ctx, "extension_id", "") or ""
35
+
36
+ # Pull declared secrets from ctx if the kernel populated them; fall
37
+ # back to an empty list so the panel still renders cleanly.
38
+ declared = list(getattr(ctx, "_declared_secrets", {}).values()) if getattr(
39
+ ctx, "_declared_secrets", None
40
+ ) else []
41
+
42
+ # Try to fetch live is_set state per declared name (cheap meta read).
43
+ statuses: dict[str, dict] = {}
44
+ if hasattr(ctx, "secrets") and ctx.secrets is not None:
45
+ try:
46
+ live = await ctx.secrets.list()
47
+ for s in live:
48
+ # SecretStatus or dict — handle both.
49
+ name = getattr(s, "name", None) or (s.get("name") if isinstance(s, dict) else None)
50
+ if name is None:
51
+ continue
52
+ statuses[name] = {
53
+ "is_set": bool(getattr(s, "is_set", None) if not isinstance(s, dict) else s.get("is_set", False)),
54
+ "last_accessed_at": (
55
+ getattr(s, "last_accessed_at", None)
56
+ if not isinstance(s, dict)
57
+ else s.get("last_accessed_at")
58
+ ),
59
+ }
60
+ except Exception:
61
+ statuses = {}
62
+
63
+ cards = []
64
+ for spec in declared:
65
+ name = getattr(spec, "name", None) or (spec.get("name") if isinstance(spec, dict) else None)
66
+ if not name:
67
+ continue
68
+ desc = getattr(spec, "description", "") or (spec.get("description", "") if isinstance(spec, dict) else "")
69
+ write_mode = (
70
+ getattr(spec, "write_mode", "user")
71
+ if not isinstance(spec, dict)
72
+ else spec.get("write_mode", "user")
73
+ )
74
+ status = statuses.get(name, {"is_set": False, "last_accessed_at": None})
75
+ is_set = bool(status.get("is_set"))
76
+ last_read = status.get("last_accessed_at")
77
+
78
+ # One card per secret with status + Manage button.
79
+ rows = [ui.Text(desc) if desc else None]
80
+ if is_set:
81
+ rows.append(ui.Badge("Set", color="green"))
82
+ if last_read:
83
+ rows.append(ui.Text(f"Last read: {last_read}", color="muted"))
84
+ else:
85
+ rows.append(ui.Badge("Not set", color="gray"))
86
+ if write_mode == "extension":
87
+ rows.append(ui.Text("(extension-write only — written after authorize)", color="muted"))
88
+
89
+ rows.append(ui.Button(
90
+ label="Manage" if is_set else "Set value",
91
+ variant="primary" if not is_set else "secondary",
92
+ on_click=ui.Navigate(path=f"/ext/{ext_id}/secrets#{name}"),
93
+ ))
94
+
95
+ cards.append(ui.Card(
96
+ title=name,
97
+ content=ui.Stack(children=[r for r in rows if r is not None]),
98
+ ))
99
+
100
+ if not cards:
101
+ cards.append(ui.Card(
102
+ title="No secrets declared",
103
+ content=ui.Stack(children=[
104
+ ui.Text(
105
+ "This extension does not declare any secrets. If you are "
106
+ "the developer, add an @ext.secret(...) declaration to "
107
+ "your app.py and redeploy."
108
+ ),
109
+ ui.Link(
110
+ text="Read @ext.secret reference →",
111
+ href="https://docs.imperal.io/en/sdk/decorator-secret-reference/",
112
+ ),
113
+ ]),
114
+ ))
115
+
116
+ root = ui.Stack(children=[
117
+ ui.Heading(f"Secrets · {ext_id}", level=2),
118
+ ui.Text(
119
+ "Credentials this extension needs — API keys, OAuth tokens. "
120
+ "Stored encrypted in Vault; never visible to admins or in logs."
121
+ ),
122
+ *cards,
123
+ ui.Link(
124
+ text=f"Open full Secrets manager →",
125
+ href=f"/ext/{ext_id}/secrets",
126
+ ),
127
+ ])
128
+
129
+ # Match the wrapper contract from @ext.panel decorator — return .to_dict()
130
+ # of the root. The kernel wraps {"ui": ..., "panel_id": "secrets"}.
131
+ if hasattr(root, "to_dict"):
132
+ return root.to_dict()
133
+ return root